Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 6 additions & 4 deletions src/tools/wasm/loader.rs
Original file line number Diff line number Diff line change
Expand Up @@ -152,8 +152,8 @@ impl WasmToolLoader {
tracing::warn!(
tool = name,
?errors,
"Invalid parameters schema in capabilities.json, \
using permissive fallback"
"Invalid parameters schema in capabilities.json; \
falling back to permissive parameters"
);
None
}
Expand All @@ -171,14 +171,16 @@ impl WasmToolLoader {
tool = name,
path = %cap_path.display(),
"Capabilities file missing \"parameters\" field; \
tool will accept any JSON object (permissive fallback)"
tool will rely on the WASM export schema if present, \
otherwise use permissive parameters"
);
}
(caps, oauth, desc, params)
} else {
tracing::warn!(
path = %cap_path.display(),
"Capabilities file not found, using default (no permissions)"
"Capabilities file not found, using default (no permissions) and \
relying on the WASM export schema if present"
);
(Capabilities::default(), None, None, None)
}
Expand Down
81 changes: 59 additions & 22 deletions src/tools/wasm/wrapper.rs
Original file line number Diff line number Diff line change
Expand Up @@ -482,8 +482,8 @@ pub struct WasmToolWrapper {
struct WasmToolSchemas {
/// Compact schema advertised in the main tools array.
///
/// This stays permissive by default to avoid serializing full exported
/// WASM schemas on every LLM call. Sidecars can override it explicitly.
/// Uses the extracted WASM schema when it is typed and small enough to
/// advertise safely. Sidecars can override it explicitly.
advertised: serde_json::Value,
/// Full schema available for discovery and runtime parameter preparation.
///
Expand All @@ -493,6 +493,8 @@ struct WasmToolSchemas {
}

impl WasmToolSchemas {
const MAX_ADVERTISED_SCHEMA_BYTES: usize = 64 * 1024;

fn permissive_schema() -> serde_json::Value {
serde_json::json!({
"type": "object",
Expand Down Expand Up @@ -521,9 +523,23 @@ impl WasmToolSchemas {
.unwrap_or(0)
}

fn is_reasonably_small_schema(schema: &serde_json::Value) -> bool {
serde_json::to_string(schema)
.map(|json| json.len() <= Self::MAX_ADVERTISED_SCHEMA_BYTES)
.unwrap_or(false)
}
Comment thread
G7CNF marked this conversation as resolved.

fn new(discovery: serde_json::Value) -> Self {
let advertised = if Self::typed_property_count(&discovery) > 0
&& Self::is_reasonably_small_schema(&discovery)
{
discovery.clone()
Comment on lines +533 to +536

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Validate exported WASM schemas before advertising them

If a WASM extension's schema() export is only partially valid (for example, it has one typed property but also an array property without items or an orphaned required key), this branch now publishes that raw schema to the LLM/provider because WasmToolSchemas::new() only checks typed_property_count() and size. Unlike sidecar schemas in src/tools/wasm/loader.rs, exported schemas never go through validate_tool_schema(), and the provider-side normalizer in src/llm/rig_adapter.rs only adds strict-mode fields; it does not repair those structural errors. Before this change we fell back to the permissive {} schema, so malformed exports did not break function-calling for the tool.

Useful? React with 👍 / 👎.

Comment on lines 532 to +536
} else {
Self::permissive_schema()
};

Self {
advertised: Self::permissive_schema(),
advertised,
discovery,
}
}
Expand Down Expand Up @@ -1490,7 +1506,7 @@ mod tests {
}

#[tokio::test]
async fn test_advertised_schema_stays_permissive_until_sidecar_override() {
async fn test_typed_wasm_schema_is_advertised_to_llm() {
let discovery_schema = serde_json::json!({
"type": "object",
"properties": {
Expand All @@ -1510,30 +1526,18 @@ mod tests {
wrapper.schemas = super::WasmToolSchemas::new(discovery_schema.clone());
wrapper.description = "Search documents".to_string();

// Advertised schema stays permissive; discovery holds the typed schema
assert_eq!(
wrapper.parameters_schema(),
serde_json::json!({
"type": "object",
"properties": {},
"additionalProperties": true
})
);
// Typed discovery schema is advertised directly to the LLM
assert_eq!(wrapper.parameters_schema(), discovery_schema);
assert_eq!(wrapper.discovery_schema(), discovery_schema);

// Raw description is clean — no tool_info hint baked in
assert!(!wrapper.description().contains("tool_info"));

// But schema() composes the hint at display time when advertised is permissive
// schema() should not compose a tool_info hint when the schema is typed
let schema = wrapper.schema();
assert!(
schema.description.contains("tool_info"),
"schema().description should contain tool_info hint: {}",
schema.description
);
assert!(
schema.description.contains("include_schema: true"),
"hint should mention include_schema: true: {}",
!schema.description.contains("tool_info"),
"schema().description should not contain tool_info hint when typed: {}",
schema.description
);

Expand All @@ -1558,7 +1562,6 @@ mod tests {
);
assert_eq!(wrapper.discovery_schema(), wrapper.parameters_schema());

// With typed schema, schema() should NOT include tool_info hint
let schema = wrapper.schema();
assert!(
!schema.description.contains("tool_info"),
Expand All @@ -1567,6 +1570,40 @@ mod tests {
);
}

#[tokio::test]
async fn test_untyped_wasm_schema_stays_permissive() {
let discovery_schema = serde_json::json!({
"type": "object",
"properties": {}
});

let runtime = Arc::new(WasmToolRuntime::new(WasmRuntimeConfig::for_testing()).unwrap());
let prepared = runtime
.prepare("search", b"\0asm\x0d\0\x01\0", None)
.await
.unwrap();
let mut wrapper =
super::WasmToolWrapper::new(Arc::clone(&runtime), prepared, Capabilities::default());
wrapper.schemas = super::WasmToolSchemas::new(discovery_schema);
wrapper.description = "Search documents".to_string();

assert_eq!(
wrapper.parameters_schema(),
serde_json::json!({
"type": "object",
"properties": {},
"additionalProperties": true
})
);

let schema = wrapper.schema();
assert!(
schema.description.contains("tool_info"),
"schema().description should contain tool_info hint when untyped: {}",
schema.description
);
}

#[test]
fn test_capabilities_default() {
let caps = Capabilities::default();
Expand Down
Loading