feat(sandbox): per-environment network egress policy#1331
Merged
Conversation
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.
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds a per-environment network egress policy to sandbox environments. A
SandboxEnvironmentcan now carry anegress_policy(default/intercept + per-host rules) and encryptedegress_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.mdviadocs/superpowers/plans/2026-06-23-sandbox-egress-integration.md.What's in here (6 commits)
feat(sandbox): add egress proxy wire schemasEgressRule/Secret/Policy/ConfigRequest/ConfigResponseincore/sandbox/schemas.py, mirroring daiv-sandbox; registered in the schema-drift test; dump regenerated (egress keys only).feat(sandbox): client.configure_egress …DAIVSandboxClient.configure_egressPOSTs the policy tosession/{id}/egress/, sending secret values as plaintext viaget_secret_value()(nevermodel_dump, which would maskSecretStr).feat(sandbox-envs): store per-env egress policy + encrypted secretsegress_policyJSONField +egress_secrets(encrypted-at-rest descriptor) +has_egress; clean-time_validate_egressthat validates through the Pydantic wire schema; migration0005(additive, nullable).feat(sandbox-envs): carry typed egress config through the runtime mergeegressonSandboxEnvOverride/SandboxRuntime;row_to_overridedrops a malformed config (never reaches the sidecar);merge_sandbox_runtimeselects egress wholesale (per-run > global).feat(sandbox): provision egress policy at session start, fail-closedSandboxEgressUnavailableError+_provision_egresswired intoSandboxMiddleware.abefore_agenton 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 caveatSecurity properties
env_vars) and redacted in repr/logs (SecretStr); plaintext appears only on the wire to the sidecar.Compatibility
0005is additive + nullable, no data migration. Existing environments getegress_policy = None→has_egressFalse → 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
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)
feat/egress-proxycarries unrelated in-flight schema drift (removal ofApplyMutations*/PutMutation/MutationResult; anexcludefield onFsGrepRequest/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.py→ 388 passed (+23 new tests).make lintclean;makemigrations --check --dry-run→ "No changes detected".🤖 Generated with Claude Code