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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -34,3 +34,6 @@ dist/

# Node
node_modules/

# WASM build artifacts
*.wasm
4 changes: 3 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@ categories = ["command-line-utilities", "parser-implementations"]

[workspace.dependencies]
# Async runtime
tokio = { version = "1", features = ["full"] }
# Base features are WASM-compatible; crates needing more (rt-multi-thread, fs, net, process)
# add them in their own Cargo.toml or via [target.'cfg(...)'.dependencies].
tokio = { version = "1", features = ["sync", "macros", "io-util", "rt", "time"] }
async-trait = "0.1"
futures = "0.3"

Expand Down
2 changes: 1 addition & 1 deletion crates/bashkit-bench/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ path = "src/main.rs"

[dependencies]
bashkit = { path = "../bashkit" }
tokio.workspace = true
tokio = { workspace = true, features = ["rt-multi-thread", "process"] }
serde.workspace = true
serde_json.workspace = true
clap.workspace = true
Expand Down
2 changes: 1 addition & 1 deletion crates/bashkit-eval/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ path = "src/main.rs"

[dependencies]
bashkit = { path = "../bashkit", features = ["scripted_tool"] }
tokio.workspace = true
tokio = { workspace = true, features = ["rt-multi-thread"] }
serde.workspace = true
serde_json.workspace = true
clap.workspace = true
Expand Down
9 changes: 3 additions & 6 deletions crates/bashkit-js/package-lock.json

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

18 changes: 17 additions & 1 deletion crates/bashkit-js/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,15 @@
"version": "0.1.10",
"description": "Sandboxed bash interpreter for JavaScript/TypeScript",
"main": "wrapper.js",
"browser": "bashkit.wasi-browser.js",
"types": "wrapper.d.ts",
"type": "module",
"exports": {
".": {
"browser": "./bashkit.wasi-browser.js",
"default": "./wrapper.js"
}
},
"license": "MIT",
"repository": {
"type": "git",
Expand Down Expand Up @@ -34,11 +41,17 @@
"index.cjs",
"index.d.ts",
"index.d.cts",
"bashkit.*.node"
"bashkit.*.node",
"bashkit.wasi-browser.js",
"bashkit.wasi.cjs",
"bashkit.wasm32-wasi.wasm",
"wasi-worker.mjs",
"wasi-worker-browser.mjs"
],
"scripts": {
"build": "npm run build:napi && npm run build:cjs && npm run build:ts",
"build:napi": "napi build --platform --release",
"build:wasm": "napi build --platform --target wasm32-wasip1-threads --release",
"build:cjs": "node -e \"const fs=require('fs'); if(fs.existsSync('index.js')){fs.renameSync('index.js','index.cjs')}; if(fs.existsSync('index.d.ts')){fs.copyFileSync('index.d.ts','index.d.cts')}\"",
"build:ts": "tsc",
"artifacts": "napi artifacts",
Expand All @@ -49,6 +62,9 @@
"type-check": "tsc --noEmit",
"version": "napi version"
},
"optionalDependencies": {
"@napi-rs/wasm-runtime": "^1.1.1"
},
"devDependencies": {
"@napi-rs/cli": "^3.0.0",
"@types/node": "^25.5.0",
Expand Down
2 changes: 1 addition & 1 deletion crates/bashkit-python/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ pyo3 = { workspace = true }
pyo3-async-runtimes = { workspace = true }

# Async runtime
tokio = { workspace = true }
tokio = { workspace = true, features = ["rt-multi-thread"] }

# Serialization
serde_json = { workspace = true }
4 changes: 4 additions & 0 deletions crates/bashkit/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -134,3 +134,7 @@ required-features = ["realfs"]
[[example]]
name = "realfs_readwrite"
required-features = ["realfs"]

# Additional tokio features needed only on native (not WASM)
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
tokio = { version = "1", features = ["rt-multi-thread", "fs"] }
100 changes: 59 additions & 41 deletions crates/bashkit/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -480,6 +480,7 @@ pub struct Bash {
fs: Arc<dyn FileSystem>,
interpreter: Interpreter,
/// Parser timeout (stored separately for use before interpreter runs)
#[cfg(not(target_family = "wasm"))]
parser_timeout: std::time::Duration,
/// Maximum input script size in bytes
max_input_bytes: usize,
Expand All @@ -503,13 +504,15 @@ impl Bash {
pub fn new() -> Self {
let fs: Arc<dyn FileSystem> = Arc::new(InMemoryFs::new());
let interpreter = Interpreter::new(Arc::clone(&fs));
#[cfg(not(target_family = "wasm"))]
let parser_timeout = ExecutionLimits::default().parser_timeout;
let max_input_bytes = ExecutionLimits::default().max_input_bytes;
let max_ast_depth = ExecutionLimits::default().max_ast_depth;
let max_parser_operations = ExecutionLimits::default().max_parser_operations;
Self {
fs,
interpreter,
#[cfg(not(target_family = "wasm"))]
parser_timeout,
max_input_bytes,
max_ast_depth,
Expand Down Expand Up @@ -554,6 +557,7 @@ impl Bash {
)));
}

#[cfg(not(target_family = "wasm"))]
let parser_timeout = self.parser_timeout;
let max_ast_depth = self.max_ast_depth;
let max_parser_operations = self.max_parser_operations;
Expand All @@ -568,50 +572,62 @@ impl Bash {
"Parsing script"
);

// Parse with timeout using spawn_blocking since parsing is sync
let parse_result = tokio::time::timeout(parser_timeout, async {
tokio::task::spawn_blocking(move || {
let parser =
Parser::with_limits(&script_owned, max_ast_depth, max_parser_operations);
parser.parse()
// On WASM, tokio::task::spawn_blocking and tokio::time::timeout don't
// work (no blocking thread pool, timer driver unreliable). Parse inline.
#[cfg(target_family = "wasm")]
let ast = {
let parser = Parser::with_limits(&script_owned, max_ast_depth, max_parser_operations);
parser.parse()?
};

// On native targets, parse with timeout using spawn_blocking since
// parsing is sync and we don't want to block the async runtime.
#[cfg(not(target_family = "wasm"))]
let ast = {
let parse_result = tokio::time::timeout(parser_timeout, async {
tokio::task::spawn_blocking(move || {
let parser =
Parser::with_limits(&script_owned, max_ast_depth, max_parser_operations);
parser.parse()
})
.await
})
.await
})
.await;

let ast = match parse_result {
Ok(Ok(result)) => {
match &result {
Ok(_) => {
#[cfg(feature = "logging")]
tracing::debug!(target: "bashkit::parser", "Parse completed successfully");
}
Err(_e) => {
#[cfg(feature = "logging")]
tracing::warn!(target: "bashkit::parser", error = %_e, "Parse error");
.await;

match parse_result {
Ok(Ok(result)) => {
match &result {
Ok(_) => {
#[cfg(feature = "logging")]
tracing::debug!(target: "bashkit::parser", "Parse completed successfully");
}
Err(_e) => {
#[cfg(feature = "logging")]
tracing::warn!(target: "bashkit::parser", error = %_e, "Parse error");
}
}
result?
}
Ok(Err(join_error)) => {
#[cfg(feature = "logging")]
tracing::error!(
target: "bashkit::parser",
error = %join_error,
"Parser task failed"
);
return Err(Error::Parse(format!("parser task failed: {}", join_error)));
}
Err(_elapsed) => {
#[cfg(feature = "logging")]
tracing::error!(
target: "bashkit::parser",
timeout_ms = parser_timeout.as_millis() as u64,
"Parser timeout exceeded"
);
return Err(Error::ResourceLimit(LimitExceeded::ParserTimeout(
parser_timeout,
)));
}
result?
}
Ok(Err(join_error)) => {
#[cfg(feature = "logging")]
tracing::error!(
target: "bashkit::parser",
error = %join_error,
"Parser task failed"
);
return Err(Error::Parse(format!("parser task failed: {}", join_error)));
}
Err(_elapsed) => {
#[cfg(feature = "logging")]
tracing::error!(
target: "bashkit::parser",
timeout_ms = parser_timeout.as_millis() as u64,
"Parser timeout exceeded"
);
return Err(Error::ResourceLimit(LimitExceeded::ParserTimeout(
parser_timeout,
)));
}
};

Expand Down Expand Up @@ -1562,6 +1578,7 @@ impl BashBuilder {
interpreter.set_history_file(hf);
}

#[cfg(not(target_family = "wasm"))]
let parser_timeout = limits.parser_timeout;
let max_input_bytes = limits.max_input_bytes;
let max_ast_depth = limits.max_ast_depth;
Expand All @@ -1571,6 +1588,7 @@ impl BashBuilder {
Bash {
fs,
interpreter,
#[cfg(not(target_family = "wasm"))]
parser_timeout,
max_input_bytes,
max_ast_depth,
Expand Down
15 changes: 15 additions & 0 deletions examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,3 +101,18 @@ LangChain.js ReAct agent with `DynamicStructuredTool`.
export OPENAI_API_KEY=sk-...
node examples/langchain_agent.mjs
```

### browser/

Bashkit running in the browser via WebAssembly. A minimal terminal UI that
lets you type bash commands and see output — all executed client-side in a
sandboxed WASM interpreter.

Requires `Cross-Origin-Opener-Policy` and `Cross-Origin-Embedder-Policy`
headers for `SharedArrayBuffer` support (Vite config handles this).

```bash
cd examples/browser
npm install
npm run dev
```
36 changes: 36 additions & 0 deletions examples/browser/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# Bashkit Browser Example

A sandboxed Bash interpreter running entirely in the browser via WebAssembly.

![Bashkit Browser Terminal](demo.png)

## Quick Start

```bash
# Requires: rustup target add wasm32-wasip1-threads
npm install
npm start
```

`npm start` builds the WASM binary and starts the Vite dev server. Open http://localhost:5173.

## How It Works

Bashkit compiles to `wasm32-wasip1-threads` via [napi-rs](https://napi.rs). The browser loads the WASM binary through `@napi-rs/wasm-runtime`, which provides WASI preview1 support and a thread pool using Web Workers + SharedArrayBuffer.

The terminal UI is a single `index.html` — no framework, no build step beyond WASM compilation.

## Scripts

| Command | Description |
|---------|-------------|
| `npm start` | Build WASM + start dev server |
| `npm run dev` | Start dev server (WASM must already be built) |
| `npm run build` | Build WASM + production bundle |
| `npm run build:wasm` | Build WASM only |

## Requirements

- Node.js >= 18
- Rust with `wasm32-wasip1-threads` target
- Browser with SharedArrayBuffer support (requires COOP/COEP headers, configured in `vite.config.js`)
Binary file added examples/browser/demo.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Loading