mirror of
https://github.com/hanzoai/mcp.git
synced 2026-10-15 12:30:12 +00:00
The walk recurses with a command's nesting, and a flat chain nests as deep as it is long, so a hostile command overflowed the caller's stack and aborted the server (the classifier's own deep-nesting test died under a debug build). A command over 64 bytes is now read on a thread with a 256 MiB stack, reserved and not committed, and one over 64 KiB is refused unread as unparsed. npx and uvx run through exec, whose classifier asks before a package is fetched, and they had no way to pass the approval: since the merge neither could run at all. They take approve as exec does. The hostile-text bound is 3 s for an optimized build and 15 s for an unoptimized one on a loaded host; it catches a quadratic read, which takes minutes. The unified surface lists the kai tool beside the eight purpose-built ones.
36 lines
1.7 KiB
Rust
36 lines
1.7 KiB
Rust
//! npx, uvx and jq as python-sdk offers them: a package run is an exec of its
|
|
//! argv (in exec's process table), and jq takes its filter as one argv word.
|
|
|
|
use hanzo_mcp::ToolRegistry;
|
|
use serde_json::{json, Value};
|
|
|
|
async fn call(name: &str, args: Value) -> hanzo_mcp::ToolResult {
|
|
ToolRegistry::with_defaults().execute(name, args).await.unwrap()
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn npx_runs_its_argv_through_exec() {
|
|
// A package run fetches from the network, so exec's classifier asks before it runs.
|
|
let asked = ToolRegistry::with_defaults().execute("npx", json!({"package": "--version", "yes": false})).await;
|
|
assert!(asked.as_ref().is_err_and(|e| e.to_string().contains("approval required")), "{asked:?}");
|
|
let r = call("npx", json!({"package": "--version", "yes": false, "approve": true})).await;
|
|
assert_eq!(r.content["status"], "success", "{:?}", r.content);
|
|
assert!(r.content["stdout"].as_str().unwrap().trim().chars().next().unwrap().is_ascii_digit());
|
|
let defs = ToolRegistry::with_defaults().get_definitions();
|
|
for t in ["npx", "uvx", "jq"] {
|
|
assert!(defs.iter().any(|d| d["name"] == t), "{t} is offered");
|
|
}
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn jq_filters_without_a_shell() {
|
|
let r = call("jq", json!({"filter": ".items[] | select(.on != false) | .name", "input": r#"{"items":[{"name":"a","on":true},{"name":"b","on":false}]}"#, "raw": true})).await;
|
|
assert!(r.success, "{:?}", r.error);
|
|
assert_eq!(r.content, json!("a"));
|
|
|
|
let bad = call("jq", json!({"filter": ".", "input": "{nope"})).await;
|
|
assert!(bad.error.unwrap().starts_with("Error: Invalid JSON input"));
|
|
let none = call("jq", json!({"filter": "."})).await;
|
|
assert_eq!(none.error.unwrap(), "Error: Either 'input' or 'file' is required");
|
|
}
|