From eef8b609393f90bb555ebec0bf813f5b3e14088b Mon Sep 17 00:00:00 2001 From: Mykhailo Chalyi Date: Fri, 19 Jun 2026 20:46:16 +0000 Subject: [PATCH 1/2] feat(tool): add BashToolBuilder::configure for full BashBuilder access MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit BashToolBuilder re-declared a handful of BashBuilder knobs (username, hostname, limits, cwd, env, builtins) but couldn't reach the rest — network allowlist, mounts, hooks, git/ssh config, python/typescript, etc. — so tool users had strictly less power than Bash users (the 'inconsistent coverage' half of the duplication problem). Add BashToolBuilder::configure(|b: BashBuilder| ...), a thin-adapter escape hatch that runs against the fresh BashBuilder create_bash builds per execution, after the convenience setters so it can extend or override them. Steps are Arc BashBuilder + Send + Sync> so the tool stays Clone and rebuilds a shell per call. The convenience fields stay (they also feed the help / system-prompt text). Also simplify input_schema / output_schema to call the static schema functions instead of reconstructing a throwaway builder. --- crates/bashkit/src/tool.rs | 79 ++++++++++++++++++++++++++++---------- 1 file changed, 58 insertions(+), 21 deletions(-) diff --git a/crates/bashkit/src/tool.rs b/crates/bashkit/src/tool.rs index efc6cd2f6..b75d21466 100644 --- a/crates/bashkit/src/tool.rs +++ b/crates/bashkit/src/tool.rs @@ -547,6 +547,11 @@ pub trait Tool: Send + Sync { // BashTool - Implementation // ============================================================================ +/// One configuration step applied to a fresh [`BashBuilder`](crate::BashBuilder) +/// each time the tool creates a shell. `Fn` (not `FnOnce`) and `Arc`-wrapped so +/// it can run on every `create_bash` call and the tool stays `Clone`. +type ToolConfigStep = Arc crate::BashBuilder + Send + Sync>; + /// Builder for configuring BashTool #[derive(Default)] pub struct BashToolBuilder { @@ -564,6 +569,11 @@ pub struct BashToolBuilder { env_vars: Vec<(String, String)>, /// Custom builtins (name, implementation). Arc enables reuse across create_bash calls. builtins: Vec<(String, Arc)>, + /// Extra `BashBuilder` configuration applied per shell via [`Self::configure`]. + /// The convenience setters above cover the common knobs and feed the help / + /// system-prompt text; `configure` is the thin-adapter escape hatch for the + /// rest of the `BashBuilder` surface (network, mounts, hooks, git/ssh, …). + config: Vec, } impl BashToolBuilder { @@ -581,6 +591,21 @@ impl BashToolBuilder { self } + /// Configure the underlying [`BashBuilder`](crate::BashBuilder) directly. + /// + /// This is the escape hatch that makes `BashToolBuilder` a thin adapter: + /// any `BashBuilder` capability not surfaced by a convenience method + /// (network allowlist, mounts, hooks, git/ssh config, …) is reachable here. + /// The closure runs on a fresh builder each time the tool creates a shell, + /// after the convenience setters above, so it can extend or override them. + pub fn configure(mut self, f: F) -> Self + where + F: Fn(crate::BashBuilder) -> crate::BashBuilder + Send + Sync + 'static, + { + self.config.push(Arc::new(f)); + self + } + /// Set custom username for virtual identity pub fn username(mut self, username: impl Into) -> Self { self.username = Some(username.into()); @@ -738,6 +763,7 @@ impl BashToolBuilder { cwd: self.cwd.clone(), env_vars: self.env_vars.clone(), builtins: self.builtins.clone(), + config: self.config.clone(), builtin_names, builtin_hints, } @@ -793,6 +819,8 @@ pub struct BashTool { cwd: Option, env_vars: Vec<(String, String)>, builtins: Vec<(String, Arc)>, + /// Extra `BashBuilder` configuration applied per execution (via `configure`). + config: Vec, /// Names of custom builtins (for documentation) builtin_names: Vec, /// LLM hints from registered builtins @@ -805,7 +833,10 @@ impl BashTool { BashToolBuilder::new() } - /// Create a Bash instance with configured settings + /// Create a Bash instance with configured settings. + /// + /// Each configured step runs against a fresh `BashBuilder`, so the tool can + /// build an isolated shell per execution from cloneable configuration. fn create_bash(&self) -> Bash { let mut builder = Bash::builder(); @@ -828,6 +859,11 @@ impl BashTool { for (name, builtin) in &self.builtins { builder = builder.builtin(name.clone(), Box::new(Arc::clone(builtin))); } + // Thin-adapter escape hatch: apply any extra BashBuilder configuration + // last so it can extend or override the convenience settings above. + for step in &self.config { + builder = step(builder); + } builder.build() } @@ -967,29 +1003,11 @@ impl Tool for BashTool { } fn input_schema(&self) -> serde_json::Value { - BashToolBuilder { - locale: self.locale.clone(), - username: self.username.clone(), - hostname: self.hostname.clone(), - limits: self.limits.clone(), - cwd: self.cwd.clone(), - env_vars: self.env_vars.clone(), - builtins: self.builtins.clone(), - } - .build_input_schema() + tool_request_schema() } fn output_schema(&self) -> serde_json::Value { - BashToolBuilder { - locale: self.locale.clone(), - username: self.username.clone(), - hostname: self.hostname.clone(), - limits: self.limits.clone(), - cwd: self.cwd.clone(), - env_vars: self.env_vars.clone(), - builtins: self.builtins.clone(), - } - .build_output_schema() + tool_response_schema() } fn version(&self) -> &str { @@ -1545,6 +1563,25 @@ mod tests { assert!(output.metadata.duration >= Duration::from_millis(0)); } + #[tokio::test] + async fn test_configure_applies_to_underlying_bash_builder() { + // The configure escape hatch reaches BashBuilder directly, and runs + // after the convenience setters so it can override them. + let tool = BashTool::builder() + .env("FROM_CONVENIENCE", "a") + .configure(|b| b.env("FROM_CONFIGURE", "b").username("via_configure")) + .build(); + let output = tool + .execution( + serde_json::json!({"commands": "echo $FROM_CONVENIENCE $FROM_CONFIGURE $USER"}), + ) + .unwrap_or_else(|err| panic!("execution should be created: {err}")) + .execute() + .await + .unwrap_or_else(|err| panic!("execution should succeed: {err}")); + assert_eq!(output.result["stdout"], "a b via_configure\n"); + } + #[tokio::test] async fn test_execution_stream_emits_output_chunks() { use futures_util::StreamExt; From 3f151d762680ee8be5670e7553ebfc4e7626c1c2 Mon Sep 17 00:00:00 2001 From: Mykhailo Chalyi Date: Fri, 19 Jun 2026 20:49:18 +0000 Subject: [PATCH 2/2] docs(tool): note configure() changes aren't in help/system-prompt text Per review: help()/system_prompt() are generated from the convenience setters' stored fields, so values set only inside a configure() closure run but don't show up in the documentation text. Document the caveat and point users at the convenience setters for anything that should be documented. --- crates/bashkit/src/tool.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/crates/bashkit/src/tool.rs b/crates/bashkit/src/tool.rs index b75d21466..ecc83d06b 100644 --- a/crates/bashkit/src/tool.rs +++ b/crates/bashkit/src/tool.rs @@ -598,6 +598,12 @@ impl BashToolBuilder { /// (network allowlist, mounts, hooks, git/ssh config, …) is reachable here. /// The closure runs on a fresh builder each time the tool creates a shell, /// after the convenience setters above, so it can extend or override them. + /// + /// Note: `help()` / `system_prompt()` text is generated from the values set + /// via the convenience setters (`username`, `hostname`, `limits`, `cwd`, + /// `env`, custom builtins), **not** from inside this closure. Changes made + /// only here run for real but are not reflected in the documentation text — + /// use the convenience setters for any value that should be documented. pub fn configure(mut self, f: F) -> Self where F: Fn(crate::BashBuilder) -> crate::BashBuilder + Send + Sync + 'static,