Skip to content

🔬 Spike: run Cloudflare Worker Shell and Worker JavaScript natively in Deno (#351) - #353

Merged
taras merged 2 commits into
mainfrom
spike/issue-351-worker-backends
Aug 7, 2026
Merged

🔬 Spike: run Cloudflare Worker Shell and Worker JavaScript natively in Deno (#351)#353
taras merged 2 commits into
mainfrom
spike/issue-351-worker-backends

Conversation

@taras

@taras taras commented Aug 6, 2026

Copy link
Copy Markdown
Owner

Why

#351 asks whether Cloudflare Computer's Worker Shell and Worker JavaScript can run over the Deno-local DOFS Workspace (#349) without workerd, Wrangler, Docker, or another JavaScript runtime — so #346 can pick the smallest local topology that still supplies useful API.Process behavior. Both are optional imperative extensions over the declarative durable core, not prerequisites for it.

Measured verdicts

Backend Verdict Basis
Worker Shell include initially, scoped Runs natively; containment measured (~50 escape attempts, zero leaks, no native-execution path); one availability hazard needs a boundary decision
Worker JavaScript defer Full contract works, but isolation holds only in a compiled artifact and CPU-bound user code cannot be preempted

Bundled workerd is not necessary for this scope — both execution models run natively in Deno at 139.7 MB carrying both backends plus DOFS, versus 191 MB for the workerd host.

What changes

Before: #349 proved the Deno-local filesystem but had no imperative execution over it; the only proven backends were #347's, inside workerd.

After: deno task spike:351 builds one compiled artifact that runs both backends against the #349 workspace, with 9 scenario tests covering coherence, isolation, semantics, and refusal behavior.

How it works

deno task spike:351
  → vendor: build @cloudflare/dofs (shared with the #349 spike, not vendored twice)
  → build:  deno compile → dist/proof
  → test:   9 scenarios; each op is a separate process against the same database
  • Shell: Cloudflare's WorkspaceFsAdapter vendored byte-identical + just-bash. The WorkerEntrypoint/Workers-RPC/Dynamic-Worker glue exists only to reach a filesystem across an isolate boundary, so it disappears when the workspace is in-process. New code: a ~60-line shim for the three stub-only methods.
  • JavaScript: a port of WorkspaceRuntimeLoader — in-memory module graph as blob URLs with specifiers rewritten to absolute blob URLs (relative imports don't resolve from a blob base), run in a Deno Worker, filesystem capability crossing as async postMessage RPC. Nothing is materialized to disk.

Key findings (full detail in evidence/EVIDENCE.md)

  • Shell containment is architectural, and was measured — canaries planted, ~50 attempts, zero leaks: /etc/passwd, ../../.. traversal, find /, grep -r / all resolve inside DOFS; environment is fabricated; no native execution exists (/bin/sh, id, uname → command not found); QuickJS guest has no Deno, blocked Function constructor, workspace-scoped require("fs"); network off by default, with allowlist + private/loopback rejection + DNS-rebinding pinning when enabled.
  • just-bash's own defense-in-depth must be disabled under Deno (on by default, hard-fails) — the same one-line opt-out Cloudflare already ships for workerd. Containment holds without it.
  • The shell's real hazard is availability, not capability: a 6.4 s CPU-bound loop starved the host event loop to 0 of 127 expected ticks — the abort timer never fired. Recommendation: run it in a Deno Worker (10/10 parity unmodified, 39.6 ms boot, ~0.04 ms/call).
  • JS isolation depends on shipping compiled: permissions: "none" governs Deno ops, not the module loader — under deno run a locked worker still imports jsr:/npm:; deno compile's frozen graph closes it. A deno run host must never be presented as a sandbox.
  • Named missing primitive: Worker.terminate() cannot preempt CPU-spinning JS. Needed Deno-side: a CPU/wall budget on new Worker(...), or an interrupting terminate(). Cloudflare's limits.cpuMs has no equivalent.
  • Explicit refusal, never fallback — an operation the host doesn't install fails where it's called, at both refusal points; the isolate has no path to host execution.
  • Upstream defects found (affect Cloudflare equally): relative symlinks broken in @cloudflare/dofs; a redirect to a missing parent throws out of bash.exec(); ls -l reports a stale mode.

What must stay true

  • The four verification gates keep their scope: spikes is root-excluded, the spike owns its deps, no root dependency changed. All four run clean at root.
  • Vendored Cloudflare source is pinned, MIT-noticed, and byte-identical (sha1 recorded); just-bash is an ordinary npm dependency (Apache-2.0, Vercel Labs — not Cloudflare).
  • No production <Workspace>, provider, process API, or declarative components — evidence only.

How to verify it

  • deno task spike:351 from the repo root.
  • Coherence tests fail if a host write isn't visible to the shell or vice versa across restarts; isolation tests fail if user code reaches host files or the environment; the refusal test fails if an uninstalled operation silently succeeds; the native-execution test fails if a /bin/* invocation ever runs.
  • Security and availability claims beyond the suite are reproducible from evidence/probes/ (every command and escape attempt).

Scope

Included

  • spikes/351-worker-backends/, root task aliases.

Intentionally unchanged

Risks and limitations

  • Worker JavaScript is deferred, not adopted — shipping it requires resolving the preemption gap or accepting an out-of-process worker.
  • python3 inside just-bash remains unusable under Deno (off by default); js-exec/sqlite3 need a one-line process.connected prelude.
  • Timing and size figures are single-host measurements (macOS arm64).

@github-actions

github-actions Bot commented Aug 6, 2026

Copy link
Copy Markdown

PR #353: 🔬 Spike: run Cloudflare Worker Shell and Worker JavaScript natively in Deno (#351)

23 files, +4163 / -4

Scope

🔴 PR has 4167 lines changed. Split into focused PRs.

🟡 4167 lines changed. PRs under 400 receive more thorough review.

🟡 23 files changed. Are all changes related?

🟡 PR mixes config and source changes.

🟡 New abstraction files: spikes/351-worker-backends/vendor/worker-shell/adapter.ts. Verify 3+ consumers.

Structural

🟡 4 console statements.

Slop

  • spikes/351-worker-backends/vendor/worker-shell/adapter.ts:33// shape matches that stub one-for-one.
  • spikes/351-worker-backends/vendor/worker-shell/adapter.ts:219// what just-bash's other adapters do.
  • spikes/351-worker-backends/vendor/worker-shell/adapter.ts:257// missing entry surfaces ENOENT.

Static Analysis

✅ Oxlint found no issues.

Correctness

No extraneous code patterns detected.

@taras

taras commented Aug 6, 2026

Copy link
Copy Markdown
Owner Author

CI scope note: this PR targets spike/issue-349-dofs-adapters (it consumes that spike's vendored DOFS), and .github/workflows/ci.yml triggers only on pull requests targeting main. So the eight-job suite — lint, test-deno, test-node, test-bun, jsr, smoke, composability, site — did not run here; the two green checks are review and the Deno Deploy build only. Do not read them as a full CI pass.

The four verification gates were run locally on this branch instead, all clean:

  • deno task lint — 0 errors, all files correctly formatted (423 files)
  • deno task check — no errors
  • deno task test — 363 passed (2502 steps), 0 failed
  • deno task check:jsr — Success, dry run complete

Plus the spike's own suite: deno task spike:351 — 9 passed, 0 failed.

Full CI will run once #350 merges and this retargets to main.

taras added 2 commits August 6, 2026 23:05
#351)

Both of Cloudflare Computer's imperative backends execute from one compiled
Deno artifact against the #349 workspace, with no workerd, Wrangler, Docker,
or second JavaScript runtime. The shell reuses Cloudflare's filesystem
adapter byte-identical and drops the Dynamic Worker transport, since the
workspace is in-process. The JavaScript backend is a port of
WorkspaceRuntimeLoader: an in-memory module graph supplied as blob URLs with
rewritten specifiers, run in a Deno Worker, with the workspace capability
crossing as async postMessage RPC and nothing materialized to disk.

Nine scenarios cover bidirectional coherence across restarts, workspace
identity isolation, shell semantics, module graphs with dependencies,
committed-state coherence, and explicit refusal of operations the host does
not install.
)

Worker Shell is include-initially-scoped: it runs natively over the
Deno-local workspace, reuses Cloudflare's adapter byte-identical, and its
containment was measured — around fifty escape attempts with zero leaks and
no native-execution path. Its one hazard is availability, not capability: a
CPU-bound script starves the host event loop, so the boundary recommendation
is a Deno Worker.

Worker JavaScript is defer: the whole contract works, including an in-memory
module graph with no materialization, but isolation holds only in a compiled
artifact and CPU-bound user code cannot be preempted. The missing Deno
primitive is named precisely.

COMPARISON.md answers the two topology questions separately and states that
bundled workerd is not necessary for this scope.
@taras
taras force-pushed the spike/issue-351-worker-backends branch from d546046 to 702861b Compare August 7, 2026 03:06

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Found 3 redundant comments. Inline suggestions to remove them below.

// Declared structurally so tests can swap in a fake or a real stub
// without coupling the adapter to the cloudflare:workers RpcTarget
// base class. Production callers pass `workspace.stub().fs`; the
// shape matches that stub one-for-one.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant comment — restates what the code does.

Suggested change
// shape matches that stub one-for-one.

// The store doesn't have a native rename today, so model mv as
// copy+delete. POSIX mv is atomic when src and dest live on
// the same filesystem; this approach isn't, but it matches
// what just-bash's other adapters do.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant comment — restates what the code does.

Suggested change
// what just-bash's other adapters do.

// through" — for a non-symlink the answer is the canonical
// path; for a symlink the answer is the readlink target
// resolved relative to its parent. Stat the path first so a
// missing entry surfaces ENOENT.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant comment — restates what the code does.

Suggested change
// missing entry surfaces ENOENT.

@taras
taras merged commit d96f5bf into main Aug 7, 2026
10 checks passed
@taras taras mentioned this pull request Aug 7, 2026
4 tasks
@taras

taras commented Aug 7, 2026

Copy link
Copy Markdown
Owner Author

Correction: this evidence spike was mistakenly merged while collapsing the stack and is reverted from main by #364. Its findings remain valid and reviewable here; #362 remains stacked evidence and production implementation belongs to #363.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant