Important
PREVIEW ONLY This package is provided as a preview for feedback only. APIs are unstable and the design is subject to change.
A Cloudflare Worker + Durable Object that runs a Workspace whose
shell is a Dynamic Worker loaded through env.LOADER. The
shell is just-bash;
the user-facing HTTP surface mirrors
examples/container so the same curl
recipes work, just without the container.
client ─► Worker /c/<name>/{file,exec}
│ (DO RPC calls)
â–¼
DO (ContainerExample) ──► Workspace ──► WorkerShellBackend
│
│ env.LOADER.get(...)
â–¼
Dynamic Worker
(ShellWorker)
│
│ env.HOST.get(id)
│ .getWorkspace()
â–¼
back to ContainerExample DO
- The DO constructs a
WorkerShellBackendfrom@cloudflare/computer/backends/worker-shell, passing the Loader binding, a{binding, id}reference to itself, andctx. The backend handles the rest internally: it builds the Loader callback (with the code-split shell modules + the seek-bzip stub), mints aWorkspaceServiceProxyloopback throughctx.exports.WorkspaceServiceProxy(...), callsenv.LOADER.get(...).getEntrypoint("ShellWorker"), and turns the resulting Fetcher into aShellRPC. - The loader callback wires the
WorkspaceServiceProxyloopback into the Dynamic Worker'senv.HOST. The proxy is a small WorkerEntrypoint whosegetWorkspace()method does the namespace lookup on the host side and returns aWorkspaceStub. A rawDurableObjectNamespacedoesn't survive structured clone into the loader's env; the binding-shape Fetcher the proxy produces does. ShellWorker(shipped in@cloudflare/computer/backends/worker-shell) lives inside that Dynamic Worker. Eachexec(input)callsenv.HOST.getWorkspace(), builds a freshBasharound aWorkspaceFsAdapterwrapping the stub's.fs, runs the command, and disposes the stub when the run settles.- Filesystem operations from inside
Bashround-trip through the host DO's own RPC surface, so storage handles stay valid and one workspace per DO is the natural boundary. BackendHandle.syncis"none". There's a single authoritative store (the DO's SQLite); push and pull short-circuit. The runtime result'spushed/pulledcounts are always zero.
The DO is a thin host. There's no Dockerfile; the Dynamic Worker lifecycle is the loader's problem.
PUT /c/<name>/file/workspace/hello.txt writes
/workspace/hello.txt, and
GET /c/<name>/file/workspace/r2/x reads /workspace/r2/x —
the URL and the on-disk path always match. Any URL outside
/workspace returns 400.
exec runs with cwd defaulting to /workspace. Commands run
inside the just-bash interpreter — broad textual tooling
(cat, grep, awk, sed, jq, sort, …) but not the full
Linux userland. The Dynamic Worker has globalOutbound: null,
so the shell can't reach the public internet on its own.
Identical to the container example. Seed once with:
npm run seed:r2:local --workspace @example/computer-worker-shell
# or after deploy
npm run seed:r2 --workspace @example/computer-worker-shellPUT /c/<name>/file/workspace/<path> raw body → writeFile at /workspace/<path>
GET /c/<name>/file/workspace/<path> octet-stream of /workspace/<path>
(any path outside /workspace returns 400)
POST /c/<name>/exec { command | argv, cwd?, encoding? }
cwd defaults to /workspace
→ JSON { exitCode, stdout, stderr }
No Docker, no extra build step. The shell ships as pre-bundled
feature groups inside @cloudflare/computer/backends/worker-shell: an
always-on core plus one optional group per command at
@cloudflare/computer/shell/<feature>. WorkerShellBackend assembles
core with whatever groups you pass to its commands option and
spreads the result into the Loader callback internally. This
example opts curl and sqlite in:
import curlModules from "@cloudflare/computer/shell/curl";
import sqliteModules from "@cloudflare/computer/shell/sqlite";
new WorkerShellBackend({
loader: env.LOADER,
workspace: { binding: "ContainerExample", id: ctx.id.toString() },
ctx,
commands: [curlModules, sqliteModules],
});A group you never import (html-to-markdown, python, js-exec,
yq, file, xan, jq, or either of the two above) is
unreachable in the bundle and the bundler drops it — opting a
command in is a single import, and opting out is deleting it. The
core entry module parses on cold start; each opted-in group's
chunks stay cold until a script reaches for them.
npm run dev --workspace @example/computer-worker-shellSmoke test (same recipes as the container example):
curl http://127.0.0.1:8787/
echo 'hello' | curl -X PUT --data-binary @- \
http://127.0.0.1:8787/c/demo/file/workspace/hello.txt
curl http://127.0.0.1:8787/c/demo/file/workspace/hello.txt
curl -X POST http://127.0.0.1:8787/c/demo/exec \
-H 'content-type: application/json' \
-d '{"command":"cat hello.txt && wc -l hello.txt","encoding":"utf8"}'examples/worker-shell/
wrangler.jsonc Worker + DO + worker_loaders binding
src/index.ts Worker handler + DO (ContainerExample)
Nothing else. The Dynamic Worker source ships from
@cloudflare/computer/backends/worker-shell as a pre-built module
string.
- Exec is run-and-collect. The handler awaits
handle.result()and emits one JSON response. just-bash itself doesn't stream chunks; the shell emits at most one stdout event and one stderr event per run. getExecreattach is intentionally absent. Each exec is scoped to its own call; an id observed in one envelope can't be reached from a later request.- PATH-walk diagnostics in
wrangler dev. just-bash probes every$PATHdirectory for every command name. Each miss prints anUncaught WorkspaceFsError: no such path: ...in the dev log, even though just-bash catches the rejection locally. Cosmetic; exec returns the correct result.