use crate::agentic::{
AutoInjectedParamType, EnrichedAgentMethod, EnrichedParameterSchema, ExtendedAgentConstructor,
};
use crate::golem_agentic::golem::agent::common::{
HttpEndpointDetails, HttpMountDetails, PathSegment, PhantomIdBinding,
};
use std::collections::HashSet;
#[doc(hidden)]
pub fn validate_wire_agent_http(
agent: &crate::golem_agentic::golem::agent::common::AgentType,
) -> Result<(), String> {
use crate::golem_agentic::golem::agent::common::{FieldSource, InputSchema};
use crate::schema::wit::wire::Role;
let names = |input: &InputSchema, principal: bool| {
let InputSchema::Parameters(fields) = input;
fields
.iter()
.filter(|field| matches!(field.source, FieldSource::AutoInjected(_)) == principal)
.map(|field| field.name.clone())
.collect::<HashSet<_>>()
};
let roles = |input: &InputSchema, role: Role| {
let InputSchema::Parameters(fields) = input;
fields
.iter()
.filter(|field| {
matches!(field.source, FieldSource::UserSupplied)
&& agent.schema.type_nodes[field.schema as usize]
.metadata
.role
.as_ref()
.is_some_and(|actual| {
std::mem::discriminant(actual) == std::mem::discriminant(&role)
})
})
.map(|field| field.name.clone())
.collect::<HashSet<_>>()
};
if let Some(mount) = &agent.http_mount {
let input = &agent.constructor.input_schema;
let parameters = names(input, false);
validate_phantom_selector(
mount,
&names(input, false)
.union(&names(input, true))
.cloned()
.collect(),
)?;
if mount.phantom_id_binding.is_some()
&& (agent.mode == crate::golem_agentic::golem::agent::common::AgentMode::Ephemeral
|| agent.kind
== crate::golem_agentic::golem::agent::common::AgentTypeKind::HttpRouter)
{
return Err("Phantom selectors require a regular durable agent".into());
}
validate_no_catch_all_in_http_mount(&agent.type_name, mount)?;
for (role, kind) in [
(Role::UnstructuredText, "UnstructuredText"),
(Role::UnstructuredBinary, "UnstructuredBinary"),
(Role::Multimodal, "Multimodal"),
] {
if let Some(name) = roles(input, role).iter().next() {
return Err(format!(
"Agent '{}' constructor parameter '{}' cannot be of type '{}' when used with HTTP mount",
agent.type_name, name, kind
));
}
}
validate_mount_variables_are_not_principal(mount, &names(input, true))?;
validate_mount_variables_exist_in_constructor(mount, ¶meters)?;
validate_constructor_vars_are_satisfied(mount, ¶meters)?;
}
for method in &agent.methods {
if method.http_endpoint.is_empty() {
continue;
}
if agent.http_mount.is_none() {
return Err(format!(
"Agent method '{}' of '{}' defines HTTP endpoints but the agent is not mounted over HTTP. Please specify mount details in 'agent_definition'",
method.name, agent.type_name
));
}
let input = &method.input_schema;
for endpoint in &method.http_endpoint {
validate_phantom_endpoint(agent.http_mount.as_ref().unwrap(), endpoint)?;
validate_endpoint_variables(
endpoint,
&names(input, false),
&names(input, true),
&roles(input, Role::UnstructuredBinary),
&roles(input, Role::UnstructuredText),
&roles(input, Role::Multimodal),
)?;
}
}
Ok(())
}
pub fn validate_http_mount(
agent_class_name: &str,
agent_mount: &HttpMountDetails,
agent_constructor: &ExtendedAgentConstructor,
parameters_for_principal: &HashSet<String>,
) -> Result<(), String> {
let constructor_input_params = collect_constructor_input_parameter_names(agent_constructor);
let all_parameters = constructor_input_params
.union(parameters_for_principal)
.cloned()
.collect();
validate_phantom_selector(agent_mount, &all_parameters)?;
validate_no_catch_all_in_http_mount(agent_class_name, agent_mount)?;
validate_constructor_params_are_http_safe(agent_class_name, agent_constructor)?;
validate_mount_variables_are_not_principal(agent_mount, parameters_for_principal)?;
validate_mount_variables_exist_in_constructor(agent_mount, &constructor_input_params)?;
validate_constructor_vars_are_satisfied(agent_mount, &constructor_input_params)?;
Ok(())
}
pub fn validate_http_endpoint(
agent_class_name: &str,
agent_method: &EnrichedAgentMethod,
http_mount_details: Option<&HttpMountDetails>,
) -> Result<(), String> {
if agent_method.http_endpoint.is_empty() {
return Ok(());
}
validate_mount_is_defined_for_http_endpoint(
agent_class_name,
agent_method,
http_mount_details,
)?;
let method_vars_without_auto_injected_variables =
collect_method_input_vars(&agent_method.input_schema);
let principal_params = collect_principal_parameter_names(&agent_method.input_schema);
let unstructured_binary_params = collect_unstructured_binary_params(&agent_method.input_schema);
let unstructured_text_params = collect_unstructured_text_params(&agent_method.input_schema);
let multimodal_params = collect_multimodal_params(&agent_method.input_schema);
for endpoint in &agent_method.http_endpoint {
validate_phantom_endpoint(http_mount_details.unwrap(), endpoint)?;
validate_endpoint_variables(
endpoint,
&method_vars_without_auto_injected_variables,
&principal_params,
&unstructured_binary_params,
&unstructured_text_params,
&multimodal_params,
)?;
}
Ok(())
}
fn collect_principal_parameter_names(
input_schema: &[(String, EnrichedParameterSchema)],
) -> HashSet<String> {
let mut principal_params = HashSet::new();
for (param_name, param_schema) in input_schema {
if let EnrichedParameterSchema::AutoInject(auto_injected_schema) = param_schema {
match auto_injected_schema {
AutoInjectedParamType::Principal => {
principal_params.insert(param_name.clone());
}
}
}
}
principal_params
}
fn collect_unstructured_binary_params(
input_schema: &[(String, EnrichedParameterSchema)],
) -> HashSet<String> {
let mut unstructured_binary_params = HashSet::new();
for (param_name, param_schema) in input_schema {
if let EnrichedParameterSchema::Value(schema) = param_schema
&& is_unstructured_binary_schema(schema)
{
unstructured_binary_params.insert(param_name.clone());
}
}
unstructured_binary_params
}
fn collect_unstructured_text_params(
input_schema: &[(String, EnrichedParameterSchema)],
) -> HashSet<String> {
let mut unstructured_text_params = HashSet::new();
for (param_name, param_schema) in input_schema {
if let EnrichedParameterSchema::Value(schema) = param_schema
&& is_unstructured_text_schema(schema)
{
unstructured_text_params.insert(param_name.clone());
}
}
unstructured_text_params
}
fn collect_multimodal_params(
input_schema: &[(String, EnrichedParameterSchema)],
) -> HashSet<String> {
let mut multimodal_params = HashSet::new();
for (param_name, param_schema) in input_schema {
if let EnrichedParameterSchema::Value(schema) = param_schema
&& is_multimodal_schema(schema)
{
multimodal_params.insert(param_name.clone());
}
}
multimodal_params
}
fn collect_method_input_vars(
input_schema: &[(String, EnrichedParameterSchema)],
) -> HashSet<String> {
let mut param_names = HashSet::new();
for (param_name, param_schema) in input_schema {
if let EnrichedParameterSchema::AutoInject(_) = param_schema {
continue;
}
param_names.insert(param_name.clone());
}
param_names
}
fn is_unstructured_text_schema(schema: &crate::schema::SchemaGraph) -> bool {
schema.root.metadata().role.as_ref() == Some(&crate::schema::Role::UnstructuredText)
}
fn is_unstructured_binary_schema(schema: &crate::schema::SchemaGraph) -> bool {
schema.root.metadata().role.as_ref() == Some(&crate::schema::Role::UnstructuredBinary)
}
fn is_multimodal_schema(schema: &crate::schema::SchemaGraph) -> bool {
schema.root.metadata().role.as_ref() == Some(&crate::schema::Role::Multimodal)
}
fn validate_endpoint_variables(
endpoint: &HttpEndpointDetails,
method_vars: &HashSet<String>,
principal_params: &HashSet<String>,
unstructured_binary_params: &HashSet<String>,
unstructured_text_params: &HashSet<String>,
multimodal_params: &HashSet<String>,
) -> Result<(), String> {
fn validate_variable(
variable_name: &str,
location: &str,
principal_params: &HashSet<String>,
unstructured_binary_params: &HashSet<String>,
unstructured_text_params: &HashSet<String>,
multimodal_params: &HashSet<String>,
method_vars: &HashSet<String>,
binary_error: &str,
text_error: &str,
multimodal_error: &str,
) -> Result<(), String> {
if principal_params.contains(variable_name) {
return Err(format!(
"HTTP endpoint {} variable '{}' cannot be used for parameters of type 'Principal'",
location, variable_name
));
}
if unstructured_binary_params.contains(variable_name) {
return Err(binary_error.to_string());
}
if unstructured_text_params.contains(variable_name) {
return Err(text_error.to_string());
}
if multimodal_params.contains(variable_name) {
return Err(multimodal_error.to_string());
}
if !method_vars.contains(variable_name) {
return Err(format!(
"HTTP endpoint {} variable '{}' is not defined in method input parameters.",
location, variable_name
));
}
Ok(())
}
for var in &endpoint.header_vars {
validate_variable(
&var.variable_name,
"header",
principal_params,
unstructured_binary_params,
unstructured_text_params,
multimodal_params,
method_vars,
&format!(
"HTTP endpoint header variable '{}' cannot be used for method parameters of type 'UnstructuredBinary'",
var.variable_name
),
&format!(
"HTTP endpoint header variable '{}' cannot be used for method parameters of type 'UnstructuredText'",
var.variable_name
),
&format!(
"HTTP endpoint header variable '{}' cannot be used for method parameters of type 'Multimodal'",
var.variable_name
),
)?;
}
for var in &endpoint.query_vars {
validate_variable(
&var.variable_name,
"query",
principal_params,
unstructured_binary_params,
unstructured_text_params,
multimodal_params,
method_vars,
&format!(
"HTTP endpoint query variable '{}' cannot be used when the method has a single 'UnstructuredBinary' parameter.",
var.variable_name
),
&format!(
"HTTP endpoint query variable '{}' cannot be used when the method has a single 'UnstructuredText' parameter.",
var.variable_name
),
&format!(
"HTTP endpoint query variable '{}' cannot be used when the method has a single 'Multimodal' parameter.",
var.variable_name
),
)?;
}
for segment in &endpoint.path_suffix {
match segment {
PathSegment::RemainingPathVariable(path_variable)
| PathSegment::PathVariable(path_variable) => {
let name = &path_variable.variable_name;
validate_variable(
name,
"path",
principal_params,
unstructured_binary_params,
unstructured_text_params,
multimodal_params,
method_vars,
&format!(
"HTTP endpoint path variable '{}' cannot be used when the method has a single 'UnstructuredBinary' parameter.",
name
),
&format!(
"HTTP endpoint path variable '{}' cannot be used when the method has a single 'UnstructuredText' parameter.",
name
),
&format!(
"HTTP endpoint path variable '{}' cannot be used when the method has a single 'Multimodal' parameter.",
name
),
)?;
}
PathSegment::Literal(_) => {}
PathSegment::SystemVariable(_) => {}
}
}
Ok(())
}
fn validate_mount_is_defined_for_http_endpoint(
agent_class_name: &str,
agent_method: &EnrichedAgentMethod,
http_mount_details: Option<&HttpMountDetails>,
) -> Result<(), String> {
if http_mount_details.is_none() && !agent_method.http_endpoint.is_empty() {
return Err(format!(
"Agent method '{}' of '{}' defines HTTP endpoints but the agent is not mounted over HTTP. \
Please specify mount details in 'agent_definition'",
agent_method.name, agent_class_name
));
}
Ok(())
}
pub(crate) fn reject_query_param_in_string(path: &str, entity_name: &str) -> Result<(), String> {
if path.contains('?') {
return Err(format!("{} cannot contain query parameters", entity_name));
}
Ok(())
}
pub(crate) fn reject_empty_string(name: &str, entity_name: &str) -> Result<(), String> {
if name.is_empty() {
return Err(format!("{} cannot be empty", entity_name));
}
Ok(())
}
fn validate_no_catch_all_in_http_mount(
agent_class_name: &str,
agent_mount: &HttpMountDetails,
) -> Result<(), String> {
if let Some(catch_all_variable) =
agent_mount
.path_prefix
.iter()
.find_map(|segment| match segment {
PathSegment::RemainingPathVariable(variable) => Some(&variable.variable_name),
_ => None,
})
{
return Err(format!(
"HTTP mount for agent '{}' cannot contain catch-all path variable '{}'",
agent_class_name, catch_all_variable
));
}
Ok(())
}
fn collect_http_mount_variables(agent_mount: &HttpMountDetails) -> HashSet<String> {
let mut vars = HashSet::new();
for segment in &agent_mount.path_prefix {
if let PathSegment::PathVariable(path_variable) = segment {
vars.insert(path_variable.variable_name.clone());
}
}
vars
}
fn collect_constructor_input_parameter_names(
agent_constructor: &ExtendedAgentConstructor,
) -> HashSet<String> {
let mut param_names = HashSet::new();
for (name, schema) in &agent_constructor.input_schema {
if let EnrichedParameterSchema::Value(_) = schema {
param_names.insert(name.clone());
}
}
param_names
}
fn validate_constructor_params_are_http_safe(
agent_class_name: &str,
agent_constructor: &ExtendedAgentConstructor,
) -> Result<(), String> {
for (name, schema) in &agent_constructor.input_schema {
if let EnrichedParameterSchema::Value(schema) = schema {
if is_unstructured_text_schema(schema) {
return Err(format!(
"Agent '{}' constructor parameter '{}' cannot be of type 'UnstructuredText' when used with HTTP mount",
agent_class_name, name
));
}
if is_unstructured_binary_schema(schema) {
return Err(format!(
"Agent '{}' constructor parameter '{}' cannot be of type 'UnstructuredBinary' when used with HTTP mount",
agent_class_name, name
));
}
if is_multimodal_schema(schema) {
return Err(format!(
"Agent '{}' constructor parameter '{}' cannot be of type 'Multimodal' when used with HTTP mount",
agent_class_name, name
));
}
}
}
Ok(())
}
fn validate_mount_variables_are_not_principal(
agent_mount: &HttpMountDetails,
parameters_for_principal: &HashSet<String>,
) -> Result<(), String> {
for segment in &agent_mount.path_prefix {
if let PathSegment::PathVariable(variable) = segment {
let variable_name = &variable.variable_name;
if parameters_for_principal.contains(variable_name) {
return Err(format!(
"HTTP mount path variable '{}' cannot be used for constructor parameters of type 'Principal'",
variable_name,
));
}
}
}
Ok(())
}
fn validate_mount_variables_exist_in_constructor(
agent_mount: &HttpMountDetails,
constructor_vars: &std::collections::HashSet<String>,
) -> Result<(), String> {
for (segment_index, segment) in agent_mount.path_prefix.iter().enumerate() {
if let PathSegment::PathVariable(path_variable) = segment {
let variable_name = &path_variable.variable_name;
if matches!(&agent_mount.phantom_id_binding, Some(PhantomIdBinding::Path(binding)) if &binding.name == variable_name)
{
continue;
}
if !constructor_vars.contains(variable_name) {
return Err(format!(
"HTTP mount path variable '{}' (in path segment {}) is not defined in the agent constructor.",
variable_name, segment_index,
));
}
}
}
Ok(())
}
fn validate_phantom_selector(
mount: &HttpMountDetails,
constructor_vars: &HashSet<String>,
) -> Result<(), String> {
let Some(selector) = &mount.phantom_id_binding else {
return Ok(());
};
let (PhantomIdBinding::Path(binding) | PhantomIdBinding::Query(binding)) = selector;
if binding.name.is_empty() || !mount.filesystem_bindings.is_empty() {
return Err("Phantom selectors require a nonempty name and no filesystem exposure".into());
}
if matches!(selector, PhantomIdBinding::Path(_)) &&
(constructor_vars.contains(&binding.name) || mount.path_prefix.iter().filter(|s| matches!(s, PathSegment::PathVariable(v) if v.variable_name == binding.name)).count() != 1) {
return Err("Phantom path selector must name exactly one mount capture, not a constructor parameter".into());
}
Ok(())
}
fn validate_phantom_endpoint(
mount: &HttpMountDetails,
endpoint: &HttpEndpointDetails,
) -> Result<(), String> {
let conflict = match &mount.phantom_id_binding {
Some(PhantomIdBinding::Path(binding)) => endpoint.path_suffix.iter().any(|s| matches!(s, PathSegment::PathVariable(v) | PathSegment::RemainingPathVariable(v) if v.variable_name == binding.name)),
Some(PhantomIdBinding::Query(binding)) => endpoint.query_vars.iter().any(|q| q.query_param_name == binding.name) || (endpoint.durable_streams.is_some() && ["offset", "cursor", "live"].contains(&binding.name.as_str())),
None => false,
};
if conflict {
return Err(
"Phantom selector conflicts with an endpoint binding or durable-stream control".into(),
);
}
Ok(())
}
fn validate_constructor_vars_are_satisfied(
agent_mount: &HttpMountDetails,
constructor_vars: &std::collections::HashSet<String>,
) -> Result<(), String> {
let provided_vars = collect_http_mount_variables(agent_mount);
for constructor_var in constructor_vars {
if !provided_vars.contains(constructor_var) {
return Err(format!(
"Agent constructor variable '{}' is not provided by the HTTP mount path.",
constructor_var,
));
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use test_r::test;
use super::*;
use crate::agentic::{Schema, StructuredSchema, UnstructuredBinary, UnstructuredText};
use crate::golem_agentic::golem::agent::common::{
AuthDetails, CorsOptions, HeaderVariable, HttpMethod, PathVariable, QueryVariable,
};
use golem_rust_macro::{AllowedLanguages, AllowedMimeTypes};
use std::collections::HashSet;
fn principal_params(names: &[&str]) -> HashSet<String> {
names.iter().map(|s| s.to_string()).collect()
}
fn constructor_with_params(params: Vec<(&str, StructuredSchema)>) -> ExtendedAgentConstructor {
let input_schema = params
.into_iter()
.map(|(name, schema)| {
let schema = match schema {
StructuredSchema::Default(schema) => schema,
StructuredSchema::AutoInject(_) => {
panic!("AutoInjected schema not supported in this test constructor")
}
};
(name.to_string(), EnrichedParameterSchema::Value(schema))
})
.collect();
ExtendedAgentConstructor {
name: None,
description: "".to_string(),
prompt_hint: None,
input_schema,
}
}
fn mount_with_segments(segments: Vec<PathSegment>) -> HttpMountDetails {
HttpMountDetails {
path_prefix: segments,
auth_details: Some(AuthDetails { required: true }),
phantom_agent: false,
phantom_id_binding: None,
cors_options: CorsOptions {
allowed_patterns: vec![],
},
webhook_suffix: vec![],
static_bindings: vec![],
filesystem_bindings: vec![],
file_response_headers: vec![],
openapi_provider_method: None,
}
}
fn path_var(name: &str) -> PathSegment {
PathSegment::PathVariable(PathVariable {
variable_name: name.to_string(),
})
}
fn catch_all(name: &str) -> PathSegment {
PathSegment::RemainingPathVariable(PathVariable {
variable_name: name.to_string(),
})
}
fn literal() -> PathSegment {
PathSegment::Literal("literal".to_string())
}
#[test]
fn phantom_capture_is_not_a_constructor_binding() {
use crate::golem_agentic::golem::agent::common::PhantomIdBindingDetails;
let constructor = constructor_with_params(vec![("customer", String::get_type())]);
let mut mount =
mount_with_segments(vec![literal(), path_var("customer"), path_var("instance")]);
mount.phantom_id_binding = Some(PhantomIdBinding::Path(PhantomIdBindingDetails {
name: "instance".into(),
optional: true,
}));
assert!(validate_http_mount("Selected", &mount, &constructor, &HashSet::new()).is_ok());
mount.path_prefix.push(path_var("other"));
assert!(
validate_http_mount("Selected", &mount, &constructor, &HashSet::new())
.unwrap_err()
.contains("other")
);
mount.path_prefix.pop();
let colliding = constructor_with_params(vec![
("customer", String::get_type()),
("instance", String::get_type()),
]);
assert!(
validate_http_mount("Selected", &mount, &colliding, &HashSet::new())
.unwrap_err()
.contains("constructor parameter")
);
mount.path_prefix.pop();
assert!(
validate_http_mount("Selected", &mount, &constructor, &HashSet::new())
.unwrap_err()
.contains("exactly one")
);
mount.phantom_id_binding = Some(PhantomIdBinding::Query(PhantomIdBindingDetails {
name: "instance".into(),
optional: false,
}));
assert!(validate_http_mount("Selected", &mount, &constructor, &HashSet::new()).is_ok());
mount.path_prefix.push(path_var("instance"));
assert!(validate_http_mount("Selected", &mount, &constructor, &HashSet::new()).is_err());
}
#[test]
fn validate_http_mount_success() {
let constructor = constructor_with_params(vec![
("user_id", String::get_type()),
("org_id", String::get_type()),
]);
let mount = mount_with_segments(vec![literal(), path_var("user_id"), path_var("org_id")]);
let principal_params = principal_params(&[]);
let result = validate_http_mount("MyAgent", &mount, &constructor, &principal_params);
assert!(result.is_ok());
}
#[test]
fn fails_on_catch_all_segment() {
let constructor = constructor_with_params(vec![("id", String::get_type())]);
let mount = mount_with_segments(vec![path_var("id"), catch_all("rest")]);
let err =
validate_http_mount("MyAgent", &mount, &constructor, &HashSet::new()).unwrap_err();
assert_eq!(
err,
"HTTP mount for agent 'MyAgent' cannot contain catch-all path variable 'rest'"
);
}
#[derive(AllowedMimeTypes, Clone, Debug)]
enum MimeTypes {
#[mime_type("application/json")]
ApplicationJson,
Fr,
}
#[test]
fn fails_on_unstructured_binary_constructor_param() {
let constructor =
constructor_with_params(vec![("blob", UnstructuredBinary::<MimeTypes>::get_type())]);
let mount = mount_with_segments(vec![path_var("blob")]);
let err =
validate_http_mount("MyAgent", &mount, &constructor, &HashSet::new()).unwrap_err();
assert_eq!(
err,
"Agent 'MyAgent' constructor parameter 'blob' cannot be of type 'UnstructuredBinary' when used with HTTP mount"
);
}
#[test]
fn fails_when_mount_variable_is_principal() {
let constructor = constructor_with_params(vec![("user", String::get_type())]);
let mount = mount_with_segments(vec![path_var("user")]);
let principal_params = principal_params(&["user"]);
let err =
validate_http_mount("MyAgent", &mount, &constructor, &principal_params).unwrap_err();
assert_eq!(
err,
"HTTP mount path variable 'user' cannot be used for constructor parameters of type 'Principal'"
);
}
#[test]
fn fails_when_mount_variable_not_in_constructor() {
let constructor = constructor_with_params(vec![("id", String::get_type())]);
let mount = mount_with_segments(vec![path_var("missing")]);
let err =
validate_http_mount("MyAgent", &mount, &constructor, &HashSet::new()).unwrap_err();
assert_eq!(
err,
"HTTP mount path variable 'missing' (in path segment 0) is not defined in the agent constructor."
);
}
#[test]
fn fails_when_constructor_var_not_satisfied() {
let constructor = constructor_with_params(vec![
("id", String::get_type()),
("org", String::get_type()),
]);
let mount = mount_with_segments(vec![path_var("id")]);
let err =
validate_http_mount("MyAgent", &mount, &constructor, &HashSet::new()).unwrap_err();
assert_eq!(
err,
"Agent constructor variable 'org' is not provided by the HTTP mount path."
);
}
fn make_schema(
normal_vars: Vec<&str>,
principal_vars: Vec<&str>,
unstructured_vars: Vec<&str>,
) -> Vec<(String, EnrichedParameterSchema)> {
make_schema_with_text(normal_vars, principal_vars, unstructured_vars, vec![])
}
fn make_schema_with_text(
normal_vars: Vec<&str>,
principal_vars: Vec<&str>,
unstructured_binary_vars: Vec<&str>,
unstructured_text_vars: Vec<&str>,
) -> Vec<(String, EnrichedParameterSchema)> {
let mut fields = vec![];
for name in normal_vars {
fields.push((
name.to_string(),
EnrichedParameterSchema::Value(String::get_type().get_schema_graph().unwrap()),
));
}
for name in principal_vars {
fields.push((
name.to_string(),
EnrichedParameterSchema::AutoInject(AutoInjectedParamType::Principal),
));
}
for name in unstructured_binary_vars {
fields.push((
name.to_string(),
EnrichedParameterSchema::Value(crate::agentic::unstructured_binary_schema_graph(
None,
)),
));
}
for name in unstructured_text_vars {
fields.push((
name.to_string(),
EnrichedParameterSchema::Value(crate::agentic::unstructured_text_schema_graph(
None,
)),
));
}
fields
}
fn make_endpoint(
http_method: HttpMethod,
path_vars: Vec<&str>,
header_vars: Vec<(&str, &str)>,
query_vars: Vec<&str>,
auth: Option<bool>,
cors: Vec<&str>,
) -> HttpEndpointDetails {
HttpEndpointDetails {
http_method,
path_suffix: path_vars
.into_iter()
.map(|name| {
PathSegment::PathVariable(PathVariable {
variable_name: name.to_string(),
})
})
.collect(),
header_vars: header_vars
.into_iter()
.map(|(header_name, variable_name)| HeaderVariable {
header_name: header_name.to_string(),
variable_name: variable_name.to_string(),
})
.collect(),
query_vars: query_vars
.into_iter()
.map(|var| QueryVariable {
query_param_name: var.to_string(),
variable_name: var.to_string(),
})
.collect(),
auth_details: auth.map(|b| AuthDetails { required: b }),
cors_options: CorsOptions {
allowed_patterns: cors.into_iter().map(|s| s.to_string()).collect(),
},
durable_streams: None,
}
}
fn make_agent_method(
name: &str,
input_schema: Vec<(String, EnrichedParameterSchema)>,
endpoints: Vec<HttpEndpointDetails>,
) -> EnrichedAgentMethod {
EnrichedAgentMethod {
name: name.to_string(),
description: "".to_string(),
prompt_hint: None,
input_schema,
output_schema: vec![],
http_endpoint: endpoints,
read_only: None,
}
}
#[test]
fn test_no_http_endpoints() {
let agent_method = make_agent_method("foo", make_schema(vec!["x"], vec![], vec![]), vec![]);
assert!(validate_http_endpoint("AgentA", &agent_method, None).is_ok());
}
#[test]
fn test_http_endpoint_without_mount() {
let endpoint = make_endpoint(
HttpMethod::Get,
vec!["x"],
vec![],
vec![],
Some(true),
vec![],
);
let agent_method = make_agent_method(
"foo",
make_schema(vec!["x"], vec![], vec![]),
vec![endpoint],
);
let err = validate_http_endpoint("AgentA", &agent_method, None).unwrap_err();
assert!(err.contains("defines HTTP endpoints but the agent is not mounted over HTTP"));
}
#[test]
fn test_valid_endpoint() {
let endpoint = make_endpoint(
HttpMethod::Get,
vec!["x"],
vec![("X-Test", "x")],
vec!["x"],
Some(true),
vec!["*"],
);
let agent_method = make_agent_method(
"foo",
make_schema(vec!["x"], vec![], vec![]),
vec![endpoint],
);
let mount = mount_with_segments(vec![path_var("foo")]);
assert!(validate_http_endpoint("AgentA", &agent_method, Some(&mount)).is_ok());
}
#[test]
fn test_header_principal_error() {
let endpoint = make_endpoint(
HttpMethod::Get,
vec![],
vec![("X-Test", "p")],
vec![],
Some(true),
vec![],
);
let agent_method = make_agent_method(
"foo",
make_schema(vec![], vec!["p"], vec![]),
vec![endpoint],
);
let mount = mount_with_segments(vec![path_var("foo")]);
let err = validate_http_endpoint("AgentA", &agent_method, Some(&mount)).unwrap_err();
assert!(err.contains("cannot be used for parameters of type 'Principal'"));
}
#[test]
fn test_header_unstructured_binary_error() {
let endpoint = make_endpoint(
HttpMethod::Get,
vec![],
vec![("X-Test", "b")],
vec![],
Some(true),
vec![],
);
let agent_method = make_agent_method(
"foo",
make_schema(vec![], vec![], vec!["b"]),
vec![endpoint],
);
let mount = mount_with_segments(vec![path_var("foo")]);
let err = validate_http_endpoint("AgentA", &agent_method, Some(&mount)).unwrap_err();
assert!(err.contains("cannot be used for method parameters of type 'UnstructuredBinary'"));
}
#[test]
fn test_path_variable_not_in_method_params() {
let endpoint = make_endpoint(
HttpMethod::Get,
vec!["y"],
vec![],
vec![],
Some(true),
vec![],
);
let agent_method = make_agent_method(
"foo",
make_schema(vec!["x"], vec![], vec![]),
vec![endpoint],
);
let mount = mount_with_segments(vec![path_var("foo")]);
let err = validate_http_endpoint("AgentA", &agent_method, Some(&mount)).unwrap_err();
assert!(err.contains("is not defined in method input parameters"));
}
#[derive(AllowedLanguages, Clone, Debug)]
enum Languages {
#[code("en")]
En,
#[code("de")]
De,
}
#[test]
fn fails_on_unstructured_text_constructor_param() {
let constructor =
constructor_with_params(vec![("body", UnstructuredText::<Languages>::get_type())]);
let mount = mount_with_segments(vec![path_var("body")]);
let err =
validate_http_mount("MyAgent", &mount, &constructor, &HashSet::new()).unwrap_err();
assert_eq!(
err,
"Agent 'MyAgent' constructor parameter 'body' cannot be of type 'UnstructuredText' when used with HTTP mount"
);
}
#[test]
fn test_header_unstructured_text_error() {
let endpoint = make_endpoint(
HttpMethod::Get,
vec![],
vec![("X-Test", "t")],
vec![],
Some(true),
vec![],
);
let agent_method = make_agent_method(
"foo",
make_schema_with_text(vec![], vec![], vec![], vec!["t"]),
vec![endpoint],
);
let mount = mount_with_segments(vec![path_var("foo")]);
let err = validate_http_endpoint("AgentA", &agent_method, Some(&mount)).unwrap_err();
assert!(err.contains("cannot be used for method parameters of type 'UnstructuredText'"));
}
#[test]
fn test_query_unstructured_text_error() {
let endpoint = make_endpoint(
HttpMethod::Get,
vec![],
vec![],
vec!["t"],
Some(true),
vec![],
);
let agent_method = make_agent_method(
"foo",
make_schema_with_text(vec![], vec![], vec![], vec!["t"]),
vec![endpoint],
);
let mount = mount_with_segments(vec![path_var("foo")]);
let err = validate_http_endpoint("AgentA", &agent_method, Some(&mount)).unwrap_err();
assert!(err.contains("'UnstructuredText'"));
}
#[test]
fn test_path_unstructured_text_error() {
let endpoint = make_endpoint(
HttpMethod::Get,
vec!["t"],
vec![],
vec![],
Some(true),
vec![],
);
let agent_method = make_agent_method(
"foo",
make_schema_with_text(vec![], vec![], vec![], vec!["t"]),
vec![endpoint],
);
let mount = mount_with_segments(vec![path_var("foo")]);
let err = validate_http_endpoint("AgentA", &agent_method, Some(&mount)).unwrap_err();
assert!(err.contains("'UnstructuredText'"));
}
}