Problem
Once agent runs execute in fresh runner containers, we have a cleaner place to enforce network boundaries. Today a compromised prompt, dependency install, or tool invocation can generally reach whatever the runner container network can reach.
We want operators to be able to say things like:
- deny all internet access for a given agent,
- allow only GitHub and Anthropic for a reviewer agent,
- allow GitHub plus OpenAI for a Codex-backed agent,
- keep a permissive global default while locking down sensitive agents,
- log denied egress attempts for security review.
Prompt guardrails are useful, but this must be runtime-enforced. An agent prompt can be manipulated; the network policy should not depend on the model cooperating.
Product Direction
Implement egress policy for runner containers using a controlled egress proxy path, not prompt-only rules and not host-wide firewall mutation.
Policy should exist at two levels:
- Global default policy, applied to every run unless overridden.
- Per-agent policy, used to narrow or override the global default for that specific agent.
A per-workspace policy may be useful later, but it is not required for the first version unless implementation naturally needs it. Keep v1 focused.
Why Proxy-Based
Hostname allowlists are the useful product shape:
allow github.com, api.github.com, api.anthropic.com
block everything else
iptables is good for IP/CIDR/port enforcement, but it is brittle for hostnames because GitHub, Anthropic, OpenAI, package registries, and CDNs use changing IPs. /etc/hosts is not a security boundary: it does not block direct IP access, does not handle wildcard domains well, and can break TLS/CDN routing.
A proxy can enforce HTTP/HTTPS target hostnames directly through normal proxy semantics, especially HTTP CONNECT for HTTPS. It also lets us log denied hostnames in a user-understandable way.
Target Runtime Shape
For a restricted run:
daemon
-> create per-run Docker network
-> start egress proxy container with policy for this run
-> start runner container on that network
-> inject HTTP_PROXY / HTTPS_PROXY / NO_PROXY into runner
-> block or avoid direct internet access from runner
-> runner tools reach allowed services through proxy only
-> daemon removes runner, proxy, and network after run
The runner container must stay unprivileged. Do not give the runner CAP_NET_ADMIN or privileged mode so that agent-controlled code cannot remove or weaken its own network controls.
If low-level firewall rules are needed, they should be owned by daemon-controlled infrastructure or a trusted helper/proxy, not by the runner process itself.
Policy Model
Initial policy shape should be simple and explicit:
egress:
mode: allow_all | deny_all | allowlist
allow_hosts:
- github.com
- api.github.com
- api.anthropic.com
allow_ports:
- 443
Suggested semantics:
allow_all: current behavior, no egress proxy restriction required.
deny_all: runner has no internet egress.
allowlist: runner may only connect to listed hostnames/ports through the proxy.
Questions to settle during implementation:
- Whether host matching supports exact hostnames only in v1, or wildcard suffixes like
*.github.com.
- Whether
allow_ports should default to 443 only.
- Whether HTTP on port
80 should be disabled by default.
- How to represent built-in presets like
github, anthropic, openai without hiding the actual hostnames from users.
Global and Per-Agent Behavior
Global policy should be the default:
runtime:
egress:
mode: allowlist
allow_hosts:
- github.com
- api.github.com
- api.githubcopilot.com
- api.anthropic.com
- api.openai.com
allow_ports: [443]
Agent-level policy can override or narrow it:
agents:
- name: pr-reviewer
egress:
mode: allowlist
allow_hosts:
- github.com
- api.github.com
- api.githubcopilot.com
- api.anthropic.com
allow_ports: [443]
Implementation must define merge semantics clearly. Recommended v1:
- If agent policy is absent, use global policy.
- If agent policy is present, use agent policy as the complete effective policy.
- Do not implement complex merging in v1.
Required Product Surfaces
Egress policy must be configurable through the same surfaces as other runtime/fleet settings:
- UI:
- Config -> Runtime for global default egress policy.
- Agent editor for per-agent egress policy.
- Clear status badges for
allow_all, deny_all, and allowlist.
- REST:
- global runtime egress fields in
/runtime.
- per-agent egress fields in agent CRUD.
- MCP:
- expose global egress policy through runtime tools.
- expose per-agent egress policy through agent create/update tools.
- Import/export:
- include global and per-agent egress policy.
- Docs:
- quickstart/security/configuration/runtime docs must explain the policy model and limits.
Enforcement Requirements
The policy must be enforced by runtime, not just displayed.
For deny_all:
- Runner should have no direct external network access.
- It may still need loopback/local access for the CLI process itself.
- If no network is required, Docker
network_mode: none may be enough.
For allowlist:
- Runner traffic should be routed through the egress proxy.
- Proxy allows only configured hostname/port pairs.
- Proxy denies everything else and records a structured denial event/log.
- Runner should not have a direct route to the internet that bypasses the proxy.
The hardest part is bypass prevention. Setting HTTP_PROXY / HTTPS_PROXY is not enough if direct egress still works, because tools or malicious code can ignore proxy env vars. The design must either isolate direct egress or explicitly document the limitation if a first implementation cannot fully block bypasses.
Observability
Denied egress attempts should be visible enough to debug and audit:
- daemon logs should include denied hostname/port, agent, workspace, repo, run/span if available,
- trace/runners UI should eventually surface egress denial as a run diagnostic,
/backends/status or runtime diagnostics should verify proxy availability when policy requires it.
Do not log secret headers, bearer tokens, request bodies, or full URLs with sensitive query strings.
Security Constraints
- Runner containers must remain unprivileged.
- Do not grant agent-controlled containers
CAP_NET_ADMIN.
- Do not mutate host-wide iptables rules from the runner.
- If a helper requires Docker/socket privileges, keep that helper daemon-controlled and document the boundary.
- Avoid relying on
/etc/hosts as a security mechanism.
- Proxy logs must redact credentials.
- Denied egress must fail closed.
Implementation Notes
A reasonable implementation path:
- Add
EgressPolicy domain structs.
- Persist global policy as part of runtime settings.
- Persist optional per-agent policy.
- Extend REST/MCP/import/export/UI surfaces.
- Add an internal egress runtime abstraction so workflow code does not know proxy internals.
- Add or build an egress proxy image/binary.
- At run time, compute effective policy for the agent.
- For
allow_all, keep current bridge/network behavior.
- For
deny_all, start runner with no external network.
- For
allowlist, create per-run network, start proxy with policy, start runner with proxy env, prevent direct egress, clean everything up.
- Add fake-backed tests for policy resolution and runtime orchestration.
- Add at least one real Docker smoke test path behind an explicit env var/build tag if CI cannot run Docker.
The proxy can be a small Go HTTP proxy owned by this repo, or a carefully configured existing proxy if that keeps the implementation smaller. If we ship a proxy image, publish it alongside agents and agents-runner with matching tags.
Open Questions
- Should the proxy be a separate
agents-egress-proxy image or should the proxy binary be embedded in the daemon image and launched as a tiny container from the daemon image?
- Exact hostname matching only, or wildcard suffix matching in v1?
- Should package registry access be a built-in preset or always explicit?
- How should local-model deployments express local network access, e.g.
host.docker.internal, LAN CIDRs, or a local inference endpoint?
- Do we need per-workspace policy before per-agent policy, or can global + agent cover v1?
- How do we prevent bypasses robustly without overcomplicating Docker networking?
Acceptance Criteria
- Global egress policy is persisted and configurable through UI, REST, MCP, and import/export.
- Per-agent egress policy is persisted and configurable through UI, REST, MCP, and import/export.
- Effective policy is computed deterministically for every run.
allow_all preserves current behavior.
deny_all blocks internet egress for runner containers.
allowlist permits only configured hostnames/ports through a proxy path.
- Runner containers cannot trivially bypass the proxy through direct internet access.
- Denied egress attempts are logged with agent/workspace/repo/run context where available, without leaking secrets.
- Docs explain the model, limitations, Docker/network requirements, and examples for GitHub + Anthropic/OpenAI.
- Tests cover policy validation, effective-policy resolution, REST/MCP wire shapes, and runtime orchestration.
- A real Docker smoke test path exists for restricted egress behavior, even if it is opt-in for local/CI environments.
Relationship to Runner Split
This should build on the daemon/runner split from #511. It should not be included in the current runner split PR unless explicitly requested. The runner split gives us the per-run container boundary; this issue adds a controlled network boundary on top.
Problem
Once agent runs execute in fresh runner containers, we have a cleaner place to enforce network boundaries. Today a compromised prompt, dependency install, or tool invocation can generally reach whatever the runner container network can reach.
We want operators to be able to say things like:
Prompt guardrails are useful, but this must be runtime-enforced. An agent prompt can be manipulated; the network policy should not depend on the model cooperating.
Product Direction
Implement egress policy for runner containers using a controlled egress proxy path, not prompt-only rules and not host-wide firewall mutation.
Policy should exist at two levels:
A per-workspace policy may be useful later, but it is not required for the first version unless implementation naturally needs it. Keep v1 focused.
Why Proxy-Based
Hostname allowlists are the useful product shape:
iptablesis good for IP/CIDR/port enforcement, but it is brittle for hostnames because GitHub, Anthropic, OpenAI, package registries, and CDNs use changing IPs./etc/hostsis not a security boundary: it does not block direct IP access, does not handle wildcard domains well, and can break TLS/CDN routing.A proxy can enforce HTTP/HTTPS target hostnames directly through normal proxy semantics, especially HTTP CONNECT for HTTPS. It also lets us log denied hostnames in a user-understandable way.
Target Runtime Shape
For a restricted run:
The runner container must stay unprivileged. Do not give the runner
CAP_NET_ADMINor privileged mode so that agent-controlled code cannot remove or weaken its own network controls.If low-level firewall rules are needed, they should be owned by daemon-controlled infrastructure or a trusted helper/proxy, not by the runner process itself.
Policy Model
Initial policy shape should be simple and explicit:
Suggested semantics:
allow_all: current behavior, no egress proxy restriction required.deny_all: runner has no internet egress.allowlist: runner may only connect to listed hostnames/ports through the proxy.Questions to settle during implementation:
*.github.com.allow_portsshould default to443only.80should be disabled by default.github,anthropic,openaiwithout hiding the actual hostnames from users.Global and Per-Agent Behavior
Global policy should be the default:
Agent-level policy can override or narrow it:
Implementation must define merge semantics clearly. Recommended v1:
Required Product Surfaces
Egress policy must be configurable through the same surfaces as other runtime/fleet settings:
allow_all,deny_all, andallowlist./runtime.Enforcement Requirements
The policy must be enforced by runtime, not just displayed.
For
deny_all:network_mode: nonemay be enough.For
allowlist:The hardest part is bypass prevention. Setting
HTTP_PROXY/HTTPS_PROXYis not enough if direct egress still works, because tools or malicious code can ignore proxy env vars. The design must either isolate direct egress or explicitly document the limitation if a first implementation cannot fully block bypasses.Observability
Denied egress attempts should be visible enough to debug and audit:
/backends/statusor runtime diagnostics should verify proxy availability when policy requires it.Do not log secret headers, bearer tokens, request bodies, or full URLs with sensitive query strings.
Security Constraints
CAP_NET_ADMIN./etc/hostsas a security mechanism.Implementation Notes
A reasonable implementation path:
EgressPolicydomain structs.allow_all, keep current bridge/network behavior.deny_all, start runner with no external network.allowlist, create per-run network, start proxy with policy, start runner with proxy env, prevent direct egress, clean everything up.The proxy can be a small Go HTTP proxy owned by this repo, or a carefully configured existing proxy if that keeps the implementation smaller. If we ship a proxy image, publish it alongside
agentsandagents-runnerwith matching tags.Open Questions
agents-egress-proxyimage or should the proxy binary be embedded in the daemon image and launched as a tiny container from the daemon image?host.docker.internal, LAN CIDRs, or a local inference endpoint?Acceptance Criteria
allow_allpreserves current behavior.deny_allblocks internet egress for runner containers.allowlistpermits only configured hostnames/ports through a proxy path.Relationship to Runner Split
This should build on the daemon/runner split from #511. It should not be included in the current runner split PR unless explicitly requested. The runner split gives us the per-run container boundary; this issue adds a controlled network boundary on top.