Skip to content

fix(mcp): grant Deno --allow-net for the execute control socket - #43

Open
Miyamura80 wants to merge 2 commits into
beeper:mainfrom
Miyamura80:claude/deno-net-permission-fix-xg4og2
Open

fix(mcp): grant Deno --allow-net for the execute control socket#43
Miyamura80 wants to merge 2 commits into
beeper:mainfrom
Miyamura80:claude/deno-net-permission-fix-xg4og2

Conversation

@Miyamura80

Copy link
Copy Markdown

Fixes #42

Root cause

The execute tool runs model-supplied code in a Deno subprocess via @valtown/deno-http-worker. That library opens its control channel with Deno.serve({ path: <socket under $TMPDIR> }). Since Deno 2.9, binding a Unix socket requires net access on top of read/write access — and the library appends only --allow-read=<sock> / --allow-write=<sock> to the caller's runFlags, never --allow-net. So the subprocess died before it was ready:

error: Uncaught (in promise) NotCapable: Requires net access to
"unix:/var/folders/.../T/...-deno-http.sock", run again with the --allow-net flag
    const server = Deno.serve( ...

which surfaced to callers as Deno exited before being ready on every execute call. search_docs was unaffected. Consistent with a regression from 5.0.0's fix(mcp): remove Stainless sandbox execution mode.

  code-tool.ts                    @valtown/deno-http-worker            deno subprocess
  ------------                    -------------------------            ---------------
  runFlags: [                     socketFile = $TMPDIR/<uuid>          Deno.serve({path})
    --allow-read=<pkg>              -deno-http.sock                       |
    --allow-net=<apiHost>    -->  appends --allow-read=<sock>             |
    --allow-env                   appends --allow-write=<sock>            v
  ]                               (never appends --allow-net)      NotCapable: requires
       ^                                    |                      net access to unix:...
       |                                    v
       +-- caller can't name the      spawn(deno, args)  <--- FIX: spawnFunc hook
           socket: path is internal        argv                 rewrites this argv

The fix

The socket path is generated inside the library (path.join(os.tmpdir(), \${crypto.randomUUID()}-deno-http.sock`)) and is never exposed to callers — there is no option or getter for it, in 0.0.21 or in any later version. So the grant is applied through spawnFunc, a documented public option in DenoWorkerOptions: recover the socket path from the argv the library just built (the only bare, non-flag argument ending in the socket suffix) and append unix:to the existing--allow-net` allowlist.

Building the flag from the argv rather than guessing guarantees it always names the socket actually being served.

The sandbox is otherwise untouched. No -A / --allow-all, no other permission widened. Verified against Deno 2.9.4 that the allowlist matches a Unix socket by full path only--allow-net=unix:<dir> is rejected — so this is as tightly as the permission can be scoped, and the fallback to a bare --allow-net was not needed.

Verification

Tested against Deno 2.9.4 (deno 2.9.4 (stable, release, x86_64-unknown-linux-gnu)).

Standalone reproDeno.serve({ path }) under --allow-read --allow-write throws the exact NotCapable above; adding --allow-net=unix:<path> binds successfully.

End to end through the real execute handler (stub client, no Beeper Client API needed):

before after
return 1 + 1 NotCapable ... unix:/tmp/<uuid>-deno-http.sockDeno exited before being ready returns "2", console.log output captured
fetch("https://example.com") still blocked: NotCapable: Requires net access to "example.com:443"

That last row is the important one: arbitrary outbound network from sandboxed code remains blocked. Only the API base-URL host and the one control socket are reachable.

I could not exercise this against a running Beeper Client API, so the evidence is the standalone repro plus the end-to-end handler run above.

Checks — full suite per CONTRIBUTING, all green:

  • Root yarn lintprettier --check ., eslint ., build, tsc, Are The Types Wrong?, publint.
  • Root yarn test against the ./scripts/mock steady server — 448 tests / 18 suites pass.
  • packages/mcp-serveryarn lint, tsc --noEmit, jest (2/2 pass).

No version bumps or CHANGELOG edits: release-please owns those, and it treats packages/mcp-server/{package.json,manifest.json,yarn.lock} as version-bump targets. Both commits use conventional-commit fix(...) titles so they land under "Bug Fixes" in the generated changelog.

Note for maintainers: this patches generated code

packages/mcp-server/src/code-tool.ts and src/options.ts both carry the Stainless File generated from our OpenAPI spec header, and packages/mcp-server has no src/lib/ escape hatch — only the root SDK package does. spawnFunc is the only seam that can reach the argv, so there is no hand-editable override to use instead.

CONTRIBUTING states modifications to generated code are persisted across regenerations (with possible merge conflicts) rather than silently reverted, and the preceding commit 1c50f9f already patches this same file directly — so I landed it in place. Please route it through the generator config if that's the house preference. My read is that it's better kept here: it's a workaround for a Deno-runtime behavior change, ideally temporary, and the real long-term home is upstream in @valtown/deno-http-worker, which owns both the socket path and the existing "extend the caller's permission flags" logic. Keeping the patch visible in the generated file means the next regeneration conflict forces someone to re-read it.

Worth filing upstream at val.town separately; note that ^0.0.21 pins exactly for a 0.0.x range, so absorbing any upstream fix needs a deliberate bump regardless.

Minor aside (separate commit)

npx @beeper/desktop-mcp --version printed true. yargs treats a single argument to .version() as the version string, so .version(true) printed true. Changed to .version(), which reads the version from package.json — now prints 5.0.0.


🤖 Generated with Claude Code

https://claude.ai/code/session_01H7CWSE6BSsshnh7rZNgomB

claude added 2 commits August 1, 2026 13:22
The execute tool runs model-supplied code in a Deno subprocess, and
@valtown/deno-http-worker talks to that subprocess over a Unix socket served
with `Deno.serve({ path })`. Since Deno 2.9, binding a Unix socket requires net
access in addition to read/write access, so the worker died at startup with:

    NotCapable: Requires net access to "unix:/.../<uuid>-deno-http.sock",
    run again with the --allow-net flag

which surfaced as "Deno exited before being ready" on every execute call.

The socket path is generated inside the worker library and is not exposed to
callers, so grant net access by patching the argv the library builds: find the
socket path it passes to the bootstrap script and append `unix:<path>` to the
existing --allow-net allowlist. Deno's allowlist matches a Unix socket only by
its full path, so this is as tight as the permission can be scoped, and the
sandbox is otherwise unchanged — code running in the worker still reaches only
the API base URL host.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H7CWSE6BSsshnh7rZNgomB
yargs treats a single argument to .version() as the version string, so
.version(true) made `mcp-server --version` print "true". Calling .version()
with no arguments lets yargs read the version out of package.json.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H7CWSE6BSsshnh7rZNgomB
@coderabbitai

coderabbitai Bot commented Aug 1, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: db6a3620-c6c4-4406-875d-1976da1432d2

📥 Commits

Reviewing files that changed from the base of the PR and between c6aa0eb and 8fe641f.

📒 Files selected for processing (2)
  • packages/mcp-server/src/code-tool.ts
  • packages/mcp-server/src/options.ts
📜 Recent review details
🔇 Additional comments (2)
packages/mcp-server/src/options.ts (1)

111-111: LGTM!

packages/mcp-server/src/code-tool.ts (1)

125-145: LGTM!

Also applies to: 170-170, 219-221


📝 Walkthrough

Summary by CodeRabbit

  • New Features
    • Worker processes can now securely access the required Unix control socket while retaining restricted network permissions.
  • Improvements
    • Updated command-line version handling to provide more consistent standard behavior when displaying version information.
  • Security
    • Socket access is limited to the specific worker invocation, helping preserve the existing network access boundaries.

Walkthrough

The Deno worker now receives scoped network access for its Unix control socket. The yargs configuration now uses default version output behavior.

Changes

Deno control-socket access

Layer / File(s) Summary
Worker socket permission wiring
packages/mcp-server/src/code-tool.ts
The worker spawn wrapper detects the Unix control-socket argument and appends its path to the final --allow-net permissions.

Yargs version output

Layer / File(s) Summary
Default version configuration
packages/mcp-server/src/options.ts
Yargs now uses .version() instead of .version(true).

Estimated code review effort: 2 (Simple) | ~10 minutes

Sequence Diagram(s)

sequenceDiagram
  participant codeTool
  participant spawnFunc
  participant DenoWorker
  codeTool->>spawnFunc: provide generated worker arguments
  spawnFunc->>spawnFunc: append scoped unix control-socket access
  spawnFunc->>DenoWorker: launch with patched arguments
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 66.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly identifies the main fix: granting scoped Deno network access for the execute control socket.
Description check ✅ Passed The description explains the Deno socket permission failure, the fix, verification, and the related version output correction.
Linked Issues check ✅ Passed The changes satisfy issue #42 by granting scoped socket access, preserving sandbox restrictions, and correcting the --version output.
Out of Scope Changes check ✅ Passed All changes relate to issue #42, including the secondary --version correction explicitly identified in the linked issue.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Comment @coderabbitai help to get the list of available commands.

@Miyamura80

Copy link
Copy Markdown
Author

@batuhan would appreciate it if you could please review! Thank you

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

Labels

None yet

Development

Successfully merging this pull request may close these issues.

execute fails on Deno 2.9.x: NotCapable net access to unix control socket (Deno.serve spawned without --allow-net)

2 participants