Consolidate egress configuration across backends - #88
Conversation
Default ambient execution network access to blocked across containers and Dynamic Workers. Support explicit direct access or an HTTP gateway while keeping host Git and artifact capabilities independent. Route container gateway requests through authenticated workspace callbacks and scope Worker shell loader identities by policy. Replace the custom shell fetcher path with typed loader and external runtime sources.
🦋 Changeset detectedLatest commit: 2d47263 The changes in this PR will be included in the next version bump. This PR includes changesets to release 1 package
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
|
Thanks for your interest in Cloudflare Computer. This repository does not accept unsolicited pull requests. Please use one of the accepted contribution paths instead:
If a maintainer asked you to open this pull request, they can add the |
commit: |
| this.#container.start({ enableInternet, env }); | ||
| } else if (!this.#container.running) { | ||
| this.#container.start({ enableInternet: true, env }); | ||
| this.#container.start({ enableInternet, env }); |
There was a problem hiding this comment.
🔴 Network-blocking setting is ignored when the container is already up
The chosen network setting is only applied while launching a container (start({ enableInternet, env }) at packages/computer/src/backends/container/container-host.ts:125) and is skipped whenever one is already running, so a workspace asked to have no network can still reach the internet.
Impact: An application that switches to the new blocked-by-default network policy may silently keep full outbound internet access in its container.
Why an already-running container keeps the previous internet setting
WorkspaceContainerAPI.start (packages/computer/src/backends/container/container-host.ts:108-128) only calls this.#container.start(...) when a prior exit is recorded or this.#container.running is false. enableInternet is a launch-time option, so when the container generation was started elsewhere or earlier with a different value, the new value never takes effect. CloudflareContainerBackend.connect derives it from the policy (packages/computer/src/backends/container/cloudflare-container.ts:208) and then proceeds as if it were applied.
A concrete topology in this repo: examples/think-compare-runtimes/worker/computer-container-pool.ts:139 pre-starts warm-pool containers with start({ enableInternet: true, env }). Any backend attached to such a warm container — including the new default { mode: "none" } — inherits full internet access, and neither connect() nor status() reports the mismatch.
Possible mitigations: force a fresh generation (destroy + start) when the running generation's enableInternet differs from the requested one, track the last-applied value alongside the lifecycle state, or at minimum surface an error/warning when the policy cannot be applied.
Prompt for agents
WorkspaceContainerAPI.start now takes an enableInternet flag derived from the new WorkspaceEgressPolicy, but it only calls container.start() when there is a prior exit or the container is not running. enableInternet is a launch-time option, so when a container generation is already running (for example warm-pool containers started with enableInternet: true in examples/think-compare-runtimes/worker/computer-container-pool.ts, or a generation started by an earlier backend with a different policy), the requested policy is silently dropped and the container keeps its old ambient network access. Consider recording the enableInternet value applied to the current generation (alongside the existing lifecycle state used by containerExitInfo/installContainerMonitor) and forcing a destroy+start when the requested value differs, or otherwise making the mismatch visible to the caller instead of silently ignoring it.
Was this helpful? React with 👍 or 👎 to provide feedback.
| readonly #backend = new CloudflareContainerBackend({ | ||
| container: () => this, | ||
| workspace: { binding: "RecipeAgent", id: this.ctx.id.toString() }, | ||
| egress: { mode: "direct" }, |
There was a problem hiding this comment.
🟡 Tutorial walkthrough no longer matches the example it teaches
The tutorial's step-by-step code block still builds the container backend without the new network setting (new CloudflareContainerBackend({...}) at examples/tutorial/README.md:154), while the shipped example file now opts into direct network access, so a reader following the tutorial builds a container with different network behavior than the example.
Impact: Following the tutorial produces an app whose container behaves differently from the checked-in example.
Repository rule and the divergence
AGENTS.md states that examples are real consumers and must be updated in the same change when a public API changes. examples/tutorial/src/index.ts:51 gained egress: { mode: "direct" }, but the walkthrough in examples/tutorial/README.md:152-156, which the README says builds this exact file, was not updated. docs/12_worker_backend.md:165,304 similarly still documents the removed fetcher option of WorkerShellBackend.
Prompt for agents
examples/tutorial/src/index.ts now passes egress: { mode: "direct" } to CloudflareContainerBackend, but the tutorial walkthrough in examples/tutorial/README.md (section 4, the RecipeAgent code block) still shows the constructor without it, so the file a reader builds by following the tutorial differs from the shipped example and gets the new blocked-by-default ambient network policy. Update the README code block (and the surrounding prose if it discusses container network access). While there, docs/12_worker_backend.md still documents the removed WorkerShellBackend `fetcher` option and should point at the new `source` shape.
Was this helpful? React with 👍 or 👎 to provide feedback.
| this.#container.start({ enableInternet, env }); | ||
| } else if (!this.#container.running) { | ||
| this.#container.start({ enableInternet: true, env }); | ||
| this.#container.start({ enableInternet, env }); |
There was a problem hiding this comment.
🟨 Blocked-network policy is not enforced on an already-running container
The container's ambient internet access is only decided at launch time (start({ enableInternet, env }) at packages/computer/src/backends/container/container-host.ts:125). WorkspaceContainerAPI.start skips the launch entirely when the container is already running and no prior exit is recorded, so a backend configured with the new default { mode: "none" } (or an explicit gateway policy) attaches to a generation that may have been started with enableInternet: true. examples/think-compare-runtimes/worker/computer-container-pool.ts:139 is exactly such a topology: warm-pool containers are pre-started with enableInternet: true, and the backend's policy is then silently dropped. Callers get no error or signal that the requested network restriction was not applied.
Was this helpful? React with 👍 or 👎 to provide feedback.
| async interceptAllOutboundHttp(ref: WorkspaceRef, token: string) { | ||
| const exports = (this.#ctx as unknown as { exports: Record<string, unknown> }).exports as { | ||
| WorkspaceProxy: (opts: { props: WorkspaceRef & { egressToken: string } }) => Fetcher; | ||
| }; | ||
| const proxy = exports.WorkspaceProxy({ props: { ...ref, egressToken: token } }); | ||
| await Promise.all([ | ||
| this.#container.interceptAllOutboundHttp(proxy), | ||
| this.#container.interceptOutboundHttps("*", proxy), | ||
| ]); | ||
| } |
There was a problem hiding this comment.
🟨 Container gateway callbacks may bypass the gateway if the internal route wins interception
In http-gateway mode the backend registers both a host-specific interception for the internal callback host and a catch-all interception (packages/computer/src/backends/container/cloudflare-container.ts:209-212, implemented in packages/computer/src/backends/container/container-host.ts:155-164 via interceptAllOutboundHttp plus interceptOutboundHttps("*")). The security of the arrangement depends on the platform's precedence between the two rules, which is not asserted anywhere. If the catch-all wins, the internal /health and /ws callbacks are forwarded to the token-bearing proxy and then out to the external gateway, breaking the session; if the host-specific rule wins for arbitrary hosts (or if plain-HTTP interception does not cover all schemes/ports), container traffic can leave without passing the gateway. Only HTTP/HTTPS is covered at all — raw TCP/UDP from the container is unaffected by this policy.
Was this helpful? React with 👍 or 👎 to provide feedback.
This change gives the container, Worker shell, and Worker JavaScript backends one way to configure ambient network access. Previously the container always had direct Internet access, while the Dynamic Worker backends used backend-specific controls. The Worker shell's custom
fetcheroption also mixed runtime creation with outbound network policy.The shared
WorkspaceEgressPolicysupports three modes.noneblocks ambient network access and is the default,directuses the backend's native Internet access, andhttp-gatewaysends HTTP and HTTPS requests through a host-providedFetcher. The gateway mode does not promise portable interception of arbitrary TCP connections. Trusted host operations, including Git and artifact network access, remain controlled by their existing capability options rather than by ambient egress.A policy can be defined once and passed to any backend:
For a container backend, pass the policy alongside the container host and workspace reference. Container callbacks use an authenticated
WorkspaceProxy, so the Worker must continue to export that entrypoint. Existing applications that need the previous direct Internet behavior must now opt in withdirectEgress.For the managed Worker shell, the same policy is applied before the Loader creates or selects the shell isolate. The policy identity is included in the Loader cache key, and
revisionlets an application choose when a gateway configuration may reuse an isolate.Applications that create the shell runtime through a service binding, dispatch namespace, broker, or pool can use the explicit external runtime source. The source receives the policy before it creates or selects the runtime, and is responsible for enforcing it.
For Worker JavaScript, the policy maps directly to the Dynamic Worker Loader's outbound setting. The existing
globalOutboundoption remains available for compatibility, but it cannot be combined withegress.The change is covered by backend tests for all three modes, Worker shell source and Loader lifecycle tests, container callback authentication tests, and proxy forwarding tests. The container-based examples opt into direct egress so their existing behavior does not change.
Reviewers can verify the change with:
Reference documentation and a production example for the external Worker shell runtime remain follow-up work.