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
37 changes: 36 additions & 1 deletion .github/workflows/js.yml
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,7 @@ jobs:
${{ matrix.run }} k8s_orchestrator.mjs
${{ matrix.run }} langchain_integration.mjs
${{ matrix.run }} custom_builtins.mjs
${{ matrix.run }} runtime_functions.mjs

- name: Detect Doppler availability
id: doppler
Expand Down Expand Up @@ -243,11 +244,41 @@ jobs:
${{ matrix.run }} vercel_ai_tool.mjs
${{ matrix.run }} langchain_agent.mjs

callback-platforms:
name: Runtime callbacks (${{ matrix.os }})
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [macos-latest, windows-latest]
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- uses: dtolnay/rust-toolchain@e081816240890017053eacbb1bdf337761dc5582 # 1.95.0
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2
- uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0
with:
version: 10.33.0
- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v6
with:
node-version: "24"
cache: pnpm
cache-dependency-path: crates/bashkit-js/pnpm-lock.yaml
- run: pnpm install --frozen-lockfile
working-directory: crates/bashkit-js
- run: pnpm run build
working-directory: crates/bashkit-js
- run: pnpm exec ava __test__/runtime-functions.spec.ts
working-directory: crates/bashkit-js
- run: node --test __test__/runtime-compat/runtime-functions.test.mjs
working-directory: crates/bashkit-js

# Gate job for branch protection
js-check:
name: JS Check
if: always()
needs: [typecheck, build-and-test]
needs: [typecheck, build-and-test, callback-platforms]
runs-on: ubuntu-latest
steps:
- name: Verify all jobs passed
Expand All @@ -260,3 +291,7 @@ jobs:
echo "JS build/test failed"
exit 1
fi
if [[ "${{ needs.callback-platforms.result }}" != "success" ]]; then
echo "Runtime callback platform tests failed"
exit 1
fi
1 change: 1 addition & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ Fix root cause. Unsure: read more code; if stuck, ask w/ short options. Unrecogn
| operations/eval | LLM eval study on the mira framework, dataset format, scoring |
| operations/oils-spec | Upstream Oils spec suite pass rate vs real bash (`just oils-spec`) |
| operations/maintenance | Pre-release maintenance requirements |
| runtimes/node-package | Native JavaScript package, runtime callbacks, caller context and worker-thread safety |
| runtimes/python-package | Python package, PyPI wheels, platform matrix |
| integrations/scripted-tool-orchestration | Compose ToolDef+callback pairs into OrchestratorTool via bash scripts |
| integrations/ssh-support | Sandboxed SSH/SCP/SFTP operations |
Expand Down
1 change: 1 addition & 0 deletions Cargo.lock

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

48 changes: 33 additions & 15 deletions contracts/capability-parity.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,14 @@
"runtime_python": "Explicit embedded Python opt-in",
"runtime_typescript": "Explicit embedded TypeScript opt-in",
"runtime_sqlite": "Explicit embedded SQLite opt-in",
"interactive_terminal": "In-process interactive terminal session (keystrokes in, screen text and per-command transcript out; vi, less)"
"interactive_terminal": "In-process interactive terminal session (keystrokes in, screen text and per-command transcript out; vi, less)",
"host_functions_python": "Named host callbacks invoked from embedded Python",
"host_functions_typescript": "Named host callbacks invoked from embedded TypeScript"
},
"surfaces": {
"rust_bash": {
"label": "Rust BashBuilder",
"supported": ["execution_limits", "cancellation", "deadlines", "output_caps", "cwd", "env", "runtime_env", "stdin", "custom_builtins", "host_call_suspension", "network_policy", "transport_hooks", "virtual_filesystem", "mounts", "snapshots", "runtime_python", "runtime_typescript", "runtime_sqlite", "interactive_terminal"],
"supported": ["execution_limits", "cancellation", "deadlines", "output_caps", "cwd", "env", "runtime_env", "stdin", "custom_builtins", "host_call_suspension", "network_policy", "transport_hooks", "virtual_filesystem", "mounts", "snapshots", "runtime_python", "runtime_typescript", "runtime_sqlite", "interactive_terminal", "host_functions_python", "host_functions_typescript"],
"evidence": {
"execution_limits": ["crates/bashkit/tests/integration/blackbox_security_tests.rs#max_commands_resets_per_exec"],
"cancellation": ["crates/bashkit/tests/integration/cancellation_tests.rs#cancel_aborts_execution"],
Expand All @@ -45,15 +47,17 @@
"runtime_python": ["crates/bashkit/tests/integration/python_integration_tests.rs#print_hello"],
"runtime_typescript": ["crates/bashkit/tests/integration/typescript_integration_tests.rs#bash_writes_file_ts_reads"],
"runtime_sqlite": ["crates/bashkit/tests/integration/sqlite_integration_tests.rs#inline_select_returns_value"],
"interactive_terminal": ["crates/bashkit/tests/integration/terminal_tests.rs#agent_edits_config_with_vi_and_reads_result"]
"interactive_terminal": ["crates/bashkit/tests/integration/terminal_tests.rs#agent_edits_config_with_vi_and_reads_result"],
"host_functions_python": ["crates/bashkit/tests/integration/core_extension_point_tests.rs#python_host_function_sees_request_extensions"],
"host_functions_typescript": ["crates/bashkit/tests/integration/core_extension_point_tests.rs#typescript_host_function_sees_request_extensions"]
},
"unsupported": {
"tool_callbacks": "Use ScriptedTool for named tool callbacks; BashBuilder exposes lower-level custom builtins."
}
},
"rust_bash_tool": {
"label": "Rust BashTool",
"supported": ["execution_limits", "deadlines", "output_caps", "cwd", "env", "custom_builtins", "network_policy", "transport_hooks", "virtual_filesystem", "mounts", "runtime_python", "runtime_typescript", "runtime_sqlite"],
"supported": ["execution_limits", "deadlines", "output_caps", "cwd", "env", "custom_builtins", "network_policy", "transport_hooks", "virtual_filesystem", "mounts", "runtime_python", "runtime_typescript", "runtime_sqlite", "host_functions_python", "host_functions_typescript"],
"evidence": {
"execution_limits": ["crates/bashkit/src/tool.rs#test_execution_forwards_limits_and_output_caps"],
"deadlines": ["crates/bashkit/src/tool.rs#test_tool_execute_timeout"],
Expand All @@ -67,7 +71,9 @@
"mounts": ["crates/bashkit/src/tool.rs#test_configure_forwards_lazy_mount"],
"runtime_python": ["crates/bashkit/src/tool.rs#test_python_runtime_opt_in_executes_through_tool"],
"runtime_typescript": ["crates/bashkit/src/tool.rs#test_typescript_runtime_opt_in_executes_through_tool"],
"runtime_sqlite": ["crates/bashkit/src/tool.rs#test_configure_forwards_sqlite_runtime_opt_in"]
"runtime_sqlite": ["crates/bashkit/src/tool.rs#test_configure_forwards_sqlite_runtime_opt_in"],
"host_functions_python": ["crates/bashkit/src/tool.rs#test_python_host_functions_execute_through_tool"],
"host_functions_typescript": ["crates/bashkit/src/tool.rs#test_typescript_host_functions_execute_through_tool"]
},
"unsupported": {
"runtime_env": "BashTool builds a fresh Bash per execution, so there is no live instance to mutate; env is configured on the builder.",
Expand Down Expand Up @@ -104,7 +110,9 @@
"runtime_python": "Language runtimes are intentionally absent from the logic-only shell.",
"runtime_typescript": "Language runtimes are intentionally absent from the logic-only shell.",
"runtime_sqlite": "Language runtimes are intentionally absent from the logic-only shell.",
"interactive_terminal": "ScriptedTool runs logic-only scripts per call and has no terminal device."
"interactive_terminal": "ScriptedTool runs logic-only scripts per call and has no terminal device.",
"host_functions_python": "Language runtimes are intentionally absent from the logic-only shell.",
"host_functions_typescript": "Language runtimes are intentionally absent from the logic-only shell."
}
},
"cli": {
Expand Down Expand Up @@ -132,12 +140,14 @@
"transport_hooks": "The CLI exposes allow-all/default-deny policy, not a host transport callback.",
"snapshots": "The CLI exposes no snapshot commands.",
"runtime_typescript": "The CLI build does not register the TypeScript runtime.",
"interactive_terminal": "The CLI's interactive mode runs on the host's real terminal (rustyline), not the in-process Terminal."
"interactive_terminal": "The CLI's interactive mode runs on the host's real terminal (rustyline), not the in-process Terminal.",
"host_functions_python": "A standalone process cannot receive in-process host callbacks.",
"host_functions_typescript": "A standalone process cannot receive in-process host callbacks."
}
},
"python": {
"label": "Python",
"supported": ["execution_limits", "cancellation", "deadlines", "output_caps", "cwd", "env", "runtime_env", "custom_builtins", "tool_callbacks", "network_policy", "virtual_filesystem", "mounts", "snapshots", "runtime_python", "runtime_typescript", "runtime_sqlite", "interactive_terminal"],
"supported": ["execution_limits", "cancellation", "deadlines", "output_caps", "cwd", "env", "runtime_env", "custom_builtins", "tool_callbacks", "network_policy", "virtual_filesystem", "mounts", "snapshots", "runtime_python", "runtime_typescript", "runtime_sqlite", "interactive_terminal", "host_functions_python", "host_functions_typescript"],
"evidence": {
"execution_limits": ["crates/bashkit-python/tests/_bashkit_categories.py#test_max_commands_limits_execution"],
"cancellation": ["crates/bashkit-python/tests/_bashkit_categories.py#test_bash_cancel_after_reset_still_works"],
Expand All @@ -155,7 +165,9 @@
"runtime_python": ["crates/bashkit-python/tests/_bashkit_categories.py#test_bash_python_enabled"],
"runtime_typescript": ["crates/bashkit-python/tests/test_tool_runtimes.py#test_runtimes_share_virtual_filesystem"],
"runtime_sqlite": ["crates/bashkit-python/tests/test_sqlite.py#test_sqlite"],
"interactive_terminal": ["crates/bashkit-python/tests/test_terminal.py#test_call_edits_file_in_vi"]
"interactive_terminal": ["crates/bashkit-python/tests/test_terminal.py#test_call_edits_file_in_vi"],
"host_functions_python": ["crates/bashkit-python/tests/test_external_functions.py#test_async_external_functions_use_caller_loop_and_context"],
"host_functions_typescript": ["crates/bashkit-python/tests/test_external_functions.py#test_async_external_functions_use_caller_loop_and_context"]
},
"unsupported": {
"stdin": "execute() accepts a command string and optional output callback, not top-level host stdin.",
Expand All @@ -165,7 +177,7 @@
},
"napi": {
"label": "NAPI JavaScript",
"supported": ["execution_limits", "cancellation", "deadlines", "output_caps", "cwd", "env", "runtime_env", "custom_builtins", "tool_callbacks", "network_policy", "virtual_filesystem", "mounts", "snapshots", "runtime_python", "runtime_sqlite", "interactive_terminal"],
"supported": ["execution_limits", "cancellation", "deadlines", "output_caps", "cwd", "env", "runtime_env", "custom_builtins", "tool_callbacks", "network_policy", "virtual_filesystem", "mounts", "snapshots", "runtime_python", "runtime_sqlite", "interactive_terminal", "host_functions_python", "host_functions_typescript", "runtime_typescript"],
"evidence": {
"execution_limits": ["crates/bashkit-js/__test__/security.spec.ts#WB: command limit is enforced"],
"cancellation": ["crates/bashkit-js/__test__/basic.spec.ts#Bash: cancel works after reset"],
Expand All @@ -182,13 +194,15 @@
"snapshots": ["crates/bashkit-js/__test__/snapshot-history.spec.ts#commit and checkout round-trip shell state and files"],
"runtime_python": ["crates/bashkit-js/__test__/integration.spec.ts#integration: python opt-in executes embedded Python"],
"runtime_sqlite": ["crates/bashkit-js/__test__/runtime-compat/sqlite.test.mjs#sqlite"],
"interactive_terminal": ["crates/bashkit-js/__test__/terminal.spec.ts#terminal: call edits a file in vi"]
"interactive_terminal": ["crates/bashkit-js/__test__/terminal.spec.ts#terminal: call edits a file in vi"],
"host_functions_python": ["crates/bashkit-js/__test__/runtime-functions.spec.ts#host functions preserve HTTP request contexts and async I/O"],
"host_functions_typescript": ["crates/bashkit-js/__test__/runtime-functions.spec.ts#host functions preserve HTTP request contexts and async I/O"],
"runtime_typescript": ["crates/bashkit-js/__test__/runtime-functions.spec.ts#both runtimes dispatch independently and advertise host functions"]
},
"unsupported": {
"stdin": "execute() accepts a command string and output callback, not top-level host stdin.",
"host_call_suspension": "NAPI custom builtins await callbacks directly; no request and resume handle is exposed.",
"transport_hooks": "NAPI exposes network policy and credentials but no JavaScript HttpTransport hook.",
"runtime_typescript": "The NAPI package does not compile or expose the TypeScript runtime."
"transport_hooks": "NAPI exposes network policy and credentials but no JavaScript HttpTransport hook."
}
},
"browser_wasm": {
Expand Down Expand Up @@ -216,7 +230,9 @@
"runtime_python": "The slim browser build omits embedded language runtimes.",
"runtime_typescript": "The slim browser build omits embedded language runtimes.",
"runtime_sqlite": "The slim browser build omits embedded language runtimes.",
"interactive_terminal": "Not exposed in the slim browser package yet."
"interactive_terminal": "Not exposed in the slim browser package yet.",
"host_functions_python": "The slim browser build currently omits embedded language runtimes.",
"host_functions_typescript": "The slim browser build currently omits embedded language runtimes."
}
},
"c_abi": {
Expand Down Expand Up @@ -244,7 +260,9 @@
"runtime_python": "ABI v1 omits embedded language runtimes.",
"runtime_typescript": "ABI v1 omits embedded language runtimes.",
"runtime_sqlite": "ABI v1 omits embedded language runtimes.",
"interactive_terminal": "Not exposed in the C ABI."
"interactive_terminal": "Not exposed in the C ABI.",
"host_functions_python": "ABI v1 currently omits embedded language runtimes and runtime callback registration.",
"host_functions_typescript": "ABI v1 currently omits embedded language runtimes and runtime callback registration."
}
}
}
Expand Down
6 changes: 6 additions & 0 deletions crates/bashkit-js/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -55,3 +55,9 @@ Cargo.lock

# IDE
.vscode-test

# Runtime host-function bridge output
runtime-functions.js
runtime-functions.d.ts
runtime-functions.js.map
runtime-functions.d.ts.map
3 changes: 2 additions & 1 deletion crates/bashkit-js/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,12 @@ repository.workspace = true
crate-type = ["cdylib"]

[dependencies]
bashkit = { path = "../bashkit", features = ["python", "realfs", "jq", "interop", "http_client", "sqlite", "terminal"] }
bashkit = { path = "../bashkit", features = ["python", "typescript", "realfs", "jq", "interop", "http_client", "sqlite", "terminal"] }
bashkit-scripted-tool = { path = "../bashkit-scripted-tool", features = ["python", "jq"] }
napi = { workspace = true }
napi-derive = { workspace = true }
serde_json = { workspace = true }
serde = { workspace = true }
tokio = { version = "1", features = ["sync", "macros", "io-util", "rt", "time"] }

[build-dependencies]
Expand Down
9 changes: 8 additions & 1 deletion crates/bashkit-js/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -630,10 +630,17 @@ import {
- `files?: Record<string, string | (() => string) | (() => Promise<string>)>`
- `mounts?: Array<{ path: string; root: string; writable?: boolean }>`
- `python?: boolean`
- `externalFunctions?: string[]`
- `externalFunctions?: string[]`, Python host function names
- `externalHandler?: ExternalHandler`, sync/async Python dispatcher `(name, args, kwargs, { signal })`
- `typescript?: boolean`, opt into embedded TypeScript (`ts`/`node`/`deno`/`bun`)
- `typescriptExternalFunctions?: string[]`
- `typescriptExternalHandler?: TypeScriptExternalHandler`, dispatcher `(name, args, { signal })`

- `customBuiltins?: Record<string, (ctx: BuiltinContext) => string | Promise<string>>`, JS callbacks registered as bash builtins (see [Custom Builtins](#custom-builtins))
- `network?: NetworkOptions`, outbound HTTP configuration (see [Network](#network))

Host calls require asynchronous `execute()`. See the [host function guide](../../docs/start-node.md#python-and-typescript-host-functions) for JSON values, caller context, cancellation and reentry rules.

### ScriptAnalysis

Returned by `analyze(script)`.
Expand Down
98 changes: 98 additions & 0 deletions crates/bashkit-js/__test__/fixtures/runtime-functions-shutdown.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
// Run in a subprocess: native runtime panics must not disappear into AVA's
// worker stderr. Host functions defer invocation beyond the NAPI stack frame.
import assert from "node:assert/strict";
import {
isMainThread,
parentPort,
Worker,
workerData,
} from "node:worker_threads";
import { Bash, BashTool } from "../../wrapper.js";
import { ScriptedTool } from "../../scripted.js";

if (isMainThread) {
for (const kind of ["bash", "tool"]) {
for (const language of ["python", "typescript"]) {
for (const style of ["pending", "busy"]) {
const worker = new Worker(new URL(import.meta.url), {
workerData: { kind, language, style },
});
await new Promise((resolve, reject) => {
const timer = setTimeout(
() =>
reject(
new Error(
`host handler did not start: ${kind}/${language}/${style}`,
),
),
15000,
);
worker.once("message", () => {
clearTimeout(timer);
resolve();
});
worker.once("error", (error) => {
clearTimeout(timer);
reject(error);
});
});
await worker.terminate();
}
}
}
// ScriptedTool has a synchronous callback ABI; verify its ordinary completed
// worker teardown, without interrupting V8 inside that native callback frame.
const worker = new Worker(new URL(import.meta.url), {
workerData: { kind: "scripted" },
});
await new Promise((resolve, reject) => {
let seen = false;
worker.once("message", (stdout) => {
assert.equal(stdout.trim(), "42");
seen = true;
});
worker.once("error", reject);
worker.once("exit", (code) =>
code === 0 && seen
? resolve()
: reject(new Error(`scripted worker exited ${code}`)),
);
});
assert.equal(
(await new Bash().execute("echo reusable")).stdout,
"reusable\n",
);
console.log("pending worker shutdown passed");
} else if (workerData.kind === "scripted") {
const tool = new ScriptedTool({ name: "shutdown" });
tool.addTool("lookup", "Return a value", () => "42");
void tool
.execute("lookup")
.then((result) => parentPort.postMessage(result.stdout));
} else {
const handler = () => {
parentPort.postMessage("started");
if (workerData.style === "busy") {
while (true) {
/* host JS interrupted by Worker.terminate */
}
}
return new Promise(() => {});
};
const Factory = workerData.kind === "bash" ? Bash : BashTool;
const python = workerData.language === "python";
const bash = new Factory(
python
? {
python: true,
externalFunctions: ["lookup"],
externalHandler: handler,
}
: {
typescript: true,
typescriptExternalFunctions: ["lookup"],
typescriptExternalHandler: handler,
},
);
void bash.execute(python ? "python -c 'lookup()'" : "ts -c 'await lookup()'");
}
Loading
Loading