Skip to content

Consolidate egress configuration across backends - #88

Open
aron-cf wants to merge 5 commits into
mainfrom
worker-shell-internet
Open

Consolidate egress configuration across backends#88
aron-cf wants to merge 5 commits into
mainfrom
worker-shell-internet

Conversation

@aron-cf

@aron-cf aron-cf commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

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 fetcher option also mixed runtime creation with outbound network policy.

The shared WorkspaceEgressPolicy supports three modes. none blocks ambient network access and is the default, direct uses the backend's native Internet access, and http-gateway sends HTTP and HTTPS requests through a host-provided Fetcher. 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:

import type { WorkspaceEgressPolicy } from "@cloudflare/computer";

const blockedEgress = { mode: "none" } satisfies WorkspaceEgressPolicy;
const directEgress = { mode: "direct" } satisfies WorkspaceEgressPolicy;

const gatewayEgress = (gateway: Fetcher) =>
  ({
    mode: "http-gateway",
    gateway,
    revision: "allowlist-v1",
  }) satisfies WorkspaceEgressPolicy;

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 with directEgress.

import { WorkspaceProxy } from "@cloudflare/computer";
import { CloudflareContainerBackend } from "@cloudflare/computer/backends/container";

export { WorkspaceProxy };

const backend = new CloudflareContainerBackend({
  container: () => this,
  workspace: {
    binding: "WorkspaceAgent",
    id: this.ctx.id.toString(),
  },
  egress: gatewayEgress(this.env.EGRESS_GATEWAY),
});

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 revision lets an application choose when a gateway configuration may reuse an isolate.

import { WorkspaceServiceProxy } from "@cloudflare/computer";
import { WorkerShellBackend } from "@cloudflare/computer/backends/worker-shell";

export { WorkspaceServiceProxy };

const backend = new WorkerShellBackend({
  source: {
    type: "loader",
    loader: this.env.LOADER,
    workspace: {
      binding: "WorkspaceAgent",
      id: this.ctx.id.toString(),
    },
    ctx: this.ctx,
  },
  egress: blockedEgress,
});

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.

const backend = new WorkerShellBackend({
  source: {
    type: "external-runtime",
    async connect({ egress }) {
      return createShellRuntime({ egress });
    },
  },
  egress: directEgress,
});

For Worker JavaScript, the policy maps directly to the Dynamic Worker Loader's outbound setting. The existing globalOutbound option remains available for compatibility, but it cannot be combined with egress.

import { WorkerJavaScriptBackend } from "@cloudflare/computer/backends/worker-javascript";

const backend = new WorkerJavaScriptBackend({
  loader: this.env.LOADER,
  egress: gatewayEgress(this.env.EGRESS_GATEWAY),
  allowGitNetwork: false,
  allowArtifactNetwork: false,
});

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:

npm run format
npx biome check .
npm run typecheck
npm run build --workspace @cloudflare/computer
npm test --workspace @cloudflare/computer
npm test --workspace @cloudflare/example-think-compare-runtimes

Reference documentation and a production example for the external Worker shell runtime remain follow-up work.

aron-cf added 5 commits August 7, 2026 15:26
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-bot

changeset-bot Bot commented Aug 7, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 2d47263

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@cloudflare/computer Minor

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

@github-actions

github-actions Bot commented Aug 7, 2026

Copy link
Copy Markdown

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 allow-pr label and reopen it.

@github-actions github-actions Bot closed this Aug 7, 2026
@aron-cf aron-cf added the allow-pr Allow a PR to remain open. label Aug 7, 2026
@aron-cf aron-cf reopened this Aug 7, 2026
@pkg-pr-new

pkg-pr-new Bot commented Aug 7, 2026

Copy link
Copy Markdown

Open in StackBlitz

npm i https://pkg.pr.new/@cloudflare/computer@88

commit: 2d47263

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Devin Review found 4 potential issues.

View 1 additional finding in Devin Review.

Open in Devin Review

this.#container.start({ enableInternet, env });
} else if (!this.#container.running) {
this.#container.start({ enableInternet: true, env });
this.#container.start({ enableInternet, env });

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔴 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.
Open in Devin Review

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" },

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 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.
Open in Devin Review

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 });

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟨 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.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +155 to +164
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),
]);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟨 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.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

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

Labels

allow-pr Allow a PR to remain open.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant