bashkit 0.20.1

Awesomely fast virtual sandbox with bash and file system
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
# Bashkit core library
# Sandboxed bash interpreter for multi-tenant environments

[package]
name = "bashkit"
version.workspace = true
edition.workspace = true
license.workspace = true
authors.workspace = true
repository.workspace = true
description.workspace = true
keywords.workspace = true
categories.workspace = true
readme = "../../README.md"

[dependencies]
# Async runtime
tokio = { workspace = true }
async-trait = { workspace = true }
# futures-core / tower are only pulled in by the LLM `BashTool` wrapper
# (and the `bashkit-scripted-tool` crate on top of it). Optional, gated under `bash_tool`.
futures-core = { workspace = true, optional = true }
futures-util = { workspace = true }
tower = { workspace = true, optional = true }

# Error handling
thiserror = { workspace = true }
anyhow = { workspace = true }

# Serialization
serde = { workspace = true }
serde_json = { workspace = true }
serde_norway = { workspace = true, optional = true }

# Regex
regex = { workspace = true }
regex-automata = { workspace = true }
fancy-regex = { workspace = true }

# Date/Time
chrono = { workspace = true }
chrono-tz = { workspace = true, optional = true }
vt100 = { workspace = true, optional = true }

# HTTP client (for curl/wget) - optional, enabled with http_client feature
reqwest = { workspace = true, optional = true }
# Direct rustls dep so we can install the crypto provider ourselves at runtime
# (paired with reqwest's `rustls-no-provider` feature). Optional, gated under
# http_client. The backend is chosen by the `ring` (default) / `aws-lc-rs`
# features, not pinned here.
rustls = { workspace = true, optional = true }

# SSH client (for ssh/scp/sftp) - optional, enabled with ssh feature
russh = { workspace = true, optional = true }

# Fault injection for testing (optional)
fail = { workspace = true, optional = true }

# URL parsing
url = "2"

# Compact flag bitsets used by the interpreter for variable attributes
# (readonly/integer/lower/upper) and shell options (set -e/-u/-x/-o pipefail).
# Stays the format!()-based `_READONLY_X`/`SHOPT_*` keys off the hot path.
bitflags = "2"

# JSON processing (jq) - optional, enabled with jq feature
jaq-core = { workspace = true, optional = true }
jaq-std = { workspace = true, optional = true }
# jaq-json is vendored into builtins/jq/jaq_json/ (TM-DOS-110, #2444) so its
# value operations can enforce size limits; these are its dependencies,
# pinned to the ranges upstream jaq-json declares. See
# knowledge/runtimes/jaq-json-vendor.md.
bstr = { version = "1", default-features = false, optional = true }
bytes = { version = "1", default-features = false, optional = true }
foldhash = { version = "0.2", default-features = false, optional = true }
hifijson = { version = "0.5.0", default-features = false, features = ["alloc", "std"], optional = true }
indexmap = { version = "2.0", default-features = false, optional = true }
num-bigint = { version = "0.4.6", default-features = false, optional = true }
ryu = { version = "1.0.20", optional = true }
self_cell = { version = "1.2.0", optional = true }

# Compression (for gzip/gunzip and bzip2/bunzip2)
flate2 = { workspace = true }
bzip2 = { workspace = true }

# Base64 encoding (for base64 builtin and HTTP basic auth)
base64 = { workspace = true }

# Ed25519 signing for bot-auth request signing (optional)
ed25519-dalek = { workspace = true, optional = true }
rand = { workspace = true, optional = true }
zeroize = { workspace = true, optional = true }

# CSPRNG for credential placeholder generation
getrandom = { workspace = true }

# Checksums (for md5sum, sha1sum, sha256sum builtins)
md-5 = { workspace = true }
sha1 = { workspace = true }
sha2 = { workspace = true }
blake2 = { workspace = true }
hmac = "0.13"

# Logging/tracing (optional)
tracing = { workspace = true, optional = true }

# CLI-style argument parsing for custom builtins and ported coreutils
# argument surfaces. Always on — see knowledge/runtimes/coreutils-args-port.md.
clap = { workspace = true }

# Pinned to uutils' workspace versions. Used by the vendored
# uucore::format module (see crates/bashkit/src/builtins/generated/format)
# that backs `printf`. Kept platform-clean — no rustix / errno
# transitive deps — so wasm32 stays buildable.
bigdecimal = "0.4"
num-traits = "0.2"
unit-prefix = "0.5"
os_display = "0.1.3"

# Embedded Python interpreter (optional).
# Registry dep since monty 0.0.19 — the first version published to crates.io.
# Keeps `python` usable for downstream crates and lets the publish workflow
# ship the feature instead of stripping it.
#
# Important decision (security): held at 0.0.19. Cargo treats `0.0.z` as its own
# compatibility range (`^0.0.19` == `>=0.0.19, <0.0.20`), so this is already an
# exact pin; the matching `ignore` entries in .github/dependabot.yml stop the
# bump PRs from being regenerated.
#
# 0.0.20–0.0.22 move `max_memory` enforcement from host-side accounting to a probe of
# the `LIVE_MEMORY`/`BASELINE_MEMORY` globals, which only the separate
# `monty-alloc` crate writes — and only when installed as the process-wide
# global allocator. Bashkit is an embeddable library (and bashkit-python is a
# CPython extension module), so it cannot impose a global allocator on its
# hosts. Without one, `probe_memory()` is `0.saturating_sub(usize::MAX)` == 0
# and the Python sandbox's memory ceiling silently stops being enforced.
# These versions also remove the ResourceTracker trait used for shared work
# and cancellation checkpoints; charging only around start/resume loses that
# enforcement during an uninterrupted VM loop.
# See knowledge/runtimes/python-builtin.md § Upgrade blocker.
monty = { version = "=0.0.19", optional = true }

# Real CPython 3.14 as a WASI guest, precompiled to Pulley (cpython feature).
# See knowledge/runtimes/cpython-wasm.md.
bashkit-cpython-wasm = { path = "../bashkit-cpython-wasm", version = "0.20.1", optional = true }
wasmtime = { workspace = true, optional = true, features = ["pooling-allocator"] }
monty-types = { version = "=0.0.19", optional = true }
# Monty's ruff AST uses compact_str 0.9; get-size2 >=0.10.2 only implements
# GetSize for compact_str 0.10. Enforce downstream too, not only in Cargo.lock.
#
# No bashkit source uses this crate. It is declared solely to hold the whole
# graph at 0.10.1 — bumping it releases ruff's copy to float to 0.10.3 and
# breaks the `GetSize` derive (#2381). Dependabot ignores it for that reason;
# keep the ignore in .github/dependabot.yml in step with this pin.
get-size2 = { version = "=0.10.1", optional = true }

# Embedded TypeScript interpreter (optional)

# Embedded SQLite engine via Turso (optional, BETA upstream).
# Pulls a multi-MB transitive dep tree only when the `sqlite` feature is active.
turso_core = { workspace = true, optional = true }

# Feature-unification shim for `branches`, a transitive dep of `turso_core`.
# Never referenced in code — see the comment in the workspace Cargo.toml.
branches = { workspace = true, optional = true }

[features]
default = ["bash_tool", "ring", "tzdata"]
# Named IANA timezone support for the `date` builtin's `TZ=` handling, via the
# chrono-tz database. On by default.
#
# Important decision: this is a feature, not an unconditional dep, because
# chrono-tz compiles the whole IANA database in -- by far the largest single
# artifact in the tree (an 18.6 MB rlib) -- to serve three lines of use in
# builtins/date.rs. Builds that do not need named zones (the slim browser wasm
# package) should not carry it.
#
# Turning it off does not open a hole. `SandboxTimezone` closes to UTC alone and
# `date` reports that it cannot honour a named zone, exiting 1 rather than
# formatting in UTC and returning a plausible-looking wrong time. The
# fail-closed direction of THREAT[TM-INF-018] is unchanged: host timezone state
# is still never consulted. `TZ=UTC`, unset, empty, and path-style values are
# unaffected.
tzdata = ["dep:chrono-tz"]
# Enable the LLM-facing `BashTool` wrapper: the `Tool` trait, `ToolExecution`,
# OpenAI tool-definition/JSON-schema helpers, and the `tower::Service` bridge.
# On by default. Disable with `--no-default-features` to compile just the
# embeddable `Bash` interpreter and drop the `tower` / `futures-core` deps.
# Usage: cargo build --no-default-features [--features <others>]
bash_tool = ["dep:tower", "dep:futures-core"]
# Route getrandom at JS (`crypto.getRandomValues`) on wasm32-unknown-unknown.
# Needed by JS hosts (browsers, workers, Node); must stay off for non-JS wasm
# embedders, which supply their own getrandom backend via
# `--cfg getrandom_backend="custom"`.
# Usage: cargo build --target wasm32-unknown-unknown --features wasm_js
wasm_js = [
    "getrandom/wasm_js",
    "chrono/wasmbind",
    "dep:web-time",
    "dep:gloo-timers",
    "dep:send_wrapper",
    "dep:wasm-bindgen-futures",
]
# In-process terminal: a PTY-like `Terminal` (keystrokes in, screen text out)
# wrapping a `Bash`, plus the `vi` builtin that needs it. Off by default.
#
# Important decision: the screen model is the pure-Rust `vt100` crate, not
# libghostty. vt100 is MIT, has three small pure-Rust deps, and builds for every
# bashkit target (wasm32 included). libghostty needs a Zig toolchain and C FFI,
# which would break the wasm, PyPI and npm builds for a feature this small.
# See knowledge/integrations/in-process-terminal.md.
# Usage: cargo build --features terminal
terminal = ["dep:vt100"]
# Enable jq builtin via embedded jaq interpreter
# Usage: cargo build --features jq
jq = [
    "dep:jaq-core",
    "dep:jaq-std",
    "dep:bstr",
    "dep:bytes",
    "dep:foldhash",
    "dep:hifijson",
    "dep:indexmap",
    "dep:num-bigint",
    "dep:ryu",
    "dep:self_cell",
    "dep:serde_norway",
]
# HTTP client (curl/wget) over rustls. Does NOT imply a crypto backend: pick
# `ring` (on by default) or `aws-lc-rs`. Enabling neither is a compile error.
http_client = ["reqwest", "rustls"]
# Crypto backend for the rustls-based HTTP client. Exactly one is used at
# runtime; `ring` wins when both are on, which keeps Cargo's additive feature
# unification sound (a consumer asking for `aws-lc-rs` still compiles and still
# installs exactly one provider when something else re-enables `ring`).
#
# Important decision: `ring` is the default because it keeps the dependency
# tree free of C-compiled crypto (no aws-lc-sys), which is what keeps
# cross-compiled aarch64 manylinux wheel builds green — that cross sysroot is
# missing `AT_HWCAP2` and aws-lc-sys trips over it. `aws-lc-rs` exists for
# consumers who need a FIPS-capable backend or who already link aws-lc-rs and
# want to avoid shipping two crypto libraries.
ring = ["rustls?/ring"]
aws-lc-rs = ["rustls?/aws-lc-rs"]
# Enable Ed25519 request signing per RFC 9421 / web-bot-auth profile
bot-auth = ["http_client", "dep:ed25519-dalek", "dep:rand", "dep:zeroize"]
# Enable fail points for security/fault injection testing
# Usage: FAILPOINTS="fail_point_name=action" cargo test --features failpoints
failpoints = ["fail/failpoints"]
# Enable structured logging via tracing crate
# Usage: cargo build --features logging
logging = ["tracing"]
# Enable git builtin for sandboxed git operations
# Phase 1: Local operations (init, config, add, commit, status, log)
# Phase 2 will add gix dependency for remote operations
# Usage: cargo build --features git
git = []
# Enable ssh/scp/sftp builtins for remote command execution and file transfer
# Usage: cargo build --features ssh
ssh = ["russh"]
# Enable python/python3 builtins via embedded Monty interpreter
# Usage: cargo build --features python
python = ["dep:monty", "dep:monty-types", "dep:get-size2"]
# Enable python/python3 builtins via real CPython 3.14 in a WebAssembly sandbox
# Usage: cargo build --features cpython
cpython = ["dep:bashkit-cpython-wasm", "dep:wasmtime"]
# Opt-in: CPython compiled to native machine code at build time instead of
# Pulley bytecode. ~2x faster calls; needs executable memory, and the first
# load in a process copies the module (call bashkit::warm_up() at startup).
# Usage: cargo build --features cpython-native
cpython-native = ["cpython", "bashkit-cpython-wasm/native"]
# Enable ts/node/deno/bun builtins via embedded ZapCode TypeScript interpreter
# Usage: cargo build --features typescript
typescript = ["dep:zapcode-core", "dep:oxc_parser", "dep:oxc_ast", "dep:oxc_allocator", "dep:oxc_span", "dep:oxc_syntax", "dep:postcard", "dep:indexmap", "indexmap/serde", "serde/rc"]
# Enable sqlite/sqlite3 builtins backed by Turso (pure-Rust SQLite-compatible
# engine). Phase 1 uses turso's `MemoryIO` with a load/flush against the VFS
# at command boundaries; Phase 2 plugs the bashkit VFS in via a custom `IO`
# implementation. Both paths share the same builtin and dot-command surface.
# See knowledge/runtimes/sqlite-builtin.md.
#
# Turso is BETA upstream — keep this off by default and document the risk.
# Requires the multi-threaded tokio runtime so the sync `IO` trait can bridge
# back to the async VFS via `block_in_place`.
# Usage: cargo build --features sqlite
sqlite = ["dep:turso_core", "dep:branches", "tokio/rt-multi-thread"]
# Enable RealFs backend for accessing host filesystem directories
# WARNING: This intentionally breaks the sandbox boundary.
# Usage: cargo build --features realfs
realfs = ["dep:rand"]
# Enable native-extension interop contracts such as bashkit::interop::fs.
interop = ["tokio/rt-multi-thread"]

[package.metadata.docs.rs]
all-features = true
rustdoc-args = ["--cfg", "docsrs"]

[dev-dependencies]
tokio-test = { workspace = true }
pretty_assertions = { workspace = true }
insta = { workspace = true }
criterion = { version = "0.8", features = ["async_tokio"] }
proptest = { workspace = true }
tempfile = "3"
serial_test = { workspace = true }

# Important decision: consolidate the bulk of integration tests into one
# binary. Cargo's auto-discovery makes every `tests/*.rs` its own crate,
# statically linking the world (monty, zapcode, turso, russh, jaq,
# reqwest+rustls, ed25519-dalek) — 80+ link steps blew CI runner disk.
# This single `[[test]]` rolls them up. Tests that need their own binary
# (process-global env mutation, `--test-threads=1`, ssh-only feature
# isolation) remain as siblings at `tests/<name>.rs`. See
# `knowledge/operations/testing.md` for the convention.
[[test]]
name = "integration"
path = "tests/integration/main.rs"

[[bench]]
name = "parallel_execution"
harness = false

[[bench]]
name = "hotpath"
harness = false

[[bench]]
name = "builtin_args"
harness = false

[[bench]]
name = "file_ops"
harness = false

[[bench]]
name = "awk_regex"
harness = false

[[bench]]
name = "snapshot_history"
harness = false

[[bench]]
name = "sqlite"
harness = false
required-features = ["sqlite"]

[[bench]]
name = "python"
harness = false
required-features = ["python", "cpython"]

[[example]]
name = "terminal_vi"
required-features = ["terminal"]

[[example]]
name = "terminal_record"
required-features = ["terminal"]

[[example]]
name = "agent_tool"
required-features = ["http_client"]

[[example]]
name = "git_workflow"
required-features = ["git"]

[[example]]
name = "ssh_supabase"
required-features = ["ssh"]

[[example]]
name = "show_tool_output"
required-features = ["bash_tool"]

[[example]]
name = "python_scripts"
required-features = ["python"]

[[example]]
name = "python_external_functions"
required-features = ["python"]

[[example]]
name = "python_startup"
required-features = ["python", "cpython"]

[[example]]
name = "cpython_scripts"
required-features = ["cpython"]

[[example]]
name = "cpython_load"
required-features = ["cpython"]

[[example]]
name = "typescript_scripts"
required-features = ["typescript"]

[[example]]
name = "typescript_external_functions"
required-features = ["typescript"]

[[example]]
name = "realfs_readonly"
required-features = ["realfs"]

[[example]]
name = "realfs_readwrite"
required-features = ["realfs"]

[[example]]
name = "sqlite_basic"
required-features = ["sqlite"]

[[example]]
name = "sqlite_workflow"
required-features = ["sqlite"]

# Additional tokio features needed only on native (not WASM)
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
tokio = { version = "1", features = ["rt-multi-thread", "fs"] }

# std::time::{Instant,SystemTime} panic on wasm32-unknown-unknown; see
# time_compat/mod.rs. Only pulled in for wasm32 — everywhere else time_compat
# re-exports std::time directly with zero overhead.
#
# These three are JS-backed (wasm-bindgen imports), so they are optional and
# pulled in by `wasm_js`. A non-JS wasm embedder gets time_compat::host_clock
# instead and links none of them: their imports would be unresolvable in a
# runtime with no JS engine.
[target.'cfg(target_arch = "wasm32")'.dependencies]
web-time = { workspace = true, optional = true }

[target.'cfg(all(target_arch = "wasm32", target_os = "unknown"))'.dependencies]
# Vendored browser ZapCode uses the same parser/codec versions as upstream.
oxc_parser = { version = "0.117.0", optional = true }
oxc_ast = { version = "0.117.0", optional = true }
oxc_allocator = { version = "0.117.0", optional = true }
oxc_span = { version = "0.117.0", optional = true }
oxc_syntax = { version = "0.117.0", optional = true }
postcard = { version = "1.1.3", features = ["alloc"], optional = true }

gloo-timers = { version = "0.4", features = ["futures"], optional = true }
send_wrapper = { version = "0.6", features = ["futures"], optional = true }
# Task spawner for the host-call execution driver (see host_call.rs). JS-backed
# like the three above — a non-JS wasm embedder links none of it and drives the
# execution future from `next_event` instead.
wasm-bindgen-futures = { version = "0.4", optional = true }

# Vendored uucore modules (e.g. generated/format/human.rs) carry upstream
# `#[cfg(feature = "i18n-decimal")]` gates. bashkit has no i18n surface —
# the port tool hard-errors on Fluent/translate imports — so this feature
# is never defined here and the gated branch is always compiled out. Declare
# it as a known cfg so `unexpected_cfgs` (denied via -D warnings in CI) does
# not flag the vendored code. See knowledge/runtimes/coreutils-args-port.md.
[lints.rust]
unexpected_cfgs = { level = "warn", check-cfg = ['cfg(feature, values("i18n-decimal"))'] }

# The process feature is native test infrastructure only; keep WASM builds
# on the workspace Tokio feature set without changing versions or the lock.
[target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies]
tokio = { workspace = true, features = ["process"] }

[target.'cfg(not(all(target_arch = "wasm32", target_os = "unknown")))'.dependencies]
zapcode-core = { version = "1.5.1", optional = true }