Skip to content

feat(sandbox): per-environment network egress policy#1331

Merged
srtab merged 10 commits into
mainfrom
worktree-sandbox-egress
Jun 30, 2026
Merged

feat(sandbox): per-environment network egress policy#1331
srtab merged 10 commits into
mainfrom
worktree-sandbox-egress

Conversation

@srtab

@srtab srtab commented Jun 23, 2026

Copy link
Copy Markdown
Owner

Summary

Adds a per-environment network egress policy to sandbox environments. A SandboxEnvironment can now carry an egress_policy (default/intercept + per-host rules) and encrypted egress_secrets (named header credentials). When set and network is enabled, the agent provisions them onto the daiv-sandbox MITM egress proxy at session start — so credentials are injected by the sidecar and never enter the sandbox container — fail-closed: if the sandbox does not have the egress proxy enabled, a run using an egress-configured environment aborts rather than falling back to unrestricted network.

Implements docs/superpowers/specs/2026-06-23-sandbox-egress-integration-design.md via docs/superpowers/plans/2026-06-23-sandbox-egress-integration.md.

What's in here (6 commits)

Commit Change
feat(sandbox): add egress proxy wire schemas EgressRule/Secret/Policy/ConfigRequest/ConfigResponse in core/sandbox/schemas.py, mirroring daiv-sandbox; registered in the schema-drift test; dump regenerated (egress keys only).
feat(sandbox): client.configure_egress … DAIVSandboxClient.configure_egress POSTs the policy to session/{id}/egress/, sending secret values as plaintext via get_secret_value() (never model_dump, which would mask SecretStr).
feat(sandbox-envs): store per-env egress policy + encrypted secrets egress_policy JSONField + egress_secrets (encrypted-at-rest descriptor) + has_egress; clean-time _validate_egress that validates through the Pydantic wire schema; migration 0005 (additive, nullable).
feat(sandbox-envs): carry typed egress config through the runtime merge egress on SandboxEnvOverride/SandboxRuntime; row_to_override drops a malformed config (never reaches the sidecar); merge_sandbox_runtime selects egress wholesale (per-run > global).
feat(sandbox): provision egress policy at session start, fail-closed SandboxEgressUnavailableError + _provision_egress wired into SandboxMiddleware.abefore_agent on both fresh-create and warm-reuse paths; 404/409 → abort; other HTTP errors propagate unchanged.
docs(sandbox-envs): document per-env egress policy and deny-all caveat CHANGELOG + operator comment (deny-all caveat once the proxy is enabled).

Security properties

  • Secrets are encrypted at rest (same descriptor as env_vars) and redacted in repr/logs (SecretStr); plaintext appears only on the wire to the sidecar.
  • Fail-closed: a 404 ("proxy not enabled") / 409 ("session has no proxy") aborts the run — no raw-network fallback. Fresh-create failures force-close the session (not the shared transport).
  • The wire schemas are kept structurally identical to daiv-sandbox's via the existing drift test.

Compatibility

  • Migration 0005 is additive + nullable, no data migration. Existing environments get egress_policy = Nonehas_egress False → no provisioning, so behavior is unchanged until the sandbox egress proxy is enabled. Configurable via Django admin / API for now (form UI to follow).

Cross-repo dependency

  • Depends on daiv-sandbox egress support (feat/egress-proxy). A companion commit there (build(egress): include egress schemas in the wire-schema dump) makes the schema-dump script emit the egress types so daiv's drift test can cover them.

Follow-up (not in this PR)

  • daiv-sandbox feat/egress-proxy carries unrelated in-flight schema drift (removal of ApplyMutations*/PutMutation/MutationResult; an exclude field on FsGrepRequest/FsGlobRequest). This PR's dump regeneration was deliberately scoped to the 5 egress keys only. A separate task should reconcile the rest of the daiv↔daiv-sandbox schemas once that branch lands.

Testing

  • uv run pytest -s tests/unit_tests/core/sandbox/ tests/unit_tests/sandbox_envs/ tests/unit_tests/automation/agent/middlewares/test_sandbox.py388 passed (+23 new tests).
  • make lint clean; makemigrations --check --dry-run → "No changes detected".

🤖 Generated with Claude Code

srtab added 7 commits June 23, 2026 16:54
Only HTTP 404 (egress proxy disabled — permanent) converts to
SandboxEgressUnavailableError. 409 (ambiguous "session busy" vs "no
proxy", already classified as transient) and 5xx now propagate
unchanged instead of being mislabeled as a permanent "enable the
proxy" diagnosis; they still fail closed via the existing cleanup.

Re-provision egress on warm session reuse so the guarantee holds on
resumed turns too, without force-closing the resumable container on a
possibly transient blip.

When stored egress config is unusable (rotated DAIV_ENCRYPTION_KEY,
hand-edited row), fail closed to an empty deny-all policy rather than
dropping to None — None would let a network-enabled session fall back
to raw network or the sidecar default.

Extract EgressConfigRequest.from_stored() as the single source of
truth for mapping persisted policy/secrets onto the wire schema, and
simplify egress precedence in merge_sandbox_runtime via the generic
pick() helper.
srtab and others added 3 commits June 25, 2026 15:17
The earlier egress commits described the sandbox proxy as gated by a
DAIV_SANDBOX_EGRESS_PROXY_ENABLED boolean flag, but daiv-sandbox enables
egress by configuring a shared MITM CA (EGRESS_CA_CERT_FILE +
EGRESS_CA_KEY_FILE) instead. A network-enabled session is built as a
triad and reaches the internet only through the per-session proxy; if no
CA is configured the run is rejected up front rather than dropping to
raw network.

Correct the contract everywhere it was described: the actionable
SandboxEgressUnavailableError message, the provisioning docstrings, the
SandboxEnvironment/services comments, and the test rationale comments.
Clarify that the 404 fail-closed path is effectively unreachable on a
fresh create (rejected at start_session) but still matters for warm
reuse after CA rotation.

Wire the local docker setup to the real mechanism: mount ./data/certs
read-only and set DAIV_SANDBOX_EGRESS_CA_{CERT,KEY}_FILE, replacing the
raw-network DAIV_SANDBOX_EXTRA_HOSTS sibling-resolution approach with
egress-only routing through the proxy.
Address review findings on the per-environment egress work:

- Resolve the git-platform egress credential AFTER the repo clone in
  set_runtime_ctx, so it observes any token the GitLab clone self-heal
  re-minted. The proxy overrides Authorization on every platform request,
  so a pre-clone credential would pin the sidecar to the stale token the
  clone just discarded and break the in-sandbox push.
- Harden the egress wire schema: reject CR/LF and blank host/header on
  EgressRule/EgressSecret via a shared _no_crlf helper, closing the
  header-injection gap on the from_stored/apply_platform_egress paths
  that bypass the form.
- Correct the fail-closed abort message to name the prefixed
  DAIV_SANDBOX_EGRESS_CA_CERT_FILE / _KEY_FILE env vars.
- Log unusable egress config via logger.exception so the shape-error
  cause (which nothing else logs) is captured, with the env name.
- Document the upgrade behaviour (existing network-enabled envs become
  network-isolated, no auto-conversion) and fix two doc inaccuracies
  (empty allow-list under Network On is a validation error; saved
  credentials show a "keep existing" placeholder, not bullets).
- Add regression/branch tests: clone-before-credential ordering,
  _validate_egress non-dict/size-cap branches, is_git_auth_error_text
  contract, env_vars decryption-swallow, and the new CR/LF validators.
@srtab srtab merged commit 8dfe928 into main Jun 30, 2026
8 checks passed
@srtab srtab deleted the worktree-sandbox-egress branch June 30, 2026 14:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant