🔬 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

@tarastaras 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

BackendVerdictBasis
Worker Shellinclude initially, scopedRuns natively; containment measured (~50 escape attempts, zero leaks, no native-execution path); one availability hazard needs a boundary decision
Worker JavaScriptdeferFull 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-actionsBot 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
OwnerAuthor

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
tarasforce-pushed the spike/issue-351-worker-backends branch from d546046 to 702861bCompareAugust 7, 2026 03:06

@github-actionsgithub-actionsBot 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 mainAug 7, 2026
10 checks passed
@tarastaras mentioned this pull request Aug 7, 2026
4 tasks
@taras

taras commented Aug 7, 2026

Copy link
Copy Markdown
OwnerAuthor

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 freeto 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

@taras
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all \u003cpre\u003e\u003ccode\u003e blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks"); } } catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); } })(); (function(){ try { var __m = "github.com"; var __re = new RegExp('^' + "github\\.com" + '
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

@tarastaras 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

BackendVerdictBasis
Worker Shellinclude initially, scopedRuns natively; containment measured (~50 escape attempts, zero leaks, no native-execution path); one availability hazard needs a boundary decision
Worker JavaScriptdeferFull 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-actionsBot 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
OwnerAuthor

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
tarasforce-pushed the spike/issue-351-worker-backends branch from d546046 to 702861bCompareAugust 7, 2026 03:06

@github-actionsgithub-actionsBot 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 mainAug 7, 2026
10 checks passed
@tarastaras mentioned this pull request Aug 7, 2026
4 tasks
@taras

taras commented Aug 7, 2026

Copy link
Copy Markdown
OwnerAuthor

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 freeto 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

@taras
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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

@tarastaras 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

BackendVerdictBasis
Worker Shellinclude initially, scopedRuns natively; containment measured (~50 escape attempts, zero leaks, no native-execution path); one availability hazard needs a boundary decision
Worker JavaScriptdeferFull 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-actionsBot 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
OwnerAuthor

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
tarasforce-pushed the spike/issue-351-worker-backends branch from d546046 to 702861bCompareAugust 7, 2026 03:06

@github-actionsgithub-actionsBot 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 mainAug 7, 2026
10 checks passed
@tarastaras mentioned this pull request Aug 7, 2026
4 tasks
@taras

taras commented Aug 7, 2026

Copy link
Copy Markdown
OwnerAuthor

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 freeto 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

@taras
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length \u003e 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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

@tarastaras 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

BackendVerdictBasis
Worker Shellinclude initially, scopedRuns natively; containment measured (~50 escape attempts, zero leaks, no native-execution path); one availability hazard needs a boundary decision
Worker JavaScriptdeferFull 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-actionsBot 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
OwnerAuthor

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
tarasforce-pushed the spike/issue-351-worker-backends branch from d546046 to 702861bCompareAugust 7, 2026 03:06

@github-actionsgithub-actionsBot 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 mainAug 7, 2026
10 checks passed
@tarastaras mentioned this pull request Aug 7, 2026
4 tasks
@taras

taras commented Aug 7, 2026

Copy link
Copy Markdown
OwnerAuthor

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 freeto 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

@taras
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
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

@tarastaras 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

BackendVerdictBasis
Worker Shellinclude initially, scopedRuns natively; containment measured (~50 escape attempts, zero leaks, no native-execution path); one availability hazard needs a boundary decision
Worker JavaScriptdeferFull 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-actionsBot 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
OwnerAuthor

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
tarasforce-pushed the spike/issue-351-worker-backends branch from d546046 to 702861bCompareAugust 7, 2026 03:06

@github-actionsgithub-actionsBot 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 mainAug 7, 2026
10 checks passed
@tarastaras mentioned this pull request Aug 7, 2026
4 tasks
@taras

taras commented Aug 7, 2026

Copy link
Copy Markdown
OwnerAuthor

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 freeto 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

@taras
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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

@tarastaras 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

BackendVerdictBasis
Worker Shellinclude initially, scopedRuns natively; containment measured (~50 escape attempts, zero leaks, no native-execution path); one availability hazard needs a boundary decision
Worker JavaScriptdeferFull 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-actionsBot 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
OwnerAuthor

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
tarasforce-pushed the spike/issue-351-worker-backends branch from d546046 to 702861bCompareAugust 7, 2026 03:06

@github-actionsgithub-actionsBot 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 mainAug 7, 2026
10 checks passed
@tarastaras mentioned this pull request Aug 7, 2026
4 tasks
@taras

taras commented Aug 7, 2026

Copy link
Copy Markdown
OwnerAuthor

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 freeto 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

@taras
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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

@tarastaras 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

BackendVerdictBasis
Worker Shellinclude initially, scopedRuns natively; containment measured (~50 escape attempts, zero leaks, no native-execution path); one availability hazard needs a boundary decision
Worker JavaScriptdeferFull 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-actionsBot 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
OwnerAuthor

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
tarasforce-pushed the spike/issue-351-worker-backends branch from d546046 to 702861bCompareAugust 7, 2026 03:06

@github-actionsgithub-actionsBot 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 mainAug 7, 2026
10 checks passed
@tarastaras mentioned this pull request Aug 7, 2026
4 tasks
@taras

taras commented Aug 7, 2026

Copy link
Copy Markdown
OwnerAuthor

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 freeto 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

@taras
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
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

@tarastaras 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

BackendVerdictBasis
Worker Shellinclude initially, scopedRuns natively; containment measured (~50 escape attempts, zero leaks, no native-execution path); one availability hazard needs a boundary decision
Worker JavaScriptdeferFull 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-actionsBot 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
OwnerAuthor

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
tarasforce-pushed the spike/issue-351-worker-backends branch from d546046 to 702861bCompareAugust 7, 2026 03:06

@github-actionsgithub-actionsBot 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 mainAug 7, 2026
10 checks passed
@tarastaras mentioned this pull request Aug 7, 2026
4 tasks
@taras

taras commented Aug 7, 2026

Copy link
Copy Markdown
OwnerAuthor

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 freeto 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

@taras