← Community
bugfixed

Teams relay I/O incorrectly inherits a loaded module's network allowlist, blocking manifest publish/sync

DaveDave#317d ago · 148 views
affected: station-v0.68fixed in: station-v0.72

Reproduced on station-v0.68.

Teams control-plane relay requests appear to execute under the network sandbox context of a loaded module.

My loaded module dave/stripe-invoicing only allows:

  • api.stripe.com
  • api.groq.com

Running:

railcall team create "Dave Test Mesh" "Dave"

creates the team locally, but relay publication fails:

team created: tm_b4d6e4e6b9786cd9 (manifest v1)

WARNING: relay publish failed (module 'dave/stripe-invoicing' tried urllib.request.urlopen to 'railcall-marketplace-lggm.onrender.com' — not in the manifest's network allowlist (api.stripe.com, api.groq.com).)

railcall team sync fails for the same reason:

error: relay fetch failed: module 'dave/stripe-invoicing' tried urllib.request.urlopen to 'railcall-marketplace-lggm.onrender.com' — not in the manifest's network allowlist (api.stripe.com, api.groq.com).

I then disabled the Stripe module and restarted Station. The module-sandbox error disappeared.

Because the original team create had failed to publish manifest v1, the first sync reached the relay and returned 404 as expected.

With the Stripe module still disabled, I manually POSTed the exact locally signed manifest to:

POST /relay/team/manifest

Result:

PUBLISH: 200 {"ok":true,"team_id":"tm_b4d6e4e6b9786cd9","version":1}

Immediately afterward:

railcall team sync

Result:

manifest v1 (already current)

Restoring the Stripe module and restarting Station causes the Teams relay request to be blocked by the Stripe module's allowlist again.

A/B/A reproduction:

Stripe module loaded

→ Teams relay I/O blocked by Stripe module allowlist

Stripe module disabled

→ Relay reachable and manifest publish/sync works

Stripe module restored

→ Teams relay I/O blocked by Stripe module allowlist again

The Teams implementation in workbench/routes/team.py uses:

POST /relay/team/manifest

GET /relay/team/manifest/<team_id>

These are Station-level Teams control-plane operations and should not inherit the network policy of an unrelated loaded module.

Impact:

A loaded module with a restrictive network allowlist can prevent Station-level Teams manifest publication and synchronization. team create can leave a locally created team unpublished, preventing teammates from joining/syncing.

Expected:

Teams relay I/O executes in the Station/Teams control-plane network context, independently of loaded module network allowlists.

Actual:

Teams relay I/O is attributed to and restricted by the loaded dave/stripe-invoicing module's network allowlist.

5 pts

2 replies

Confirmed — and it's a real one. Thank you, this is a sharp report.

Your A/B/A reproduction is exactly right, and you even pointed at the correct file. Root cause:

The module network gate (workbench/module_sandbox.py) wraps the process-global stdlib — urllib.request.urlopen, http.client.HTTPConnection, socket.socket.connect — and, before this fix, those wraps enforced unconditionally the moment any requires.network module loaded. There was no "am I inside the module?" guard. So once dave/stripe-invoicing (allow: api.stripe.com, api.groq.com) was loaded, every outbound call in the station process inherited that allowlist — including the Teams relay client in routes/team.pyrelay_client.py, which does urllib.request.urlopen(...) to the marketplace host. Hence the SandboxViolation blaming the Stripe module for a Teams control-plane request. It also meant two gated modules cross-contaminated: whichever loaded last governed everyone.

Fix (committed, ships in station-v0.69): enforcement is now scoped to a contextvars context that is armed only while a module handler is on the call stack (sandbox_active(slug)), and against that module's own allowlist. When it's unset — i.e. station-level I/O: the Teams relay, LLM calls, the marketplace client — the wraps pass straight through to the real stdlib. Handlers are wrapped once at registration so every caller (workflow engine or direct command dispatch) enters the scope.

Regression test added (test_sandbox_network_scope.py), all green:

  • station urlopen to the relay host is not blocked with a gated module loaded (your exact case)
  • module code is still blocked from off-allowlist hosts (the sandbox still does its job)
  • two gated modules no longer cross-contaminate — each enforces its own list
  • leaving a module scope restores station passthrough

Marking confirmed now; it flips to fixed when v0.69 cuts (the fix is on the release branch, not yet shipped — didn't want to claim a version you can't update to yet). +5 credit — platform-wide correctness bug, caught with a clean repro. This is the third class of sandbox-global-leak you've helped us close (after v0.37 and the v0.56 socket case). Genuinely valuable work.

— Sami

Fixed in station-v0.72 — closing the loop on this one, @davelab.tech, with apologies for the lag between the fix shipping and this thread being updated.

Your A/B/A repro was exactly right: the network gate wrapped the process-global stdlib (urllib/http.client/socket), so the moment any module with a requires.network block loaded, station-level I/O — including the Teams relay client in routes/team.py — inherited that module's allowlist. A Stripe-only module could block team create/sync, and with two gated modules loaded, the last-installed allowlist governed everyone.

The fix (shipped v0.72): the stdlib wraps now enforce only while a module handler is on the call stack, and against that module's own allowlist — carried in a contextvar armed around the handler invocation. Station/Teams/LLM/marketplace I/O runs with no active sandbox and passes straight through. A follow-up hardened the thread edge: a module that offloads I/O to a spawned thread (including Thread subclasses) stays gated, so the scoping can't be escaped that way either.

Re-verified today on current (v0.87): with your exact scenario armed (dave/stripe-invoicing, allowlist api.stripe.com+api.groq.com), station urlopen to the relay host passes, module calls to non-allowlisted hosts are blocked, and each module is checked against its own allowlist. The regression suite includes a test literally named for this report. Credited — the A/B/A isolation in your report made the root cause unambiguous.

Sign in to reply.