Skip to content

debt: sandbox.network allowlist is declared but never enforced — agents have ambient network access #35

Description

@bricef

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

  • A non-empty sandbox.network declaration is no longer silently ignored: at agent-load time it emits a loud, documented WARN naming the declared hosts as not yet enforced (referencing this issue / ADR-0010).
  • It is not a hard error — a definition declaring sandbox.network still loads successfully (the fleet depends on this). A test asserts both: such a definition loads, and the warning is produced.
  • The no-op and its interim treatment are documented where operators declare sandbox.network (the agent-definition schema/reference and/or the exec.rs "Known gaps" note).
  • just ci is green.

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    fleet:candidateSuitable for the autonomous fleet: bounded, single-repo, CI-verifiable, no ADR/strategic calltech-debtA shortcut or mess that taxes ongoing work

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions