Skip to content
Merged
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
11 changes: 8 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ jobs:
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2

- name: Check bashkit for wasm32-unknown-unknown
run: cargo check -p bashkit --target wasm32-unknown-unknown --features scripted_tool,jq,wasm_js,terminal
run: cargo check -p bashkit -p bashkit-scripted-tool --target wasm32-unknown-unknown --features bashkit/jq,bashkit/wasm_js,bashkit/terminal
env:
# getrandom needs an explicit JS backend opt-in on this target.
RUSTFLAGS: --cfg getrandom_backend="wasm_js" -D warnings
Expand All @@ -100,7 +100,7 @@ jobs:
# as the browser package; no getrandom cfg here (wasip2 has a native
# backend). Check-only: there is no WASI entry-point crate yet.
- name: Check bashkit for wasm32-wasip2
run: cargo check -p bashkit --target wasm32-wasip2 --features scripted_tool,jq,terminal
run: cargo check -p bashkit -p bashkit-scripted-tool --target wasm32-wasip2 --features bashkit/jq,bashkit/terminal
env:
RUSTFLAGS: -D warnings

Expand Down Expand Up @@ -277,6 +277,11 @@ jobs:
- name: Run tests
run: cargo test --workspace --lib --bins --tests --features http_client,ssh,sqlite

# The registry's Python/TypeScript surfaces sit behind the crate's own
# features, which no workspace member turns on together.
- name: Run scripted tool tests (shell, Python, TypeScript surfaces)
run: cargo test -p bashkit-scripted-tool --features python,typescript,jq

# All three differential gates (bash, sed, grep), not just bash: each is
# the only thing checking that its spec expectations still describe the
# real tool. `spec_tests::` + `--ignored` selects exactly the gates —
Expand Down Expand Up @@ -411,7 +416,7 @@ jobs:
cargo run --example custom_fs
cargo run --example clap_builtin
cargo run --example clap_builtin_subcommands
cargo run --example scripted_tool_clap_builtin --features scripted_tool
cargo run -p bashkit-scripted-tool --example scripted_tool_clap_builtin
cargo run --example resource_limits
cargo run --example text_processing
cargo run --example live_mounts
Expand Down
29 changes: 27 additions & 2 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -92,13 +92,38 @@ jobs:
env:
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}

# Publish bashkit-scripted-tool (depends on bashkit)
publish-bashkit-scripted-tool:
name: Publish bashkit-scripted-tool
runs-on: ubuntu-latest
needs: publish-bashkit
environment: release
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false

- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@e081816240890017053eacbb1bdf337761dc5582 # 1.95.0

- name: Wait for crates.io index update
run: sleep 30

- name: Verify bashkit-scripted-tool package without credentials
run: cargo publish --dry-run -p bashkit-scripted-tool

- name: Publish bashkit-scripted-tool to crates.io
run: cargo publish --no-verify -p bashkit-scripted-tool
env:
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}

# ============================================================================
# Verify all packages were published successfully
# ============================================================================
verify-publish:
name: Verify published versions
runs-on: ubuntu-latest
needs: [publish-bashkit, publish-bashkit-cli]
needs: [publish-bashkit, publish-bashkit-cli, publish-bashkit-scripted-tool]
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
Expand All @@ -111,4 +136,4 @@ jobs:
run: |
EXPECTED=$(grep '^version' Cargo.toml | head -1 | sed 's/.*"\(.*\)".*/\1/')
echo "Expected version: $EXPECTED"
python3 scripts/verify_crates_publish.py --expected "$EXPECTED" bashkit bashkit-cli
python3 scripts/verify_crates_publish.py --expected "$EXPECTED" bashkit bashkit-cli bashkit-scripted-tool
19 changes: 19 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 7 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ Homepage: [bashkit.sh](https://bashkit.sh)
- **LLM tool contract** - `BashTool` with discovery metadata, streaming output, and system prompts
- **Script analysis** - Inspect commands, arguments, and file writes *before* running, to drive permission prompts ([guide](docs/script-analysis.md))
- **Snapshotting** - Serialize shell state and VFS contents for checkpoint/resume workflows
- **Scripted tool orchestration** - Compose ToolDef+callback pairs into multi-tool bash scripts (`scripted_tool` feature)
- **Scripted tool orchestration** - Compose ToolDef+callback pairs into multi-tool bash scripts ([`bashkit-scripted-tool`](crates/bashkit-scripted-tool) crate)
- **Async-first** - Built on tokio
- **Language bindings** - Python (PyO3) and JavaScript/TypeScript (NAPI-RS) for Node.js, Bun, and Deno
- **Experimental: Git support** - Virtual git operations on the virtual filesystem (`git` feature)
Expand All @@ -45,7 +45,12 @@ cargo add bashkit --features python # Embedded Python interpreter
cargo add bashkit --features typescript # Embedded TypeScript interpreter
cargo add bashkit --features sqlite # Embedded SQLite engine (Turso)
cargo add bashkit --features realfs # Real filesystem backend
cargo add bashkit --features scripted_tool # Tool orchestration framework
```

Tool orchestration (`ScriptedTool`, `ToolDef`, `ToolRegistry`) is a separate crate:

```bash
cargo add bashkit-scripted-tool
```

## Agent Development
Expand Down
12 changes: 6 additions & 6 deletions contracts/capability-parity.json
Original file line number Diff line number Diff line change
Expand Up @@ -80,12 +80,12 @@
"label": "Rust ScriptedTool",
"supported": ["execution_limits", "deadlines", "output_caps", "env", "stdin", "tool_callbacks"],
"evidence": {
"execution_limits": ["crates/bashkit/src/scripted_tool/execute.rs#test_scripted_tool_forwards_limits_and_output_caps"],
"deadlines": ["crates/bashkit/src/scripted_tool/execute.rs#test_scripted_tool_execute_honors_timeout_ms"],
"output_caps": ["crates/bashkit/src/scripted_tool/execute.rs#test_scripted_tool_forwards_limits_and_output_caps"],
"env": ["crates/bashkit/src/scripted_tool/mod.rs#test_execute_with_env"],
"stdin": ["crates/bashkit/src/scripted_tool/mod.rs#test_execute_stdin_pipe"],
"tool_callbacks": ["crates/bashkit/src/scripted_tool/mod.rs#test_execute_single_tool"]
"execution_limits": ["crates/bashkit-scripted-tool/src/scripted_tool/execute.rs#test_scripted_tool_forwards_limits_and_output_caps"],
"deadlines": ["crates/bashkit-scripted-tool/src/scripted_tool/execute.rs#test_scripted_tool_execute_honors_timeout_ms"],
"output_caps": ["crates/bashkit-scripted-tool/src/scripted_tool/execute.rs#test_scripted_tool_forwards_limits_and_output_caps"],
"env": ["crates/bashkit-scripted-tool/src/scripted_tool/mod.rs#test_execute_with_env"],
"stdin": ["crates/bashkit-scripted-tool/src/scripted_tool/mod.rs#test_execute_stdin_pipe"],
"tool_callbacks": ["crates/bashkit-scripted-tool/src/scripted_tool/mod.rs#test_execute_single_tool"]
},
"unsupported": {
"runtime_env": "Each execution uses a fresh logic-only shell; env is configured before the run.",
Expand Down
1 change: 0 additions & 1 deletion crates/bashkit-cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@ default = ["python", "sqlite", "interactive"]
python = ["bashkit/python"]
sqlite = ["bashkit/sqlite"]
realfs = ["bashkit/realfs"]
scripted_tool = ["bashkit/scripted_tool"]
interactive = ["dep:rustyline", "dep:terminal_size", "dep:signal-hook"]

[dependencies]
Expand Down
3 changes: 2 additions & 1 deletion crates/bashkit-eval/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@ path = "src/main.rs"
# (our own agent loop + provider stack) so we do NOT need mira-everruns.
mira-eval = { version = "0.5", features = ["macros"] }

bashkit = { path = "../bashkit", features = ["scripted_tool", "jq"] }
bashkit = { path = "../bashkit", features = ["jq"] }
bashkit-scripted-tool = { path = "../bashkit-scripted-tool", features = ["jq"] }
tokio = { workspace = true, features = ["rt-multi-thread", "io-std", "io-util", "fs"] }
serde.workspace = true
serde_json.workspace = true
Expand Down
2 changes: 1 addition & 1 deletion crates/bashkit-eval/src/mira_study.rs
Original file line number Diff line number Diff line change
Expand Up @@ -270,7 +270,7 @@ fn scripting_subject() -> impl Subject {
}

fn scripting_transcript(trace: ScriptingTrace) -> Transcript {
use bashkit::ScriptedCommandKind;
use bashkit_scripted_tool::ScriptedCommandKind;

let tool_outputs: Vec<ToolOutput> = trace
.tool_calls
Expand Down
4 changes: 2 additions & 2 deletions crates/bashkit-eval/src/scripting_agent.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@
// one at a time via individual tool_use blocks.

use anyhow::{Context, Result};
use bashkit::{
use bashkit::ToolRequest;
use bashkit_scripted_tool::{
ScriptedCommandInvocation, ScriptedCommandKind, ScriptingToolSet, ToolArgs, ToolDef,
ToolRequest,
};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
Expand Down
3 changes: 2 additions & 1 deletion crates/bashkit-js/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ repository.workspace = true
crate-type = ["cdylib"]

[dependencies]
bashkit = { path = "../bashkit", features = ["scripted_tool", "python", "realfs", "jq", "interop", "http_client", "sqlite"] }
bashkit = { path = "../bashkit", features = ["python", "realfs", "jq", "interop", "http_client", "sqlite"] }
bashkit-scripted-tool = { path = "../bashkit-scripted-tool", features = ["python", "jq"] }
napi = { workspace = true }
napi-derive = { workspace = true }
serde_json = { workspace = true }
Expand Down
4 changes: 2 additions & 2 deletions crates/bashkit-js/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,14 +22,14 @@ use bashkit::{
Credential, ExecResult as RustExecResult, ExecutionLimits, ExtFunctionResult,
FileSystem as BashFileSystem, FileType, InMemoryFs, Metadata, MontyObject, NetworkAllowlist,
OutputCallback, PosixFs, PythonExternalFnHandler, ReadOnlyFs, RealFs, RealFsMode,
ScriptedTool as RustScriptedTool, SnapshotOptions as RustSnapshotOptions, Tool, ToolArgs,
ToolDef, ToolRequest, async_trait,
SnapshotOptions as RustSnapshotOptions, Tool, ToolRequest, async_trait,
};
use bashkit::{
CapabilityFingerprint as RustCapabilityFingerprint, CheckoutPolicy as RustCheckoutPolicy,
CommitOptions as RustCommitOptions, ObjectId as RustObjectId,
SnapshotGraph as RustSnapshotGraph,
};
use bashkit_scripted_tool::{ScriptedTool as RustScriptedTool, ToolArgs, ToolDef};
use napi::bindgen_prelude::External;
use napi::{Env, JsValue, Unknown, ValueType, sys};
use napi_derive::napi;
Expand Down
6 changes: 4 additions & 2 deletions crates/bashkit-python/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,8 @@ num-bigint = "^0.4.6"
# none of which compile on wasm32-unknown-emscripten — see the wasm block
# below and knowledge/runtimes/emscripten-wheels.md.
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
bashkit = { path = "../bashkit", features = ["scripted_tool", "python", "realfs", "jq", "interop", "http_client", "sqlite"] }
bashkit = { path = "../bashkit", features = ["python", "realfs", "jq", "interop", "http_client", "sqlite"] }
bashkit-scripted-tool = { path = "../bashkit-scripted-tool", features = ["python", "jq"] }
tokio = { workspace = true, features = ["rt-multi-thread"] }
# pyo3-async-runtimes hard-pulls tokio `rt-multi-thread` + `net` (mio), which
# `compile_error!`s on wasm. Native-only; the wasm build exposes `execute_sync`
Expand All @@ -49,5 +50,6 @@ pyo3-async-runtimes = { workspace = true }
# wasm-safe base features (sync, macros, io-util, rt, time) — current-thread
# runtime only. See knowledge/runtimes/emscripten-wheels.md.
[target.'cfg(target_arch = "wasm32")'.dependencies]
bashkit = { path = "../bashkit", features = ["scripted_tool", "python", "jq"] }
bashkit = { path = "../bashkit", features = ["python", "jq"] }
bashkit-scripted-tool = { path = "../bashkit-scripted-tool", features = ["python", "jq"] }
tokio = { workspace = true }
6 changes: 3 additions & 3 deletions crates/bashkit-python/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,10 @@ use bashkit::{
ExecutionLimits, ExtFunctionResult, FileSystem, FileSystemExt, FileType as FsFileType,
FsLimits, InMemoryFs, Metadata as FsMetadata, MontyException, MontyObject, NetworkAllowlist,
OutputCallback as RustOutputCallback, OverlayFs, PosixFs, PythonExternalFnHandler,
PythonLimits, ReadOnlyFs, ScriptedTool as RustScriptedTool,
ShellStateView as RustShellStateView, SnapshotOptions as RustSnapshotOptions, Tool, ToolArgs,
ToolDef, ToolRequest, async_trait,
PythonLimits, ReadOnlyFs, ShellStateView as RustShellStateView,
SnapshotOptions as RustSnapshotOptions, Tool, ToolRequest, async_trait,
};
use bashkit_scripted_tool::{ScriptedTool as RustScriptedTool, ToolArgs, ToolDef};

/// Typed named execution-policy selector for Python constructors.
#[pyclass(name = "ExecutionProfile", eq, eq_int, from_py_object)]
Expand Down
56 changes: 56 additions & 0 deletions crates/bashkit-scripted-tool/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
# ScriptedTool and the ToolDef layer, split out of bashkit core.
#
# Important decision: everything built on `ToolDef` lives here, not in
# `bashkit`. Core only offers generic public extension points
# (`RuntimeCallContext`, `ShellFeatures`, `builtin_filter`, Tool contract
# helpers); this crate composes them. Released in lockstep with `bashkit`.
# See knowledge/integrations/scripted-tool-orchestration.md.

[package]
name = "bashkit-scripted-tool"
version.workspace = true
edition.workspace = true
license.workspace = true
authors.workspace = true
repository.workspace = true
description = "ScriptedTool for bashkit: compose ToolDef callbacks into one LLM tool driven by bash scripts"
keywords = ["bash", "llm", "tool", "agent", "everruns"]
categories.workspace = true
readme = "README.md"

[features]
default = []
# Expose registry tools to embedded Python (`python`/`python3` builtins).
python = ["bashkit/python"]
# Expose registry tools to embedded TypeScript (`ts`/`node` builtins).
typescript = ["bashkit/typescript"]
# Admit the optional `jq` builtin in the logic-only shell.
jq = ["bashkit/jq"]
# Debug logs for sanitized callback errors.
tracing = ["dep:tracing"]

[dependencies]
bashkit = { path = "../bashkit", version = "0.18.2", default-features = false, features = ["bash_tool"] }
tokio = { workspace = true }
async-trait = { workspace = true }
tower = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
tracing = { workspace = true, optional = true }

[dev-dependencies]
bashkit = { path = "../bashkit", features = ["bash_tool", "jq"] }
tokio = { workspace = true, features = ["rt-multi-thread", "macros", "time"] }
clap = { workspace = true }
anyhow = { workspace = true }
tokio-test = { workspace = true }

[[example]]
name = "scripted_tool"

[[example]]
name = "scripted_tool_clap_builtin"

[[test]]
name = "tool_registry_tests"
required-features = ["python", "typescript"]
34 changes: 34 additions & 0 deletions crates/bashkit-scripted-tool/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# bashkit-scripted-tool

ScriptedTool for [bashkit](https://crates.io/crates/bashkit): register
`ToolDef` + callback pairs and expose them to an LLM as one tool that accepts a
bash script. Each registered tool becomes a command in a logic-only shell (no
filesystem, no script execution), so a model can chain many calls in one turn
with pipes, variables, loops and `jq`.

```toml
[dependencies]
bashkit = "0.18"
bashkit-scripted-tool = { version = "0.18", features = ["jq"] }
```

```rust,ignore
use bashkit::Tool;
use bashkit_scripted_tool::{ScriptedTool, ToolArgs, ToolDef};

let tool = ScriptedTool::builder("api")
.tool_fn(ToolDef::new("greet", "Greet a user"), |args: &ToolArgs| {
Ok(format!("hello {}\n", args.param_str("name").unwrap_or("world")))
})
.build();
```

Features:

- `jq`: admit the `jq` builtin in the logic-only shell.
- `python`, `typescript`: expose a `ToolRegistry` to embedded Python and
TypeScript as host functions (`ToolRegistry::install`).
- `tracing`: debug logs for sanitized callback errors.

This crate used to be the `scripted_tool` feature of `bashkit`. See the
[migration guide](https://github.com/everruns/bashkit/blob/main/docs/migrating-scripted-tool.md).
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,13 @@
//! Demonstrates composing multiple API-like tools (ToolDef + closures) into a
//! single ScriptedTool that an LLM agent can call with bash scripts.
//!
//! Run with: cargo run --example scripted_tool --features scripted_tool
//! Run with: cargo run -p bashkit-scripted-tool --example scripted_tool
//!
//! This example simulates an e-commerce API with tools for users, orders, and
//! inventory. The ScriptedTool lets an agent compose these in one call.

use bashkit::{ScriptedTool, Tool};
use bashkit::Tool;
use bashkit_scripted_tool::ScriptedTool;

async fn run_script(tool: &ScriptedTool, commands: &str) -> anyhow::Result<String> {
let output = tool
Expand Down Expand Up @@ -140,7 +141,7 @@ async fn main() -> anyhow::Result<()> {
// ScriptedTool builder API and the demo scenarios.
// ---------------------------------------------------------------------------
mod fakes {
use bashkit::{ToolArgs, ToolDef};
use bashkit_scripted_tool::{ToolArgs, ToolDef};

// -- get_user --------------------------------------------------------

Expand Down
Loading
Loading