Architecture
August 23, 2026 ยท View on GitHub
Status
Implemented
Decision
Official name: "Bashkit" (not "BashKit"). Crate/package identifiers use lowercase bashkit.
Bashkit uses a Cargo workspace with multiple crates:
| Crate | Purpose |
|---|---|
crates/bashkit/ | Core library (parser, interpreter, VFS, builtins, tool contract) |
crates/bashkit-cli/ | CLI binary |
crates/bashkit-python/ | Python bindings (PyO3) |
crates/bashkit-js/ | JavaScript bindings (NAPI-RS) |
crates/bashkit-capi/ | Versioned native C ABI |
crates/bashkit-eval/ | LLM eval study (mira framework) |
Core library modules: parser/, interpreter/, fs/, builtins/,
network/, git/, ssh/, scripted_tool/. See source, structure evolves.
Public API
Main entry points: Bash (library) and BashTool (LLM tool contract).
See crates/bashkit/src/lib.rs for the full public API surface.
Per-invocation exec state
ExecOptions carries everything scoped to a single exec_with_options
call: streaming callback, builtin extensions, arg0, positional, and
stdin. All of it is per-call by construction, not session state.
- Positional parameters exist only inside a call frame, so the host
boundary pushes a synthetic top-level frame before
Interpreter::executeand truncates the call stack back to its baseline afterwards, including on error paths, so$#is 0 again on the next exec.$0defaults tobashwhen noarg0is supplied;set --at top level uses the same synthetic frame and must not change$0. stdinseedspipeline_stdin, whichreset_transient_stateclears at the start of every exec, so it is installed after that reset and immediately before execution. A pipe or redirect inside the script still wins for the command it applies to.
Byte-native streams
StreamData is the canonical stdin/stdout/stderr transport value. Its byte
buffer is authoritative through ExecOptions, interpreter pipeline state,
redirections, BuiltinContext, ExecResult, and streaming callbacks. Output
limits truncate by bytes, including in live callbacks. Byte-oriented builtins
and bindings use as_bytes()/into_bytes(); text-oriented consumers cross an
explicit UTF-8 boundary with text() or text_lossy().
Line-oriented consumers locate delimiters and split the authoritative byte buffer
before lossy decoding; offsets into the cached lossy view are never byte offsets.
Shell words and variables remain text. Command substitution therefore removes NUL bytes, as Bash does because variables cannot contain NUL, then decodes the remaining bytes for expansion and strips trailing newlines. JSON tool results are also text-only; native Rust, C, Python, Node, and browser results expose raw stdout/stderr bytes alongside their display strings.
Both are installed late (just before execute) so the size, hook, and
parse checks that can return early cannot leave state behind.
Process-local execution suspension
Event-backed host calls reuse the async-first execution model: an
ExecutionHandle starts the live execution future, yields an owned request to
the host, then resolves a one-shot response when the host resumes it. The
first next_event hands the future to a task spawner where the target has one
(any async runtime on native, spawn_local on JS-backed wasm) so the
interpreter timeout keeps running while the host is parked; a non-JS wasm
embedder has no executor, so there the handle keeps polling the future itself
and the deadline is enforced on the next poll. A timeout drops the session on
every target. On normal completion, into_bash returns the reusable session;
dropping a suspended handle aborts the driver and drops the session. This is
process-local scheduling, not a serializable continuation; snapshots still
capture state only between executions. See Builtin Commands and
Snapshot History.
Shared execution budget
Each exec_with_options creates exactly one ExecutionBudget before hooks or
parsing. Its Arc-backed counters are cloned, not recreated, by nested parsers,
command/process substitutions, pipelines, builtin execution plans, embedded
Python/TypeScript/SQLite, traversal/search, archive/compression work, and host
callbacks. It meters three distinct resources: monotonic work units, monotonic
aggregate consumer input bytes, and RAII live/intermediate byte leases.
Exhaustion, the request deadline, or cancellation poisons the shared budget; later descendants see the first failure and cannot resume with fresh counters. This aggregate layer complements rather than replaces parser fuel, command and loop counters, output caps, VFS quotas, and runtime-specific ceilings. The budget is request-scoped: the next host exec receives a fresh budget, while session limits continue to protect repeated host calls.
Runtime admission is deliberately conservative where a VM does not expose a portable completed-instruction count: Python reserves work from its configured memory allowance and TypeScript from its allocation allowance before entry. Python additionally meters Monty allocation/statement checkpoints. The reservation is not refunded, because refunding would recreate the reset path this layer exists to remove.
Heap buffers grown from untrusted sizes use the budget-aware owning builders in
limits.rs. BudgetedVec (including byte buffers) and BudgetedString acquire
or grow the live-byte lease before reserving capacity, roll the charge back when
the allocator or producer fails, and release it with the buffer. Archive and
compression paths use these builders because their expansion ratios and nested
buffers make post-hoc leasing unsafe. Atomic compare/exchange admission keeps
concurrent descendants from wrapping or temporarily exceeding the shared cap.
Design Principles
- Async-first: All filesystem and execution is async (tokio)
- Virtual: No real filesystem access by default
- Multi-tenant safe: Isolated state per Bash instance
- Trait-based: FileSystem and Builtin traits for extensibility
Alternatives Considered
- Single crate: rejected, CLI bloats library; Python/JS packages need separate crates.
- Sync filesystem: rejected, network ops need async; tokio already a dep.
See also
- Parser, script text to AST, ahead of the interpreter
- Virtual Filesystem, filesystem abstraction the interpreter executes against
- Builtin Commands, command layer the interpreter dispatches into
- Parallel Execution, threading model and shared-ownership rules
- Threat Model, trust boundaries these module boundaries enforce
- Known Limitations, what this architecture intentionally does not do
- Public Capability Parity, generated wrapper support and explicit exclusions