From ef58d23e50080b725575be9a22c0fc73108e978d Mon Sep 17 00:00:00 2001 From: Mykhailo Chalyi Date: Wed, 20 May 2026 20:54:02 -0500 Subject: [PATCH] refactor(deps): remove unused dependency edges --- Cargo.lock | 60 ----- Cargo.toml | 5 - crates/bashkit-cli/Cargo.toml | 2 - crates/bashkit-js/Cargo.toml | 1 - crates/bashkit/Cargo.toml | 1 - crates/bashkit/fuzz/Cargo.lock | 276 ++++++++++++++++---- crates/bashkit/src/builtins/http.rs | 62 +++-- crates/bashkit/src/scripted_tool/execute.rs | 10 +- crates/bashkit/src/scripted_tool/mod.rs | 7 +- crates/bashkit/src/scripted_tool/toolset.rs | 8 +- crates/bashkit/src/tool.rs | 73 +++++- specs/threat-model.md | 4 +- supply-chain/config.toml | 8 - 13 files changed, 342 insertions(+), 175 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 9c6b963c..4fd23f25 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -377,7 +377,6 @@ dependencies = [ "reqwest", "russh", "rustls", - "schemars", "serde", "serde_json", "serial_test", @@ -418,8 +417,6 @@ dependencies = [ "bashkit", "clap", "rustyline", - "serde", - "serde_json", "signal-hook", "tempfile", "terminal_size", @@ -465,7 +462,6 @@ dependencies = [ "napi", "napi-build", "napi-derive", - "serde", "serde_json", "tokio", ] @@ -4073,26 +4069,6 @@ dependencies = [ "bitflags", ] -[[package]] -name = "ref-cast" -version = "1.0.25" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f354300ae66f76f1c85c5f84693f0ce81d747e2c3f21a45fef496d89c960bf7d" -dependencies = [ - "ref-cast-impl", -] - -[[package]] -name = "ref-cast-impl" -version = "1.0.25" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "regex" version = "1.12.3" @@ -4616,31 +4592,6 @@ dependencies = [ "windows-sys 0.61.2", ] -[[package]] -name = "schemars" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2b42f36aa1cd011945615b92222f6bf73c599a102a300334cd7f8dbeec726cc" -dependencies = [ - "dyn-clone", - "ref-cast", - "schemars_derive", - "serde", - "serde_json", -] - -[[package]] -name = "schemars_derive" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d115b50f4aaeea07e79c1912f645c7513d81715d0420f8bc77a18c6260b307f" -dependencies = [ - "proc-macro2", - "quote", - "serde_derive_internals", - "syn", -] - [[package]] name = "scoped-tls" version = "1.0.1" @@ -4756,17 +4707,6 @@ dependencies = [ "syn", ] -[[package]] -name = "serde_derive_internals" -version = "0.29.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "serde_json" version = "1.0.149" diff --git a/Cargo.toml b/Cargo.toml index 664257f1..6976a61d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -96,17 +96,12 @@ fail = "0.5" # Property-based testing proptest = "1" -# JSON Schema generation -schemars = "1" - # Logging/tracing tracing = "0.1" tower = { version = "0.5", features = ["util"] } # SSH client (for ssh/scp/sftp builtins) russh = "0.60" -russh-keys = "0.49" - # Embedded SQLite engine (Turso, pure Rust). Upstream is BETA — gated behind # the `sqlite` feature and disabled by default; see specs/sqlite-builtin.md. turso_core = "0.6" diff --git a/crates/bashkit-cli/Cargo.toml b/crates/bashkit-cli/Cargo.toml index e54fcc5c..6af64d78 100644 --- a/crates/bashkit-cli/Cargo.toml +++ b/crates/bashkit-cli/Cargo.toml @@ -35,8 +35,6 @@ bashkit = { path = "../bashkit", version = "0.6.0", features = ["http_client", " tokio = { workspace = true, features = ["macros", "net", "rt", "rt-multi-thread", "time"] } clap.workspace = true anyhow.workspace = true -serde.workspace = true -serde_json.workspace = true rustyline = { version = "18", optional = true } terminal_size = { version = "0.4", optional = true } signal-hook = { version = "0.4", optional = true } diff --git a/crates/bashkit-js/Cargo.toml b/crates/bashkit-js/Cargo.toml index 696881f7..f44a44ee 100644 --- a/crates/bashkit-js/Cargo.toml +++ b/crates/bashkit-js/Cargo.toml @@ -17,7 +17,6 @@ crate-type = ["cdylib"] bashkit = { path = "../bashkit", features = ["scripted_tool", "python", "realfs", "jq", "interop", "sqlite"] } napi = { workspace = true } napi-derive = { workspace = true } -serde = { workspace = true } serde_json = { workspace = true } tokio = { version = "1", features = ["sync", "macros", "io-util", "rt", "time"] } diff --git a/crates/bashkit/Cargo.toml b/crates/bashkit/Cargo.toml index aa928b68..eaeb5784 100644 --- a/crates/bashkit/Cargo.toml +++ b/crates/bashkit/Cargo.toml @@ -28,7 +28,6 @@ anyhow = { workspace = true } # Serialization serde = { workspace = true } serde_json = { workspace = true } -schemars = { workspace = true } # Regex regex = { workspace = true } diff --git a/crates/bashkit/fuzz/Cargo.lock b/crates/bashkit/fuzz/Cargo.lock index ef6ba723..1aade181 100644 --- a/crates/bashkit/fuzz/Cargo.lock +++ b/crates/bashkit/fuzz/Cargo.lock @@ -113,7 +113,7 @@ checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" [[package]] name = "bashkit" -version = "0.5.0" +version = "0.6.0" dependencies = [ "anyhow", "async-trait", @@ -125,13 +125,12 @@ dependencies = [ "flate2", "futures-core", "futures-util", - "getrandom", + "getrandom 0.4.2", "hmac", "md-5", "num-traits", "os_display", "regex", - "schemars", "serde", "serde_json", "sha1", @@ -180,6 +179,12 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" +[[package]] +name = "bitflags" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" + [[package]] name = "block-buffer" version = "0.12.0" @@ -356,10 +361,10 @@ dependencies = [ ] [[package]] -name = "dyn-clone" -version = "1.0.20" +name = "equivalent" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" [[package]] name = "fancy-regex" @@ -388,6 +393,12 @@ dependencies = [ "miniz_oxide", ] +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + [[package]] name = "form_urlencoded" version = "1.2.2" @@ -438,15 +449,43 @@ name = "getrandom" version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi 5.3.0", + "wasip2", +] + +[[package]] +name = "getrandom" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" dependencies = [ "cfg-if", "js-sys", "libc", - "r-efi", + "r-efi 6.0.0", "wasip2", + "wasip3", "wasm-bindgen", ] +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash", +] + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + [[package]] name = "heck" version = "0.5.0" @@ -576,6 +615,12 @@ dependencies = [ "zerovec", ] +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + [[package]] name = "idna" version = "1.1.0" @@ -597,6 +642,18 @@ dependencies = [ "icu_properties", ] +[[package]] +name = "indexmap" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown 0.17.1", + "serde", + "serde_core", +] + [[package]] name = "is_terminal_polyfill" version = "1.70.2" @@ -615,7 +672,7 @@ version = "0.1.34" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" dependencies = [ - "getrandom", + "getrandom 0.3.4", "libc", ] @@ -629,6 +686,12 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + [[package]] name = "libc" version = "0.2.182" @@ -759,6 +822,16 @@ dependencies = [ "zerovec", ] +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn", +] + [[package]] name = "proc-macro2" version = "1.0.106" @@ -784,24 +857,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" [[package]] -name = "ref-cast" -version = "1.0.25" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f354300ae66f76f1c85c5f84693f0ce81d747e2c3f21a45fef496d89c960bf7d" -dependencies = [ - "ref-cast-impl", -] - -[[package]] -name = "ref-cast-impl" -version = "1.0.25" +name = "r-efi" +version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" [[package]] name = "regex" @@ -839,29 +898,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" [[package]] -name = "schemars" -version = "1.2.1" +name = "semver" +version = "1.0.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2b42f36aa1cd011945615b92222f6bf73c599a102a300334cd7f8dbeec726cc" -dependencies = [ - "dyn-clone", - "ref-cast", - "schemars_derive", - "serde", - "serde_json", -] - -[[package]] -name = "schemars_derive" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d115b50f4aaeea07e79c1912f645c7513d81715d0420f8bc77a18c6260b307f" -dependencies = [ - "proc-macro2", - "quote", - "serde_derive_internals", - "syn", -] +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" [[package]] name = "serde" @@ -893,17 +933,6 @@ dependencies = [ "syn", ] -[[package]] -name = "serde_derive_internals" -version = "0.29.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "serde_json" version = "1.0.149" @@ -1099,6 +1128,12 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + [[package]] name = "unit-prefix" version = "0.5.2" @@ -1138,6 +1173,15 @@ dependencies = [ "wit-bindgen", ] +[[package]] +name = "wasip3" +version = "0.4.0+wasi-0.3.0-rc-2026-01-06" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" +dependencies = [ + "wit-bindgen", +] + [[package]] name = "wasm-bindgen" version = "0.2.111" @@ -1183,6 +1227,40 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "wasm-encoder" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" +dependencies = [ + "anyhow", + "indexmap", + "wasm-encoder", + "wasmparser", +] + +[[package]] +name = "wasmparser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +dependencies = [ + "bitflags", + "hashbrown 0.15.5", + "indexmap", + "semver", +] + [[package]] name = "windows-core" version = "0.62.2" @@ -1256,6 +1334,88 @@ name = "wit-bindgen" version = "0.51.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" +dependencies = [ + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen-core" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" +dependencies = [ + "anyhow", + "heck", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" +dependencies = [ + "anyhow", + "heck", + "indexmap", + "prettyplease", + "syn", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" +dependencies = [ + "anyhow", + "bitflags", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" +dependencies = [ + "anyhow", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] [[package]] name = "writeable" diff --git a/crates/bashkit/src/builtins/http.rs b/crates/bashkit/src/builtins/http.rs index 98ea59e4..9480772e 100644 --- a/crates/bashkit/src/builtins/http.rs +++ b/crates/bashkit/src/builtins/http.rs @@ -200,9 +200,7 @@ fn build_url_with_query(base_url: &str, items: &[ItemType]) -> String { if query_items.is_empty() { return base_url.to_string(); } - let encoded: String = url::form_urlencoded::Serializer::new(String::new()) - .extend_pairs(query_items) - .finish(); + let encoded = form_urlencode_pairs(query_items); let sep = if base_url.contains('?') { "&" } else { "?" }; format!("{}{}{}", base_url, sep, encoded) } @@ -244,9 +242,38 @@ fn build_form_body(items: &[ItemType]) -> String { } }) .collect(); - url::form_urlencoded::Serializer::new(String::new()) - .extend_pairs(form_items) - .finish() + form_urlencode_pairs(form_items) +} + +fn form_urlencode_pairs(pairs: Vec<(&str, &str)>) -> String { + let mut out = String::new(); + for (idx, (key, value)) in pairs.into_iter().enumerate() { + if idx > 0 { + out.push('&'); + } + form_urlencode_component(key, &mut out); + out.push('='); + form_urlencode_component(value, &mut out); + } + out +} + +fn form_urlencode_component(input: &str, out: &mut String) { + const HEX: &[u8; 16] = b"0123456789ABCDEF"; + + for byte in input.as_bytes() { + match *byte { + b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'*' | b'-' | b'.' | b'_' => { + out.push(*byte as char); + } + b' ' => out.push('+'), + b => { + out.push('%'); + out.push(HEX[(b >> 4) as usize] as char); + out.push(HEX[(b & 0x0f) as usize] as char); + } + } + } } /// Format the parsed request for display. @@ -644,9 +671,7 @@ mod tests { "foo&admin=true".to_string(), )]; let url = build_url_with_query("https://example.com", &items); - // The & in the value must be encoded, not treated as a param separator - assert!(!url.contains("admin=true")); - assert!(url.contains("q=foo%26admin%3Dtrue") || url.contains("q=foo%26admin=true")); + assert_eq!(url, "https://example.com?q=foo%26admin%3Dtrue"); } #[test] @@ -656,7 +681,7 @@ mod tests { "hello world".to_string(), )]; let url = build_url_with_query("https://example.com", &items); - assert!(url.contains("search=hello")); + assert_eq!(url, "https://example.com?search=hello+world"); } #[test] @@ -666,12 +691,7 @@ mod tests { "admin&role=superadmin".to_string(), )]; let body = build_form_body(&items); - // The & in the value must be encoded - assert!(!body.contains("role=superadmin")); - assert!( - body.contains("user=admin%26role%3Dsuperadmin") - || body.contains("user=admin%26role%3Dsuperadmin") - ); + assert_eq!(body, "user=admin%26role%3Dsuperadmin"); } #[test] @@ -680,4 +700,14 @@ mod tests { let body = build_form_body(&items); assert_eq!(body, "name=test"); } + + #[test] + fn test_form_urlencode_component_utf8_and_reserved_bytes() { + let items = vec![ItemType::JsonField( + "sp ace".to_string(), + "café/tea?".to_string(), + )]; + let body = build_form_body(&items); + assert_eq!(body, "sp+ace=caf%C3%A9%2Ftea%3F"); + } } diff --git a/crates/bashkit/src/scripted_tool/execute.rs b/crates/bashkit/src/scripted_tool/execute.rs index f2338d55..17fe1e17 100644 --- a/crates/bashkit/src/scripted_tool/execute.rs +++ b/crates/bashkit/src/scripted_tool/execute.rs @@ -4,11 +4,11 @@ use super::{ScriptedExecutionTrace, ScriptedTool, ToolDefExtension, extension::I use crate::Bash; use crate::tool::{ Tool, ToolError, ToolExecution, ToolOutputChunk, ToolRequest, ToolResponse, ToolStatus, - VERSION, localized, tool_output_from_response, tool_request_from_value, + VERSION, localized, tool_output_from_response, tool_request_from_value, tool_request_schema, + tool_response_schema, }; use crate::tool_def::usage_from_schema; use async_trait::async_trait; -use schemars::schema_for; use std::collections::VecDeque; use std::sync::{Arc, Mutex}; @@ -193,13 +193,11 @@ impl Tool for ScriptedTool { } fn input_schema(&self) -> serde_json::Value { - let schema = schema_for!(ToolRequest); - serde_json::to_value(schema).unwrap_or_default() + tool_request_schema() } fn output_schema(&self) -> serde_json::Value { - let schema = schema_for!(ToolResponse); - serde_json::to_value(schema).unwrap_or_default() + tool_response_schema() } fn version(&self) -> &str { diff --git a/crates/bashkit/src/scripted_tool/mod.rs b/crates/bashkit/src/scripted_tool/mod.rs index f309c799..4bee32c3 100644 --- a/crates/bashkit/src/scripted_tool/mod.rs +++ b/crates/bashkit/src/scripted_tool/mod.rs @@ -140,7 +140,6 @@ pub use crate::tool_def::{ }; use crate::{ExecutionLimits, Tool, ToolService}; -use schemars::schema_for; use serde::{Deserialize, Serialize}; use std::sync::{Arc, Mutex}; @@ -435,14 +434,12 @@ impl ScriptedToolBuilder { /// Build the input schema without constructing the full tool. pub fn build_input_schema(&self) -> serde_json::Value { - let schema = schema_for!(crate::tool::ToolRequest); - serde_json::to_value(schema).unwrap_or_default() + crate::tool::tool_request_schema() } /// Build the output schema for `ToolOutput::result`. pub fn build_output_schema(&self) -> serde_json::Value { - let schema = schema_for!(crate::tool::ToolResponse); - serde_json::to_value(schema).unwrap_or_default() + crate::tool::tool_response_schema() } } diff --git a/crates/bashkit/src/scripted_tool/toolset.rs b/crates/bashkit/src/scripted_tool/toolset.rs index e055c643..5bff71dd 100644 --- a/crates/bashkit/src/scripted_tool/toolset.rs +++ b/crates/bashkit/src/scripted_tool/toolset.rs @@ -9,9 +9,10 @@ use super::{ CallbackKind, RegisteredTool, ScriptedExecutionTrace, ScriptedTool, ToolArgs, ToolDef, ToolImpl, }; use crate::ExecutionLimits; -use crate::tool::{Tool, ToolError, ToolRequest, ToolResponse, ToolStatus, VERSION}; +use crate::tool::{ + Tool, ToolError, ToolRequest, ToolResponse, ToolStatus, VERSION, tool_response_schema, +}; use async_trait::async_trait; -use schemars::schema_for; use std::sync::Arc; // ============================================================================ @@ -206,8 +207,7 @@ impl Tool for DiscoverTool { } fn output_schema(&self) -> serde_json::Value { - let schema = schema_for!(ToolResponse); - serde_json::to_value(schema).unwrap_or_default() + tool_response_schema() } fn version(&self) -> &str { diff --git a/crates/bashkit/src/tool.rs b/crates/bashkit/src/tool.rs index e1723157..24017777 100644 --- a/crates/bashkit/src/tool.rs +++ b/crates/bashkit/src/tool.rs @@ -65,7 +65,6 @@ use crate::error::Error; use crate::{Bash, ExecResult, ExecutionLimits, OutputCallback}; use async_trait::async_trait; use futures_core::Stream; -use schemars::{JsonSchema, schema_for}; use serde::{Deserialize, Serialize}; use std::collections::HashSet; use std::future::Future; @@ -265,7 +264,7 @@ od xxd hexdump base64 \ kill"; /// Request to execute bash commands -#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] +#[derive(Debug, Clone, Serialize, Deserialize)] pub struct ToolRequest { /// Bash commands to execute (like `bash -c "commands"`) pub commands: String, @@ -287,7 +286,7 @@ impl ToolRequest { } /// Response from executing a bash script -#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema)] +#[derive(Debug, Clone, Default, Serialize, Deserialize)] pub struct ToolResponse { /// Standard output from the script pub stdout: String, @@ -323,6 +322,68 @@ impl From for ToolResponse { } } +/// JSON schema for the stable tool request contract. +pub(crate) fn tool_request_schema() -> serde_json::Value { + serde_json::json!({ + "type": "object", + "required": ["commands"], + "properties": { + "commands": { + "type": "string", + "description": "Bash commands to execute" + }, + "timeout_ms": { + "type": ["integer", "null"], + "format": "uint64", + "minimum": 0, + "description": "Optional per-call timeout in milliseconds" + } + } + }) +} + +/// JSON schema for the stable tool response contract. +pub(crate) fn tool_response_schema() -> serde_json::Value { + serde_json::json!({ + "type": "object", + "required": ["stdout", "stderr", "exit_code"], + "properties": { + "stdout": { + "type": "string", + "description": "Standard output from the script" + }, + "stderr": { + "type": "string", + "description": "Standard error from the script" + }, + "exit_code": { + "type": "integer", + "format": "int32", + "description": "Exit code; 0 means success" + }, + "error": { + "type": ["string", "null"], + "description": "Error message if execution failed before running" + }, + "stdout_truncated": { + "type": "boolean", + "default": false, + "description": "Whether stdout was truncated due to output size limits" + }, + "stderr_truncated": { + "type": "boolean", + "default": false, + "description": "Whether stderr was truncated due to output size limits" + }, + "final_env": { + "type": ["object", "null"], + "additionalProperties": { "type": "string" }, + "description": "Final environment state when requested" + } + } + }) +} + /// Status update during tool execution #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ToolStatus { @@ -683,14 +744,12 @@ impl BashToolBuilder { /// Build the input schema without constructing a full tool. pub fn build_input_schema(&self) -> serde_json::Value { - let schema = schema_for!(ToolRequest); - serde_json::to_value(schema).unwrap_or_default() + tool_request_schema() } /// Build the output schema for `ToolOutput::result`. pub fn build_output_schema(&self) -> serde_json::Value { - let schema = schema_for!(ToolResponse); - serde_json::to_value(schema).unwrap_or_default() + tool_response_schema() } } diff --git a/specs/threat-model.md b/specs/threat-model.md index 73c7b3f9..4792d351 100644 --- a/specs/threat-model.md +++ b/specs/threat-model.md @@ -699,8 +699,8 @@ allowlist.allow("https://api.example.com"); | TM-NET-014 | DNS rebind via redirect | Redirect to rebinded IP | Manual redirect requires allowlist check | **MITIGATED** | | TM-NET-015 | Host proxy leakage | `HTTP_PROXY`/`HTTPS_PROXY` env vars route sandboxed traffic through host proxy | `.no_proxy()` on reqwest builder | **MITIGATED** | | TM-NET-018 | JSON body injection | `http POST url name='x","admin":true'` via unescaped string formatting | Use `serde_json` for JSON construction | **MITIGATED** | -| TM-NET-019 | Query param injection | `http GET url q=='foo&admin=true'` injects extra params | URL-encode via `url::form_urlencoded` | **MITIGATED** | -| TM-NET-020 | Form body injection | `http --form POST url user='x&role=admin'` injects extra fields | URL-encode via `url::form_urlencoded` | **MITIGATED** | +| TM-NET-019 | Query param injection | `http GET url q=='foo&admin=true'` injects extra params | URL-encode via local x-www-form-urlencoded encoder | **MITIGATED** | +| TM-NET-020 | Form body injection | `http --form POST url user='x&role=admin'` injects extra fields | URL-encode via local x-www-form-urlencoded encoder | **MITIGATED** | | TM-NET-021 | Bot identity spoofing | Forge requests as a trusted bot | Ed25519 request signing (bot-auth feature, `specs/request-signing.md`) | **MITIGATED** (opt-in) | | TM-NET-022 | IPv4-mapped IPv6 SSRF bypass | AAAA record returns `::ffff:127.0.0.1`, `::ffff:10.0.0.1`, `::ffff:169.254.169.254`, etc. — embedded v4 address would re-enter the v4 address space at the kernel/socket layer, but the v6 branch of `is_private_ip` only inspected `is_loopback`/`is_unspecified`/`fd00::/8`/`fe80::/10`, treating everything else as public | `is_private_ip` normalizes IPv4-mapped (`::ffff:0:0/96`) and IPv4-compatible (`::a.b.c.d`) v6 forms back to v4 via `Ipv6Addr::to_ipv4_mapped()` and applies the v4 classifier (`is_private_ipv4`). Tested via `test_is_private_ip_v4_mapped_v6_*` cases for loopback, RFC1918, AWS metadata (169.254.169.254), CGNAT, unspecified, public, and IPv4-compatible. Mitigation reaches both the v6 connect-time `PrivateIpFilteringResolver` and the URL/precheck path. | **FIXED** (#1569) | | TM-NET-023 | HTTP-handler SSRF via fail-open precheck and rebind window | `HttpClient::check_private_ip` returned `Ok(())` on URL-parse failures and on URLs with no host, letting malformed targets reach the connect path with no IP filter. Custom `HttpHandler` implementations are doubly exposed because they don't get reqwest's connect-time `PrivateIpFilteringResolver` — even a successful precheck leaves a rebind window between validation and the moment the handler opens its own socket. | (1) `check_private_ip` now fails closed on malformed URLs and on URLs with no host. The direct-IP branch and the successful-DNS branch remain fail-closed against private addresses (including the v4-mapped IPv6 forms covered by TM-NET-022). Tested via `test_check_private_ip_fails_closed_on_invalid_url`, `test_check_private_ip_fails_closed_on_no_host`, `test_check_private_ip_blocks_literal_private_ip`, `test_check_private_ip_blocks_metadata_via_v4_mapped_v6`. (2) The `HttpHandler` trait doc explicitly assigns SSRF responsibility to network-capable custom handlers and points them at `bashkit::network::is_private_ip` for the same classifier the default reqwest path uses. DNS-lookup errors at the precheck still pass through (fail-open by design — the rebind / connect-time threat is the handler's responsibility, and failing closed there breaks legitimate `before_http` hook flows that intentionally target unresolved hostnames). | **MITIGATED** (#1570) | diff --git a/supply-chain/config.toml b/supply-chain/config.toml index e1b77413..3906e39e 100644 --- a/supply-chain/config.toml +++ b/supply-chain/config.toml @@ -1747,14 +1747,6 @@ criteria = "safe-to-run" version = "0.1.29" criteria = "safe-to-deploy" -[[exemptions.schemars]] -version = "1.2.1" -criteria = "safe-to-deploy" - -[[exemptions.schemars_derive]] -version = "1.2.1" -criteria = "safe-to-deploy" - [[exemptions.scoped-tls]] version = "1.0.1" criteria = "safe-to-deploy"