Open
Conversation
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
This PR introduces
deepbash, an in-process WASM-based bash execution backend for deepagents. Instead of shelling out to Docker containers, remote sandboxes, or the host OS, agents execute bash commands inside a WebAssembly sandbox with a virtual filesystem all within a single node processThe Problem
We need to give models access to a shell. The current approaches all have fundamental tradeoffs:
The core tension: virtual backends (in-memory filesystem) can't expose their state to child processes spawned via
execute(). A real bash process can't see your virtual FS. Containers solve this but add latency, infrastructure dependencies, and opacity. We wanted something that runs in-process, gives subprocess access to a shared virtual filesystem, and lets the host introspect every file operation.The Solution: WASIX in WebAssembly
WASIX extends the WASI (WebAssembly System Interface) spec with POSIX features that standard WASI lacks — critically, subprocess spawning (
proc_spawn) and Berkeley sockets (sock_open,sock_connect). Processes inside a WASIX sandbox share a virtual filesystem, meaningbash -c "python script.py && cat output.json"works because both bash and python see the same virtual files.We forked the wasmer-js runtime (~5K lines of Rust) into a purpose-built package that strips networking, the Wasmer registry client, and deployment infrastructure, replacing them with fully offline package resolution from locally bundled
.webcassets. The result is a self-contained WASM module (~5.4 MB) that boots a complete bash + coreutils environment with zero network calls.Note
This is a pretty exploratory effort. I do think this is promising, but a fundamental limitation of this is that we can only bring binaries to the sandbox that can run in WASM (we can run python, but we can't run V8, rust, go, and a lot of other tooling which might make this incompatible for general purpose coding work)
Something I want to explore next is the same syscall introspection using https://github.com/butter-dot-dev/bvisor -- it's a little bit heavier but can give us the same backend introspection and without the process limits we have here
Architecture
flowchart TB subgraph Harness["Agent Harness (Node.js)"] direction TB subgraph Top[" "] direction LR AgentLoop["Agent Loop<br>(LLM, tools, langgraph)"] Mounts["BackendProtocol Mounts<br>/work → StateBackend<br>/data → FilesystemBackend"] end subgraph Backend["DeepbashBackend (extends BaseSandbox)"] VFS["In-memory virtual FS<br>(Map‹string, Uint8Array›)"] MountSys["Composable mount system"] SnapDiff["Snapshot/diff sync"] RPC["RPC-based subagent spawning"] end subgraph Runtime["deepbash-runtime (Rust → WASM, forked wasmer-js)"] WASIX["WASIX syscall layer<br>(wasmer-wasix 0.601.0)"] Workers["Multi-threaded execution<br>via Web Workers"] Offline["Offline package resolution<br>(InMemorySource)"] Assets["Bundled assets:<br>bash.webc + coreutils.webc"] end AgentLoop --> Backend Mounts --> Backend Backend --> Runtime end subgraph Executables["Bundled WASIX Executables"] Bash["bash (1.8 MB .webc)<br>GNU Bash 5.2"] Coreutils["coreutils (4.7 MB .webc)<br>100+ POSIX utilities"] Subagent["subagent (110 KB .wasm)<br>RPC CLI for agent spawning"] end Runtime --> ExecutablesExecution Flow
When an agent calls
execute("grep -r TODO src/ | wc -l"):sequenceDiagram participant Agent as Agent Loop participant DB as DeepbashBackend participant Mounts as Mounted Backends participant WASIX as WASIX Runtime (WASM) participant RPC as /.rpc/requests/ Agent->>DB: execute("grep -r TODO src/ | wc -l") rect rgb(240, 248, 255) Note over DB,Mounts: Step 1-2: Snapshot DB->>Mounts: globInfo("**/*") for each mount Mounts-->>DB: File listings DB->>Mounts: downloadFiles([...]) Mounts-->>DB: File contents (Uint8Array) DB->>DB: Populate WASIX Directory objects end rect rgb(240, 255, 240) Note over DB,RPC: Step 3: Mount subagent infra DB->>DB: /usr/local/sbin/subagent → subagent.wasm DB->>DB: /.rpc/requests/ → empty spool dir end rect rgb(255, 248, 240) Note over DB,WASIX: Step 4-5: Execute in WASM DB->>WASIX: entrypoint.run({ args: ["-c", cmd], mount: {...} }) WASIX->>WASIX: bash forks grep → forks wc Note right of WASIX: All subprocesses share virtual FS WASIX->>RPC: subagent spawn "task" → writes JSON WASIX-->>DB: { stdout, stderr, code } end rect rgb(248, 240, 255) Note over DB,Mounts: Step 7: Diff & sync back DB->>DB: Walk Directory post-execution DB->>DB: Byte-level diff vs pre-snapshot DB->>Mounts: uploadFiles(changed/new files) end rect rgb(255, 240, 245) Note over DB,RPC: Step 8: Collect spawn requests DB->>RPC: Read *.json files RPC-->>DB: SpawnRequest objects end DB-->>Agent: { output, exitCode, truncated, spawnRequests }Key Design Decisions
1. Why fork wasmer-js instead of using
@wasmer/sdk?The
@wasmer/sdknpm package works, but it has problems for our use case:bash.webcdependencies (coreutils) are resolved by queryingregistry.wasmer.iovia GraphQL and downloading ~4.5 MB fromcdn.wasmer.ioon every cold startDirectoryclass is non-extensible — you can't inject custom filesystem handlers (not implemented yet, but the idea is we can pass direct backend protocol methods to the runtime)The fork (
deepbash-runtime) preserves the battle-tested execution machinery (Command.run()→task_dedicated→run_command) while replacing the registry/networking layer withInMemorySource+ aregisterLocalPackage()API. The hot path is now:flowchart LR subgraph Before["BEFORE (@wasmer/sdk)"] direction LR B1[bash.webc] --> B2[GraphQL query] --> B3[HTTP download] --> B4[parse Container] end subgraph After["AFTER (deepbash-runtime)"] direction LR A1[bash.webc] --> A2[InMemorySource lookup] --> A3[local bytes] end style After fill:#e6ffe6,stroke:#4caf50 style Before fill:#fff3e0,stroke:#ff98002. Why WASIX instead of standard WASI?
WASIX is Wasmer's extension of WASI that adds features standard WASI lacks:
proc_spawn(subprocesses)sock_*(Berkeley sockets)pthread_create)fd_pipe(inter-process pipes)proc_signal(signals)The subprocess story is the dealbreaker. Without
proc_spawn,bash -c "grep foo *.py | wc -l"can't work — bash needs to fork child processes that share the filesystem. WASIX is the only WASM standard that supports this, and Wasmer is the only runtime that implements it. Both are MIT licensed.3. Subagent spawning: filesystem RPC, not stdout markers
Instead of parsing stdout for magic markers (fragile, can collide with real output), we use a filesystem-based RPC protocol:
Inside the sandbox, the
subagentCLI (a 110 KB Rust binary compiled to WASM) writes JSON request files:On the host side, after command execution completes,
DeepbashBackendreads/.rpc/requests/*.json, parses theSpawnRequestobjects, and returns them alongside the command output. The deepagents middleware then creates actual subagents from these requests.Why this approach:
/.rpc/requests/ls /.rpc/requests/to see pending spawnsmethodfield supports future RPC operations beyondspawn4. Composable filesystem mounts via BackendProtocol
Rather than a monolithic virtual filesystem, deepbash supports mounting any BackendProtocol implementation into the WASIX sandbox at arbitrary paths:
Before execution, files are downloaded from each mounted backend and populated into WASIX
Directoryobjects. After execution, a byte-level diff detects changes and uploads modified/new files back to the original backend. The WASIX process sees a unified filesystem while each mount path is backed by a different storage system.This means an agent can
cat /data/config.json, edit it, andcpit to/work/— the change to/data/config.jsonflows back to the real filesystem, while the copy in/work/stays in ephemeral state.5. Interactive shell mode (daemon + attach)
Beyond batch
execute()calls, deepbash supports persistent interactive shell sessions:This enables the daemon/attach architecture demonstrated in
examples/: a background process owns the WASIX sandbox, and multiple clients can attach to it for interactive shell access. The shell process persists across commands, maintaining environment variables, working directory, and process state.The Rust Layer
deepbash contains two Rust crates:
rust/runtime/— The WASIX runtime (~5K lines, forked from wasmer-js)Core dependencies:
wasmer 6.1.0— WASM engine (JS backend)wasmer-wasix 0.601.0— WASIX syscall implementationwebc 10.0.1— WEBC container parservirtual-fs 0.601.0— Virtual filesystem traitsKey modifications from upstream:
src/lib.rs— AddedregisterLocalPackage()+setSdkUrl()APIssrc/package_loader.rs— Checks localGLOBAL_CONTAINERSstore before HTTPsrc/runtime.rs—InMemorySourcereplacesBackendSourcefor dependency resolutionsrc/net.rs— Gutted (empty module)src/registry/,wasmer-backend-apidep,bincodedep, WebSocket supportrust/subagent-cli/— The subagent RPC binary (~165 lines)A minimal Rust program compiled to
wasm32-wasip1that provides thesubagent spawn <task>command inside the WASIX environment. Writes JSON files to/.rpc/requests/using only POSIX filesystem operations. Compiled output: 110 KB.How DeepbashBackend Fits the Framework
deepbash implements
SandboxBackendProtocolfrom the deepagents framework by extendingBaseSandbox:classDiagram class BackendProtocol { <<interface>> read() write() edit() grep() glob() } class SandboxBackendProtocol { <<interface>> execute() id } class BaseSandbox { <<abstract>> read/write/grep/glob via shell execute()* uploadFiles()* downloadFiles()* } class DaytonaSandbox { remote container } class ModalSandbox { remote container } class DenoSandbox { Deno Deploy } class DeepbashBackend { in-process WASM ← this PR } BackendProtocol <|-- SandboxBackendProtocol SandboxBackendProtocol <|-- BaseSandbox BaseSandbox <|-- DaytonaSandbox BaseSandbox <|-- ModalSandbox BaseSandbox <|-- DenoSandbox BaseSandbox <|-- DeepbashBackendBaseSandboxprovides default implementations for all file operations (read,write,edit,grep,glob) by composing pure POSIX shell commands and routing them throughexecute(). This means DeepbashBackend only needs to implement three methods:execute(),uploadFiles(),downloadFiles(). Everything else — the entireBackendProtocolsurface — works automatically through the inherited shell-based implementations.The integration with
createDeepAgentis a one-liner:File Changes Summary
New package:
libs/deepbash/(~15,000 lines added)src/backend.ts,src/types.ts,src/index.ts,src/node.tsrust/runtime/src/**(~50 files)rust/subagent-cli/src/main.rsassets/bash.webc,assets/coreutils.webc,assets/subagent.wasmtests/backend.test.ts,tests/backend.int.test.ts,tests/mounts.int.test.ts,tests/spawn.int.test.tsexamples/daemon.ts,examples/attach.ts,examples/test.tsrollup.config.mjs,scripts/download-assets.sh,package.jsonVerification
What's Next
FileSystemtrait enables read-only mounts, write-blocklists, etc. (architecture supports it, not yet implemented)FileSystemtrait callbacks for streaming file change events during executionstack_checkpoint/stack_restore: WASIX syscalls for pausing and resuming execution mid-command