Summary
Agent definitions carry a sandbox.network allowlist, but it is not enforced anywhere. Agents have ambient network access — they can reach any host (via gh / git / curl under the exec tool) regardless of what their definition declares. This violates design principle 3 ("sandbox by construction, not by policy").
Re-grounded 2026-07-14, decision recorded. Filed 2026-07-08; the core claim still holds in full on main. Cited line numbers have drifted and the shell tool was renamed shell.rs → exec.rs (now argv-only) — the Location section below is re-grounded. The reviewer's interim scoping call is captured under Proposed remediation: this issue delivers a loud load-time warning (not a hard error); the network proxy (option B, #208) is the chosen enforcement path, with the ADR-0010 container end-state tracked as #209. Original text is in this issue's edit history.
Location (re-grounded to main, 2026-07-14)
- Parsed and stored:
Sandbox { network: Vec<String>, .. } — services/fq-runtime/crates/fq-runtime/src/agent.rs:552 (field); accessor Sandbox::network_patterns() — agent.rs:597.
- The gap:
network_patterns() is referenced only by tests (agent.rs:923, agent/definition.rs:618) — never by any enforcement path. Sandbox::to_tool_sandbox() (agent.rs:628) materialises the runtime ToolSandbox from fs_read / fs_write / exec_cwd / env only — network is dropped entirely. The ToolSandbox struct (services/fq-runtime/crates/fq-tools/src/sandbox.rs:41) has no network field and no check_network method — only check_read (:137), check_exec_cwd (:166), check_write (:203), plus an env allowlist. So the dimension never even reaches the tool layer.
- The exec tool documents the gap explicitly under "## Known gaps": "No network isolation. Commands can open network connections." —
services/fq-runtime/crates/fq-tools/src/builtin/exec.rs:62 (this is the former shell.rs, renamed to the argv-only exec.rs).
The cost (interest)
The declarative sandbox lies about a security-relevant capability: a definition can declare a network allowlist and get unrestricted access. As agents grow more capable, longer-lived, and externally triggered, unrestricted egress is an exfiltration / abuse surface. The fs_* / exec_cwd / env dimensions are enforced (ToolSandbox::check_read/check_write/check_exec_cwd), so network is the one declared dimension that is silently a no-op — a trap: operators may believe they have restricted an agent when they have not. This bites us directly today: every dogfood fleet agent declares sandbox.network: [github.com, api.github.com], and all of those declarations are currently decorative.
How it got here
Process-level network isolation is genuinely hard — the exec tool's own doc (exec.rs, "Known gaps") notes it can only be closed with OS-level isolation, and ADR-0010 (container + network proxy) is the decided mechanism. So the declaration shipped ahead of enforcement. Surfaced in practice while wiring the M0 loop's GitHub step (dogfood, 2026-07-06).
Proposed remediation — DECISION RECORDED (2026-07-14)
The reviewer's scoping call, given the current substrate (the exec tool is argv-only and process-level — no OS network namespace yet, so nothing short of containers can be airtight):
This issue delivers option A — fail honest (a loud WARNING, not a hard error). At agent-load / validation time, when sandbox.network is non-empty, emit a loud WARN naming the declared-but-unenforced hosts and pointing at this issue / ADR-0010, and document the no-op where operators declare sandbox.network. Small, local, honest: it closes the trap even though it does not close the gap.
Why a warning and not a hard error (the issue originally offered either): a hard error on a non-empty sandbox.network would break every dogfood fleet agent at load time — they all declare [github.com, api.github.com] — and we want to keep declaring intent ahead of enforcement. So the honest interim signal is a warning, not a rejection.
Chosen enforcement path (follow-up — NOT this issue, tracked by #208): option B, a CONNECT-filtering forward proxy. Stand up a small proxy that permits only the hosts in network_patterns(), and inject HTTPS_PROXY / HTTP_PROXY / NO_PROXY into the exec child's environment (reusing the env-injection seam the exec sandbox already has). This enforces the declared host allowlist for the fleet's real egress (gh / git / curl over HTTPS) without waiting on containers, and it is not throwaway: it is precisely the ADR-0010 network-proxy component, built ahead of the container boundary and reused there later. Honest limit: it is defense-in-depth — bypassable by an agent that ignores proxy env or opens a raw socket; a true boundary needs option C.
Rejected: option 2 (blocklist network-touching binaries). gh / git are both required and network-touching, so you cannot separate "gh → github.com (allow)" from "gh → evil.com (deny)" at the argv layer. Brittle, and buys almost nothing the proxy does not buy properly.
End-state: option C — full ADR-0010 (container + network namespace + the same proxy at the container boundary). Airtight, large, tracked by #209. Not this issue.
Blast radius / risk
Option A (this issue) is small and local — the agent load/validation path, a doc note, and a test; it changes no tool-isolation behaviour and is safe for the fleet (warning, not error). Options B/C are wider, load-bearing, and tracked separately.
Acceptance criteria
Scope
In scope: closing the silent-no-op trap for sandbox.network via option A — the load-time warning, its test, and the operator-facing doc note.
Out of scope: the actual enforcement — option B (network proxy, #208) and option C (ADR-0010 container build, #209), tracked by the "Network proxy" and "Container-level sandboxing (ADR-0010)" backlog items; and credential injection.
References
Summary
Agent definitions carry a
sandbox.networkallowlist, but it is not enforced anywhere. Agents have ambient network access — they can reach any host (viagh/git/curlunder the exec tool) regardless of what their definition declares. This violates design principle 3 ("sandbox by construction, not by policy").Location (re-grounded to
main, 2026-07-14)Sandbox { network: Vec<String>, .. }—services/fq-runtime/crates/fq-runtime/src/agent.rs:552(field); accessorSandbox::network_patterns()—agent.rs:597.network_patterns()is referenced only by tests (agent.rs:923,agent/definition.rs:618) — never by any enforcement path.Sandbox::to_tool_sandbox()(agent.rs:628) materialises the runtimeToolSandboxfromfs_read/fs_write/exec_cwd/envonly —networkis dropped entirely. TheToolSandboxstruct (services/fq-runtime/crates/fq-tools/src/sandbox.rs:41) has nonetworkfield and nocheck_networkmethod — onlycheck_read(:137),check_exec_cwd(:166),check_write(:203), plus an env allowlist. So the dimension never even reaches the tool layer.services/fq-runtime/crates/fq-tools/src/builtin/exec.rs:62(this is the formershell.rs, renamed to the argv-onlyexec.rs).The cost (interest)
The declarative sandbox lies about a security-relevant capability: a definition can declare a
networkallowlist and get unrestricted access. As agents grow more capable, longer-lived, and externally triggered, unrestricted egress is an exfiltration / abuse surface. Thefs_*/exec_cwd/envdimensions are enforced (ToolSandbox::check_read/check_write/check_exec_cwd), sonetworkis the one declared dimension that is silently a no-op — a trap: operators may believe they have restricted an agent when they have not. This bites us directly today: every dogfood fleet agent declaressandbox.network: [github.com, api.github.com], and all of those declarations are currently decorative.How it got here
Process-level network isolation is genuinely hard — the exec tool's own doc (
exec.rs, "Known gaps") notes it can only be closed with OS-level isolation, and ADR-0010 (container + network proxy) is the decided mechanism. So the declaration shipped ahead of enforcement. Surfaced in practice while wiring the M0 loop's GitHub step (dogfood, 2026-07-06).Proposed remediation — DECISION RECORDED (2026-07-14)
The reviewer's scoping call, given the current substrate (the exec tool is argv-only and process-level — no OS network namespace yet, so nothing short of containers can be airtight):
This issue delivers option A — fail honest (a loud WARNING, not a hard error). At agent-load / validation time, when
sandbox.networkis non-empty, emit a loudWARNnaming the declared-but-unenforced hosts and pointing at this issue / ADR-0010, and document the no-op where operators declaresandbox.network. Small, local, honest: it closes the trap even though it does not close the gap.Chosen enforcement path (follow-up — NOT this issue, tracked by #208): option B, a CONNECT-filtering forward proxy. Stand up a small proxy that permits only the hosts in
network_patterns(), and injectHTTPS_PROXY/HTTP_PROXY/NO_PROXYinto the exec child's environment (reusing the env-injection seam the exec sandbox already has). This enforces the declared host allowlist for the fleet's real egress (gh / git / curl over HTTPS) without waiting on containers, and it is not throwaway: it is precisely the ADR-0010 network-proxy component, built ahead of the container boundary and reused there later. Honest limit: it is defense-in-depth — bypassable by an agent that ignores proxy env or opens a raw socket; a true boundary needs option C.Rejected: option 2 (blocklist network-touching binaries).
gh/gitare both required and network-touching, so you cannot separate "gh → github.com (allow)" from "gh → evil.com (deny)" at the argv layer. Brittle, and buys almost nothing the proxy does not buy properly.End-state: option C — full ADR-0010 (container + network namespace + the same proxy at the container boundary). Airtight, large, tracked by #209. Not this issue.
Blast radius / risk
Option A (this issue) is small and local — the agent load/validation path, a doc note, and a test; it changes no tool-isolation behaviour and is safe for the fleet (warning, not error). Options B/C are wider, load-bearing, and tracked separately.
Acceptance criteria
sandbox.networkdeclaration is no longer silently ignored: at agent-load time it emits a loud, documentedWARNnaming the declared hosts as not yet enforced (referencing this issue / ADR-0010).sandbox.networkstill loads successfully (the fleet depends on this). A test asserts both: such a definition loads, and the warning is produced.sandbox.network(the agent-definition schema/reference and/or theexec.rs"Known gaps" note).just ciis green.Scope
In scope: closing the silent-no-op trap for
sandbox.networkvia option A — the load-time warning, its test, and the operator-facing doc note.Out of scope: the actual enforcement — option B (network proxy, #208) and option C (ADR-0010 container build, #209), tracked by the "Network proxy" and "Container-level sandboxing (ADR-0010)" backlog items; and credential injection.
References
docs/plans/backlog.md: "Agent network sandbox is declared but not enforced" (:944; interim guidance:957— "treat any agent as network-unrestricted regardless of what its definition declares"); "Network proxy" (:221; "Enforce aggregate per-agent network policy":234); "Container-level sandboxing (ADR-0010 — decided)" (:59; "Network proxy component":70).docs/design/committed/design-principles.md— Principle 3 ("sandbox by construction, not by policy").docs/adrs/accepted/0010-agent-execution-isolation.md— Status: Accepted; the decided mechanism (containers + a network proxy enforcingsandbox.networkat the container boundary) — the home of options B (feat: enforce sandbox.network via a CONNECT-filtering forward proxy (interim, pre-container) #208) and C (feat: implement ADR-0010 — containerised agent execution isolation (airtight sandbox enforcement) #209).