Skip to content

feat(sandbox): Enforce network_mode allowlist on docker#785

Open
Yiminnn wants to merge 25 commits into
mainfrom
feat/network-mode-enforcement
Open

feat(sandbox): Enforce network_mode allowlist on docker#785
Yiminnn wants to merge 25 commits into
mainfrom
feat/network-mode-enforcement

Conversation

@Yiminnn

@Yiminnn Yiminnn commented Jun 15, 2026

Copy link
Copy Markdown
Collaborator

Problem

network_mode is the documented outbound-network control, but the launch path historically keyed off the deprecated allow_internet boolean. That made allowlist validated but unenforceable: a task could declare allowed_hosts, yet the sandbox would either run open or reject too late.

Current fix

This PR makes network_mode authoritative and fail-closed across the runtime path.

  • Docker allowlist enforcement: the main container is moved onto an internal no-egress network after agent install, and HTTP(S) traffic goes through a stdlib egress proxy sidecar that forwards only to allowed_hosts plus the model lane when allowed.
  • Daytona allowlist enforcement: exact-host allowlists are mapped to Daytona's IPv4 CIDR control when faithfully expressible; hosts are pinned in /etc/hosts; wildcard, unresolvable, over-limit, and DinD cases fail closed.
  • Hermetic mode: allow_model_endpoint: false now prevents the provider/model host from being appended to Docker/Daytona allowlists.
  • Snapshot restore guard: Docker snapshot restore now fails closed under restrictive network policies instead of reconnecting restored containers to the public compose bridge.
  • Maintainability cleanup: backend orchestration moved out of docker.py / daytona.py into docker_network_lockdown.py and daytona_network_lockdown.py; the backend files are back under the 1k-line review threshold.

Latest validation

Local validation on head 2f087e2d7:

  • uv run pytest tests/test_network_runtime.py tests/test_network_policy.py tests/test_runtime_capabilities.py tests/test_network_modes.py tests/test_connect_as_env.py -q -> 95 passed
  • uv run ruff check on touched network/runtime files -> passed
  • uv run ruff format --check on touched Python files -> passed
  • uv run ty check src/benchflow/sandbox/docker_network_lockdown.py src/benchflow/sandbox/daytona_network_lockdown.py src/benchflow/sandbox/docker.py src/benchflow/providers/litellm_runtime.py src/benchflow/task/runtime_capabilities.py -> passed
  • git diff --check -> passed

GitHub after the push: test, pip-audit, manifest-parity, and detect-scope are green; rollout-smoke is still pending.

Remaining gate

Keep this blocked until rollout-smoke is green and the security-sensitive decomposition is re-reviewed.

network_mode is now the authoritative network control across the launch
path; the deprecated allow_internet boolean is a derived shim. Previously
every backend keyed off allow_internet, so allowed_hosts never reached the
sandbox and network_mode: allowlist was validated but unenforceable -
rejected at preflight on every backend.

- allowlist on the docker sandbox is now ENFORCED: the agent container
  joins an internal (no-egress) network and its HTTP(S) traffic routes
  through a stdlib egress-proxy sidecar that forwards only to allowed_hosts.
  Other hosts, raw-IP connections, and proxy-ignoring tools have no route
  off-box (default deny). New: sandbox/_egress.py, _egress_proxy.py.
- sandbox/network_policy.py resolves the effective policy (open/block-all/
  allowlist) and gates allowlist to backends that can enforce it; daytona/
  modal fail closed (never silently open).
- runtime_capabilities: allowlist supported on docker, rejected at preflight
  elsewhere with a clear message.
- setup.py: preserve_agent_network lifts ANY restrictive policy (no-network
  OR allowlist) to public for web-disabled agent runs (they need the model
  API; no-web is enforced at the agent layer).
- docs/task-authoring-task-md.md network-policy section + CHANGELOG.

Verified on real docker (egress e2e): allowed host -> 200, non-allowed ->
blocked, direct socket with proxy stripped -> no route. Full suite green.
@Yiminnn Yiminnn temporarily deployed to pypi-internal-preview June 15, 2026 17:42 — with GitHub Actions Inactive
@bingran-you

Copy link
Copy Markdown
Collaborator

Review (automated thorough pass) — ✅ no blocking defects

I ran an adversarial correctness/security review of the allowlist enforcement plus the targeted test + lint suite on a fresh checkout of the PR head.

The enforcement is genuinely sound. Key claims verified:

  • Default-deny is real. The main service joins only bf_egress_internal (internal: true, no off-box route); the compose sequence-replace on services.main.networks correctly drops default. Tools that ignore HTTP(S)_PROXY and raw sockets simply have no route off-box — the proxy is the sole bridge.
  • Host matching is dot-anchored (any(host == a or host.endswith("." + a))): evil-example.com does not match example.com, example.com.evil.com is denied, case/trailing-dot normalized. Covered by tests.
  • Raw-IP / IPv6 / IP-literal CONNECT are denied (config rejects non-hostname allowed_hosts; raw IPs never match). DNS-rebind / SNI-fronting are inherent hostname-allowlist limitations, not bypasses.
  • Fail-closed off-docker: allowlist on daytona/modal is rejected at preflight, and resolve_network_decision fails closed as backstop.
  • allow_internet back-compat shim preserves legacy no-network behavior.
  • preserve_agent_network lift does NOT weaken allowlist — it only fires when web tools are disabled (which for an allowlist task requires self_gen_no_internet=True), mutates a deep copy, and runs after preflight.
  • No injection (allowed_hosts validated to [a-z0-9-.], JSON env, no shell); cleanup tears down sidecar + both networks via compose down with _force_kill_project backstop.

Tests/lint: tests/test_network_policy.py + tests/test_runtime_capabilities.py41 passed; ruff check clean; ty check src/benchflow/sandbox/ clean.

Non-blocking nits (follow-ups, not merge blockers)

  1. _egress.py docstring overstates coverage — says "docker and daytona-dind," but ALLOWLIST_CAPABLE_SANDBOXES = {"docker"} and daytona_dind.py still keys off legacy allow_internet; it doesn't call resolve_network_decision/build_egress_override. Recommend wording it docker-only (matches the PR's own "Scope/follow-ups").
  2. In-backend fail-closed is docker-only. modal_impl.py, daytona.py, daytona_dind.py still read allow_internet directly; the real gate is preflight (which runs first, so it's safe today). Worth routing non-docker backends through resolve_network_decision as defense-in-depth.
  3. Minor: _network_policy_compose_paths rebuilds the override on every compose invocation (incl. down) — consider memoizing. Proxy sidecar binds 0.0.0.0:8080 on both nets — could restrict to the internal interface.

Verdict: MERGE-READY (with the above as follow-ups). Leaving the actual merge to @Yiminnn since the branch was updated very recently and may still be iterating.

@mintlify

mintlify Bot commented Jun 15, 2026

Copy link
Copy Markdown

Preview deployment for your docs. Learn more about Mintlify Previews.

Project Status Preview Updated (UTC)
benchflow-bff148e7 🟢 Ready View Preview Jun 15, 2026, 6:23 PM

💡 Tip: Enable Workflows to automatically generate PRs for you.

Wildcard allowed_hosts: a single leading `*.` label (e.g. *.example.com)
matches subdomains at any depth (Harbor/nginx semantics) but not the apex;
mid/trailing wildcards are rejected at parse time. Validator in task/config.py,
matcher in sandbox/_egress_proxy.py.

Model lane: a restrictive network_mode (no-network or allowlist) on docker now
keeps one always-allow lane to the host-side benchflow model proxy open, so an
agent run reaches the model without opening the sandbox to the public internet
(no-network becomes model-only egress). The egress sidecar permits the docker
host (_docker_host_address()) and routes to it via the bridge gateway /
host-gateway. Replaces the blanket lift-to-public for web-disabled docker runs
(extracted to _lift_agent_network_to_public, now docker-excluded); other
sandboxes keep the lift. allow_model_endpoint:false (default true) closes the
lane for a hermetic, no-model run.

Live-verified through the real egress proxy: lane host + allowed host reachable,
non-allowed host blocked (403); wildcard subdomain reachable, apex blocked.

ENG-219
@greptile-apps

greptile-apps Bot commented Jun 15, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR makes network_mode the authoritative network control across the docker sandbox launch path, retiring allow_internet to a back-compat shim, and actually enforces the allowlist policy on docker by wiring an internal-network egress-proxy sidecar (bf-egress) that allows only declared allowed_hosts while blocking everything else by default-deny.

  • New egress enforcement (_egress.py, _egress_proxy.py): an allowlist task's main container is placed on an internal: true Docker network and all HTTP(S) traffic is routed through a stdlib proxy sidecar; relock_network() performs post-install network surgery with docker network connect/disconnect and verifies the swap with lockdown_complete().
  • Policy resolution (network_policy.py): introduces resolve_network_decision, a lockdown_complete guard, and Daytona-specific IPv4-CIDR mapping; allowlist on modal/unsupported backends now fails closed at preflight rather than running open.
  • restore() bypass (docker.py): the snapshot-restore path hardcodes --network {project}_default, bypassing the egress proxy — this pre-existing gap is noted in earlier review threads and is not addressed in this PR.

Confidence Score: 4/5

The core egress enforcement logic is sound and the lockdown verification loop prevents silent policy failures, but restore() in docker.py still reconnects restored containers to the unrestricted public bridge — an outstanding gap flagged in earlier review threads that this PR does not address.

The new egress proxy, policy resolution, and lockdown_complete guard are all well-designed and tested. The two new inline comments are minor (IPv6 port parsing, custom-network error clarity) and do not affect the main enforcement path. The unresolved restore() bypass remains present and is the primary reason confidence falls short of the maximum.

src/benchflow/sandbox/docker.py — specifically the restore() method which bypasses egress network enforcement after a snapshot restore.

Important Files Changed

Filename Overview
src/benchflow/sandbox/_egress.py New file: generates the compose override that confines main to an internal network and routes traffic through the bf-egress proxy sidecar; depends_on uses list form (no health-gate, flagged in earlier thread).
src/benchflow/sandbox/_egress_proxy.py New stdlib egress proxy: handles CONNECT tunnelling and plain HTTP with absolute-URI rewrite (_to_origin_form); upstream socket now cleaned up in finally block (fixed from earlier review); IPv6 authority with explicit port parses to wrong port 80.
src/benchflow/sandbox/docker.py Key changes: relock_network() with lockdown_complete() verification; restore() still hardcodes --network {project}_default, bypassing egress enforcement after a snapshot restore (pre-existing unfixed gap from earlier review).
src/benchflow/sandbox/network_policy.py New module: resolve_network_decision, lockdown_complete, plan_daytona_allowlist — clean policy resolution with fail-closed semantics for unsupported sandboxes.
src/benchflow/sandbox/setup.py _lift_agent_network_to_public now correctly excludes docker/daytona sandboxes from the blanket lift; no new issues.
src/benchflow/task/runtime_capabilities.py _append_network_issue now correctly gates allowlist at preflight for unsupported sandboxes; wildcard preflight rejection for daytona's IPv4-CIDR mode added.
tests/test_network_policy.py Good coverage: resolution logic, override shape, proxy host-matching, capability gate; lockdown_complete not directly unit-tested for the task-custom-network edge case.
src/benchflow/sandbox/daytona.py Uses new network_policy helpers; IPv4-CIDR allowlist integration; plan_daytona_allowlist resolve-at-lockdown path is sound but requires DNS resolution at lockdown time.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant rollout as Rollout
    participant docker as DockerSandbox
    participant compose as docker compose
    participant main as main container
    participant sidecar as bf-egress sidecar
    participant internet as Internet

    rollout->>docker: "start() [_network_locked=False]"
    docker->>compose: up --wait (no egress override)
    compose->>main: start (on project_default, full internet)
    rollout->>docker: relock_network(extra_allowed_hosts)
    docker->>docker: "_network_locked = True"
    docker->>docker: build_egress_override() write JSON
    docker->>compose: up --no-deps bf-egress (egress override now in paths)
    compose->>sidecar: start (on bf_egress_internal + bf_egress_external)
    docker->>main: docker network connect bf_egress_internal
    docker->>main: docker network disconnect project_default
    docker->>main: docker inspect verify attached networks
    main->>docker: lockdown_complete() check
    alt lockdown verified
        docker->>rollout: return HTTP_PROXY env
    else lockdown failed
        docker->>rollout: raise SandboxStartupError
    end
    rollout->>main: exec agent with proxy env
    main->>sidecar: HTTP(S) via proxy bf-egress:8080
    sidecar->>sidecar: _host_allowed(hostname)?
    alt allowed host
        sidecar->>internet: forward origin-form rewrite for HTTP
    else denied
        sidecar->>main: 403 Forbidden
    end
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
    participant rollout as Rollout
    participant docker as DockerSandbox
    participant compose as docker compose
    participant main as main container
    participant sidecar as bf-egress sidecar
    participant internet as Internet

    rollout->>docker: "start() [_network_locked=False]"
    docker->>compose: up --wait (no egress override)
    compose->>main: start (on project_default, full internet)
    rollout->>docker: relock_network(extra_allowed_hosts)
    docker->>docker: "_network_locked = True"
    docker->>docker: build_egress_override() write JSON
    docker->>compose: up --no-deps bf-egress (egress override now in paths)
    compose->>sidecar: start (on bf_egress_internal + bf_egress_external)
    docker->>main: docker network connect bf_egress_internal
    docker->>main: docker network disconnect project_default
    docker->>main: docker inspect verify attached networks
    main->>docker: lockdown_complete() check
    alt lockdown verified
        docker->>rollout: return HTTP_PROXY env
    else lockdown failed
        docker->>rollout: raise SandboxStartupError
    end
    rollout->>main: exec agent with proxy env
    main->>sidecar: HTTP(S) via proxy bf-egress:8080
    sidecar->>sidecar: _host_allowed(hostname)?
    alt allowed host
        sidecar->>internet: forward origin-form rewrite for HTTP
    else denied
        sidecar->>main: 403 Forbidden
    end
Loading

Reviews (14): Last reviewed commit: "fix(network): address multi-agent audit ..." | Re-trigger Greptile

Comment thread src/benchflow/sandbox/_egress_proxy.py Outdated
Comment on lines +119 to +139
# Plain HTTP: target is an absolute URI (http://host/path) in proxy form.
host = ""
if "://" in target:
host = target.split("://", 1)[1].split("/", 1)[0]
if not host:
for hl in header.split(b"\r\n"):
if hl.lower().startswith(b"host:"):
host = hl.split(b":", 1)[1].decode("latin-1").strip()
break
hostname = host.rsplit(":", 1)[0].strip("[]")
port = int(host.rsplit(":", 1)[1]) if ":" in host and "]" not in host else 80
if not _host_allowed(hostname):
_deny(client, hostname)
return
try:
upstream = socket.create_connection((hostname, port), timeout=30)
except OSError:
client.sendall(b"HTTP/1.1 502 Bad Gateway\r\nConnection: close\r\n\r\n")
return
upstream.sendall(header)
_pipe(client, upstream)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Plain-HTTP request forwarded in absolute-URI form

For plain-HTTP requests the proxy forwards the raw header bytes — including the proxy request-line (GET http://host/path HTTP/1.1) — directly to the upstream server. RFC 7230 §5.3.2 says servers MUST accept the absolute-form, but a non-negligible number of back-ends (nginx with default config, some Python WSGI servers, and many embedded HTTP stacks) return 400 Bad Request for requests that arrive in absolute-URI form, which would cause tasks to see inexplicable failures for allowlisted plain-HTTP hosts.

The fix is to rewrite the request-line before forwarding: extract the path component from target (already split at line 122), reconstruct METHOD /path HTTP/1.1\r\n, and replace the first line of header before calling upstream.sendall.

Comment thread src/benchflow/sandbox/_egress.py Outdated
"NO_PROXY": "localhost,127.0.0.1",
"no_proxy": "localhost,127.0.0.1",
},
"depends_on": [_EGRESS_SERVICE],

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 depends_on list form doesn't wait for proxy readiness

Using a plain list for depends_on tells Compose to start main as soon as bf-egress has been created, not once it is actually listening on port 8080. In practice the proxy binds almost instantly, but on a slow or loaded host the main container can fire its first proxied request (e.g. during agent framework import) before srv.accept() is running, resulting in Connection refused with no retry path. Switching to the long-form with condition: service_started is equivalent but explicitly documented; condition: service_healthy with a TCP healthcheck would eliminate the race entirely.

Suggested change
"depends_on": [_EGRESS_SERVICE],
"depends_on": {
_EGRESS_SERVICE: {"condition": "service_started"},
},

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 0c78eadd35647bc47fb7bb4536d64d8555696589.

The egress override now gives bf-egress a TCP healthcheck and makes main depend on service_healthy. relock_docker_network() also fails closed unless the sidecar container reports healthy before returning HTTP(S)_PROXY to the agent, so the proxy env is no longer exposed after mere service creation.

Validation on the pushed head:

  • uv run pytest tests/test_network_policy.py tests/test_network_modes.py tests/test_network_runtime.py -q -> 62 passed
  • uv run ruff check src/benchflow/sandbox/_egress.py src/benchflow/sandbox/docker_network_lockdown.py tests/test_network_policy.py tests/test_network_runtime.py
  • uv run ruff format --check src/benchflow/sandbox/_egress.py src/benchflow/sandbox/docker_network_lockdown.py tests/test_network_policy.py tests/test_network_runtime.py
  • uv run ty check src/benchflow/sandbox/_egress.py src/benchflow/sandbox/docker_network_lockdown.py
  • GitHub CI is green on 0c78eadd3, including test, pip-audit, manifest-parity, and integration-light / rollout-smoke.
  • The integration-light-jobs artifact validates healthy with benchflow-experiment-review: ACP trajectory, LLM trajectory, results.jsonl, reward present, 334350 tokens, 18 tool calls, and training_ready=1.

Moving the PR back to status:ready; keeping review:changes-requested because the security-sensitive PR still needs human re-review to clear the existing review decision.

Comment thread src/benchflow/sandbox/_egress_proxy.py Outdated
Comment on lines +133 to +140
try:
upstream = socket.create_connection((hostname, port), timeout=30)
except OSError:
client.sendall(b"HTTP/1.1 502 Bad Gateway\r\nConnection: close\r\n\r\n")
return
upstream.sendall(header)
_pipe(client, upstream)
upstream.close()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 upstream socket not closed on exception in the plain-HTTP path

upstream is created at line 134, but if upstream.sendall(header) (line 138) raises OSError the outer except Exception: pass silently swallows the error and upstream.close() (line 140) is never called. In CPython this is reclaimed by reference-counting when _handle returns, but in PyPy/non-reference-counted runtimes the descriptor leaks until GC. Wrapping the post-create_connection block in a try/finally (or using upstream as a context manager) would make cleanup explicit and deterministic.

New docs/network-mode-prior-art.md records the network-mode design model
(no-network/public/allowlist + wildcard + model lane), a mode/mechanism
taxonomy, and credits to the prior-art platforms the design draws on
(Harbor, Modal, AISI Inspect, WAREX, SWE-bench, SWE-bench-Live, WebArena,
Cybench, tau2-bench, browser-use) with primary-source links. Registered in
the Mintlify nav and cross-linked from the task-authoring guide and
sandbox-hardening. Citations verified against primary sources; corrects the
Harbor PR#1854 framing (wildcard-depth clarification, not the feature origin)
and notes the "N0/N1/N2" shorthand is ours, not AISI nomenclature.

ENG-219
@Yiminnn

Yiminnn commented Jun 16, 2026

Copy link
Copy Markdown
Collaborator Author

Network-mode verification — citation-check across network strategies × {docker, daytona}

Built six citation-check task variants that differ only in their environment network block, and ran them through benchflow's real launch path on both sandboxes. citation-check is a good probe because its skills call external APIs (CrossRef, NCBI/PubMed, DOI, arXiv), so enforcement is observable end-to-end.

1 · Deterministic policy + preflight matrix (authoritative, no model spend)

Each variant resolved through the real validate_task_runtime_support + resolve_network_decision:

variant   sandbox  preflight          policy     lane   allowed_hosts
public    docker   ok                 OPEN       False  -
public    daytona  ok                 OPEN       False  -
allow     docker   ok                 ALLOWLIST  True   api.crossref.org,eutils.ncbi.nlm.nih.gov,doi.org,export.arxiv.org
allow     daytona  REJECT(network)    BLOCK_ALL  True   …  <=allowlist (fail-closed)
wildcard  docker   ok                 ALLOWLIST  True   *.crossref.org,*.ncbi.nlm.nih.gov,doi.org,*.arxiv.org
wildcard  daytona  REJECT(network)    BLOCK_ALL  True   …  <=allowlist (fail-closed)
deny      docker   ok                 ALLOWLIST  True   example.com
deny      daytona  REJECT(network)    BLOCK_ALL  True   example.com  <=allowlist (fail-closed)
nonet     docker   ok                 BLOCK_ALL  True   -
nonet     daytona  ok                 BLOCK_ALL  True   -
hermetic  docker   ok                 BLOCK_ALL  False  -      ← allow_model_endpoint:false → lane OFF
hermetic  daytona  ok                 BLOCK_ALL  False  -

✅ docker resolves allowlist/no-network correctly with the model lane; hermetic (allow_model_endpoint:false) correctly turns the lane off. ✅ daytona fails closed on every allowlist variant (rejected at preflight and backstopped to BLOCK_ALL).

2 · Live parallel matrix — real openhands agent runs, --concurrency 5 on each sandbox

strategy (environment.network_mode) docker daytona
public ✅ reward 1.0 ✅ reward 1.0
allowlist — full (*.ncbi.nlm.nih.gov … + agent/model hosts) ⚠️ ERR -32603 (model 404 — see note 3, not a proxy block) 🚫 preflight REJECT (fail-closed)
allowlist — deny (example.com only) 🚫 ERR — proxy 403'd agent install 🚫 preflight REJECT
no-network 🚫 ERR — no route, agent can't install ✅ reward 1.0 (see note 2)
hermetic (no-network + allow_model_endpoint:false) 🚫 ERR — no route ✅ reward 1.0 (see note 2)

docker batch — Score: 1/5, errors=4:

[ERR] citation-check-nonet     (Agent openhands install failed (rc=127))
[ERR] citation-check-deny      (Agent openhands install failed (rc=127))
[ERR] citation-check-hermetic  (Agent openhands install failed (rc=127))
[ERR] citation-check-wildcard  (ACP error -32603: Internal error)
Job complete: 1/5 (20.0%), errors=4, time=7.2min          # the 1 pass = public (reward 1.0)

daytona batch — Score: 3/5, errors=2:

benchflow.task.runtime_capabilities.UnsupportedTaskFeatureError:
- environment.network_mode: network_mode='allowlist' is enforced only on the 'docker' sandbox
  (egress proxy); not available on this sandbox — use 'docker', 'no-network', or 'public' (ENG-219) (sandbox=daytona)
[ERR] citation-check-deny      (Task uses parsed runtime features that BenchFlow…)
[ERR] citation-check-wildcard  (Task uses parsed runtime features that BenchFlow…)
Job complete: 3/5 (60.0%), errors=2, time=6.2min          # passes = public, nonet, hermetic

3 · The proxy is enforcing in a live agent run (the load-bearing evidence)

For the deny variant the egress override is wired straight from the task.md, and the sidecar actively 403s every non-allowlisted host the agent reaches:

# applied egress override (from task.md allowed_hosts: [example.com])
{'ALLOWED_HOSTS': 'example.com', 'PORT': '8080', 'BENCHFLOW_EGRESS_LANE_HOST': '172.17.0.1'}

# openhands install log — proxy (172.21.0.2:8080) denies the Ubuntu mirrors (not allowlisted):
E: Failed to fetch http://archive.ubuntu.com/.../InRelease   403  Forbidden [IP: 172.21.0.2 8080]
E: Failed to fetch http://security.ubuntu.com/.../InRelease  403  Forbidden [IP: 172.21.0.2 8080]

When those install hosts are allowlisted, the proxy forwards them and the agent installs cleanly (apt over plain-HTTP through the proxy worked — no absolute-URI 400). So the allowlist permits exactly the listed hosts and denies the rest, in a real run.

Notes / findings

  1. Model lane covers host.docker.internal, but this deployment calls the model provider directly. The agent env sets LLM_BASE_URL=https://api.deepseek.com (not a host-side LiteLLM proxy at host.docker.internal:<port>), so the lane (172.17.0.1) is inert here and the real provider host must be allowlisted for a restrictive run. The lane mechanism itself is sound (verified separately: a host listener at 172.17.0.1 is reachable through the sidecar), but its automatic benefit only fires in the host-proxy topology. → worth folding into ENG-262 (the lane should also admit the resolved provider host under a restrictive policy, since that host is known at launch).

  2. docker vs daytona no-network strictness differs. docker enforces no-network from the start — the agent can't even install (rc=127). daytona's no-network let the agent install, reach the model, and solve (nonet/hermetic → reward 1.0). daytona backends still read allow_internet directly rather than routing through resolve_network_decision (the ENG-262 C1 defense-in-depth item), and appear to install the agent before applying the policy. Flagging as a real cross-backend inconsistency.

  3. The -32603 on docker allowlist is a model-routing quirk, not a network block. deepseek-v4-flash 404s on the configured api.deepseek.com (the model is actually served by a separate internal endpoint); even the public run hit 37× 404 before succeeding via retries to the working endpoint. The proxy permitted every listed host (install succeeded through it); the agent just couldn't complete the model call cleanly under the tighter time budget. Orthogonal to this PR.

Bottom line: the network_mode enforcement is solid — policy resolution is correct on both sandboxes, docker enforces allowlist/no-network (live 403s on unlisted hosts, agent install blocked under restrictive policy), daytona fails closed on allowlist at preflight, and allow_model_endpoint:false correctly closes the lane. The two items above (model-lane direct-provider coverage, daytona no-network strictness) are follow-ups, both already in ENG-262's scope.

Yiminnn added 4 commits June 16, 2026 03:34
Derive daytona block-all from resolve_network_decision (not the deprecated
allow_internet bool) and verify it after start with a raw-TCP egress canary;
abort with SandboxStartupError if a block-all policy can still reach the
network (platform did not honor network_block_all).
…ctive policy (ENG-263)

Under no-network/allowlist the agent cannot silently fall back to the direct
provider (the egress allowlist blocks it) — so a skipped/unavailable LiteLLM
usage proxy must abort the run instead of leaving the agent pointed at a
blocked, untracked provider endpoint. Adds network_is_restrictive +
proxy_unavailable_is_fatal and threads it through ensure_litellm_runtime.
…263)

The container now comes up open so the agent can install; relock_network()
then drops it off the public bridge (and, for allowlist/model-lane, starts the
bf-egress sidecar + moves it onto the internal-only net, returning HTTP_PROXY
for the agent). The main container is never recreated, so the install survives.
Called from install_agent() after lockdown_paths. Fixes rc=127 agent-install
failures under no-network/tight-allowlist on docker.
…s probe) (ENG-263)

The daytona no-network leak was the _lift_agent_network_to_public blanket lift
to public (the same P0 the model lane replaced for docker), still firing on
non-docker. Gate the lift for daytona/daytona-dind too (no lane there -> the
honest behavior is to keep the policy and fail closed). Also fix the egress
canary probe to a single-line python -c (the heredoc form did not execute
through daytona exec) so block-all enforcement is actually verified at start.

Verified live: daytona no-network now resolves block_all=True and the canary
(1.1.1.1:443) is unreachable -- genuinely offline, no longer silently public.
@bingran-you bingran-you added enhancement New feature or request P1 Important debt — must fix soon, but does not block the current release. status:blocked Waiting on external dependency. Add a comment explaining why. review:changes-requested Author needs to push more commits before this can merge. area:sandbox Issue / PR lives primarily in the "sandbox" subsystem. labels Jun 16, 2026
@bingran-you

Copy link
Copy Markdown
Collaborator

Automation triage (2026-06-16): still belongs in benchflow-ai/benchflow and the live verification evidence is strong, but GitHub now reports the PR as merge-conflicting (mergeStateStatus=DIRTY) with no checks on the latest head. I labeled it status:blocked / review:changes-requested / area:sandbox.\n\nNext step: rebase/merge origin/main, resolve conflicts, rerun CI + the targeted docker/daytona network-mode verification, then re-review before merge.

Resolves conflicts from #787 (Mintlify docs retirement):
- docs/docs.json: honored main's deletion (#787 retired the Mintlify
  config); the network-mode-prior-art nav entry is moot.
- docs/sandbox-hardening.md: kept main's reworded task-authoring.md line
  and Yimin's network-mode-prior-art cross-link.

Code merged cleanly. Verified: test_network_modes + test_network_policy
+ test_internet_policy + test_runtime_capabilities = 92 passed; ruff +
format + ty clean on the sandbox surface.
@bingran-you bingran-you temporarily deployed to pypi-internal-preview June 16, 2026 06:07 — with GitHub Actions Inactive
@bingran-you

Copy link
Copy Markdown
Collaborator

Automation (2026-06-16): merged origin/main into the branch and resolved the conflicts (pushed 70303ce5).

The only conflicts were docs from #787 (Mintlify config retirement):

Code merged cleanly (config.py / test files auto-merged, no logic conflicts). Re-verified locally: test_network_modes + test_network_policy + test_internet_policy + test_runtime_capabilities = 92 passed; ruff + format + ty clean on the sandbox surface.

PR should now be mergeable and CI can run on the fresh head. Still needs a human review (security-sensitive egress enforcement) + the docker/daytona network-mode E2E re-run before merge — removing status:blocked once CI is green.

@bingran-you bingran-you added review:pending PR is ready-for-review, no reviewer engagement yet. and removed status:blocked Waiting on external dependency. Add a comment explaining why. review:changes-requested Author needs to push more commits before this can merge. labels Jun 16, 2026
@Yiminnn

Yiminnn commented Jun 16, 2026

Copy link
Copy Markdown
Collaborator Author

Runtime-behavior follow-up (ENG-263) — fixes + live verification on both sandboxes

Verifying this PR's enforcement with a citation-check network-strategy matrix on docker and daytona surfaced three runtime divergences from the designed behavior (policy resolution was correct; the runtime wasn't). All three are now fixed on feat/eng-263-network-runtime, TDD'd (tests/test_network_runtime.py), 678 tests green, ruff/ty clean. Live results below (terminal captures).

Fix #1 — docker install-before-lockdown (was: agent install rc=127)

The agent installs after the policy goes live at compose up, so under no-network/tight-allowlist the install (apt/uv/pypi/github) was 403'd → openhands exit 127. Now the container comes up open for the install phase, then relock_network() drops it off the bridge non-destructively (main is never recreated — docker network disconnect/connect + up --no-deps bf-egress), returning HTTP_PROXY for the agent.

citation-check-allowcitation-only allowlist, no install/model hosts (died at rc=127 before):

Starting environment: citation-check-allow
relock_network: ALLOWLIST applied (sidecar=True)
Session: 53f168d2-...    Prompt 1/1: You are helping a research team verify the integrity of their bibliography...

→ agent installs and reaches the prompt; no rc=127.

Fix #3 — force the host usage proxy under a restrictive policy

A restrictive policy can no longer silently fall back to the (blocked, untracked) direct provider. Same run + citation-check-nonet:

relock_network: BLOCK_ALL applied (sidecar=True)
LiteLLM proxy listening on http://172.17.0.1:39921
applied egress  →  ALLOWED_HOSTS:''  +  BENCHFLOW_EGRESS_LANE_HOST:172.17.0.1   (model-only egress)

→ host LiteLLM proxy is forced + listening, the lane covers it, and no-network resolves to empty-allowlist + model-lane exactly as designed.

Fix #2 — daytona no-network now actually enforced

The daytona "leak" wasn't a leak — it was _lift_agent_network_to_public lifting no-networkpublic (the same P0 the model lane replaced for docker), still firing on non-docker. Gated for daytona/daytona-dind + a reliable egress canary at start:

verify_network_enforcement: block_all=True
egress canary 1.1.1.1:443 reachable=False        ← genuinely offline now
[ERR] citation-check-nonet (offline → agent install can't run)

Before: solved with reward 1.0 by reaching CrossRef + the model. Now: correctly fails-closed / cannot run offline. The probe also aborts the run if a future platform ever ignores the block-all flag.

Honest caveats (none are network-mode bugs)

  • The -32603 on the docker runs is an orthogonal model-routing quirk — the model deepseek-v4-flash 404s on the configured api.deepseek.com (it's served by a separate internal endpoint). The network path is correct (proxy forced, lane covers it); the proxy just relays the 404. Fix belongs in the provider registry.
  • daytona no-network + a runtime-installed agent fails at install — correct: daytona's network is fixed at sandbox creation, so it can't reorder; such tasks need pre-baked agents. It now fails cleanly instead of silently running public.
  • modal still lifts to public (not gated) — follow-up.

Full design + decisions in ENG-263. Screenshots of each run available on request.

@bingran-you

Copy link
Copy Markdown
Collaborator

Users Simulation automation review (2026-06-27T12:21Z): blocked.

Blockers:

  1. usage_tracking="required" is silently bypassed on restrictive Docker/Daytona paths. Direct probing showed ensure_litellm_runtime() in src/benchflow/providers/litellm_runtime.py:1028 can return the original env and runtime=None whenever the provider host resolves, even when token usage is explicitly required.
  2. allow_model_endpoint=false is not threaded through the restrictive allowlist path. The intent is documented around src/benchflow/task/config.py:496, but the rollout/Docker/Daytona paths still propagate the provider host into the relock allowlist. Direct Docker probe wrote ALLOWED_HOSTS=a.com,api.deepseek.com with allow_model_endpoint=False, so the no-model hermetic mode is not actually hermetic.
  3. Thermo-nuclear maintainability gate: this PR pushes both sandbox orchestrators over the 1k-line boundary: src/benchflow/sandbox/docker.py is now ~1003 lines and src/benchflow/sandbox/daytona.py is now ~1031 lines. The new network/egress lifecycle is embedded in those classes; please split the policy/relock orchestration out before landing.

Commands/evidence:

  • uv sync --extra dev --extra sandbox-daytona --locked
  • uv run pytest -q tests/test_network_modes.py tests/test_network_policy.py tests/test_network_runtime.py tests/test_runtime_capabilities.py tests/test_internet_policy.py (112 passed)
  • CLI smoke: bench tasks init, bench tasks check, bench tasks check --level runtime-capability --sandbox docker on a native allowlist task
  • Docker oracle smoke reached relock_network: ALLOWLIST applied (sidecar=True) and verifier execution, confirming sandbox startup behavior
  • validate_run_artifacts.py correctly marked the oracle smoke as non-publishable model evidence because it had no trajectory/llm_trajectory.jsonl and zero token/tool usage

Please fix the required-usage contract, honor allow_model_endpoint=false, and extract the relock/egress orchestration from the oversized sandbox classes before re-review.

@bingran-you

Copy link
Copy Markdown
Collaborator

Users Simulation automation review (2026-07-01T12:30Z): functional network fixes improved, but merge remains blocked.

Current validation on head dc85a03c6b:

  • Targeted network/usage tests passed: tests/test_network_modes.py, tests/test_network_policy.py, tests/test_network_runtime.py, tests/test_usage_required.py -> 59 passed.
  • uv run ruff check . and uv run ty check src/ passed in the PR worktree.
  • The previous allow_model_endpoint and relock concerns now have direct test coverage, so those specific functional blockers appear addressed.

Remaining blockers:

  1. GitHub still reports mergeable=CONFLICTING / mergeStateStatus=DIRTY; this must be resolved before merge.
  2. Stale user-facing preflight text remains in src/benchflow/task/runtime_capabilities.py: _append_network_issue still says network_mode='allowlist' is enforced only on Docker, even though this PR now supports Daytona allowlist planning. Please update that reason/fallback guidance so users do not get incorrect sandbox capability advice.
  3. Thermo-nuclear maintainability risk remains: src/benchflow/sandbox/docker.py is 1003 lines and src/benchflow/sandbox/daytona.py is 1031 lines. The previous review bar still applies: this should be decomposed or explicitly justified before final merge.

@bingran-you

Copy link
Copy Markdown
Collaborator

Users Simulation review (2026-07-03): blocked.

Focused tests still pass (tests/test_network_modes.py tests/test_runtime_capabilities.py tests/test_network_runtime.py tests/test_internet_policy.py tests/test_connect_as_env.py -> 105 passed), but current head still has merge-blocking runtime and structure issues:

  • allow_model_endpoint=false is not wired through lockdown. Rollout._lock_down_network() always passes the provider host into relock_network(), and Docker/Daytona add it to the allowlist; a smoke showed the same api.deepseek.com extra host for both true and false.
  • Task-authoring docs still say Daytona allowlist tasks are rejected at preflight, while current validation accepts exact-host allowlists on Daytona.
  • src/benchflow/sandbox/docker.py is 1003 lines and src/benchflow/sandbox/daytona.py is 1031 lines, so the prior thermo-nuclear decomposition blocker remains.

No model-backed trajectory was generated in this review.

@bingran-you

Copy link
Copy Markdown
Collaborator

Users Simulation automation review (2026-07-04): still blocked on unchanged head dc85a03c.

The old dirty-branch issue appears gone in the local PR worktree and focused network tests passed, but the security-sensitive model-lane bug remains real:

  • Stubbed Docker and Daytona relock probes both showed allow_model_endpoint=false still lets the provider host through the allowlist path. Docker captured ('pypi.org', 'api.openai.com'); Daytona captured model_host=api.openai.com.
  • Source confirms the shape: resolve_network_decision() records model_lane, but rollout still resolves the provider host and Docker/Daytona relock still append it.
  • docs/task-authoring-task-md.md still says Daytona allowlist tasks are rejected, while this branch now implements Daytona allowlist enforcement when expressible.
  • The thermo-nuclear structure bar is still violated in security hot paths: docker.py is 1003 lines, daytona.py is 1031, and litellm_runtime.py is 1191.

Please wire allow_model_endpoint=false through relock host selection, add regression coverage for allowlist + no model lane on Docker/Daytona, update the docs, and split the oversized runtime modules before re-review. Keeping status:blocked / review:changes-requested.

@bingran-you

Copy link
Copy Markdown
Collaborator

Users Simulation automation review (2026-07-05): blocked on head dc85a03c6b192db3082b8fa2fe402e03f5b206dd.

Fresh isolated validation reproduced the security-sensitive blocker: resolve_network_decision(..., allow_model_endpoint=False).model_lane == False, but Rollout._lock_down_network() still passes the provider host into relock_network, so Docker/Daytona allowlists still keep the model host open. The probe captured rollout_extra_allowed_hosts=('api.deepseek.com',).

Other checks: focused network/runtime tests passed (105 passed), ruff passed, ty check src/ passed, shared CLI help passed, Docker allowlist task check passed, and Modal allowlist rejection failed as expected. The blockers are the model-lane leak plus thermo-nuclear structure debt: src/benchflow/sandbox/docker.py is 1003 lines and src/benchflow/sandbox/daytona.py is 1031 lines.

Labels: keep status:blocked + review:changes-requested.

@bingran-you

Copy link
Copy Markdown
Collaborator

Users Simulation automation review (2026-07-06): functional sandbox-policy checks pass on head dc85a03c, but merge remains blocked.

Evidence:

  • Isolated worktree checks passed: Docker available, uv sync --extra dev --locked, targeted sandbox/network ruff, ty check src/benchflow, and targeted pytest slice (118 passed).
  • CLI/config smoke passed: bench eval run --help, bench tasks check --help, and a TaskConfig allowlist smoke.

Merge blockers remain live on GitHub: PR is still DIRTY against main, reviewDecision=CHANGES_REQUESTED, and no fresh end-to-end rollout/traj artifact was generated in this pass. Keep status:blocked / review:changes-requested until conflicts are resolved, review is refreshed, and an updated merge-head validation is available.

@bingran-you bingran-you temporarily deployed to pypi-internal-preview July 6, 2026 13:14 — with GitHub Actions Inactive
@bingran-you

Copy link
Copy Markdown
Collaborator

Users Simulation automation update (2026-07-06): pushed 2f087e2d7 to address the code/config blockers found on this PR.\n\nWhat changed:\n- Merged current origin/main, clearing the PR conflict state.\n- Decomposed network lockdown orchestration out of the oversized backends into focused helpers: src/benchflow/sandbox/docker_network_lockdown.py and src/benchflow/sandbox/daytona_network_lockdown.py. Current line counts are below the thermo-nuclear 1k threshold: docker.py 933 lines, daytona.py 970 lines.\n- Fixed the hermetic-policy leak: allow_model_endpoint=false now prevents the provider/model host from being appended to Docker/Daytona allowlists.\n- Guarded Docker snapshot restore under restrictive network policies so restore cannot reconnect a task to the public compose bridge and bypass enforcement.\n- Updated Daytona allowlist docs/runtime message to reflect exact-host IPv4 allowlist support and fail-closed wildcard/over-limit behavior.\n\nValidation run locally:\n- uv run pytest tests/test_network_runtime.py tests/test_network_policy.py tests/test_runtime_capabilities.py tests/test_network_modes.py tests/test_connect_as_env.py -q -> 95 passed\n- uv run ruff check ... on touched network/runtime files -> passed\n- uv run ruff format --check ... on touched Python files -> passed\n- uv run ty check src/benchflow/sandbox/docker_network_lockdown.py src/benchflow/sandbox/daytona_network_lockdown.py src/benchflow/sandbox/docker.py src/benchflow/providers/litellm_runtime.py src/benchflow/task/runtime_capabilities.py -> passed\n- git diff --check -> passed\n\nGitHub CI after the push: test, pip-audit, manifest-parity, and detect-scope are green; rollout-smoke is still pending. Keeping status:blocked / review:changes-requested until that final CI signal is green and the reviewer can re-review the decomposition/security changes.

@bingran-you bingran-you added status:ready Triaged, unassigned, available to claim. and removed status:blocked Waiting on external dependency. Add a comment explaining why. labels Jul 6, 2026
@bingran-you

Copy link
Copy Markdown
Collaborator

Users Simulation automation follow-up (2026-07-06): GitHub CI is now green on head 2f087e2d7.\n\nPassed checks:\n- test\n- pip-audit\n- manifest-parity/parity\n- integration-light/rollout-smoke\n- detect-scope jobs\n\nI removed status:blocked and added status:ready because the code/config blockers from this automation pass are addressed. Keeping review:changes-requested because this security-sensitive PR still needs reviewer re-review/approval after the merge + decomposition + hermetic-policy/restore guards.

@bingran-you

Copy link
Copy Markdown
Collaborator

Users Simulation automation review (2026-07-08): simulation ready, waiting on human re-review on head 2f087e2d70df2d2fd31a53a6a710bb8d0b848ab7.

Evidence:

  • Focused network/security checks passed in PR worktrees: tests/test_network_runtime.py, tests/test_network_policy.py, tests/test_runtime_capabilities.py, tests/test_network_modes.py, tests/test_connect_as_env.py, plus subagent coverage of tests/test_litellm_runtime.py and tests/test_task_config.py.
  • Prior blockers are fixed: allow_model_endpoint=false no longer leaks the model host into restrictive allowlists; Docker snapshot restore is blocked under restrictive network_mode; docs/runtime messaging describe the restrictive model-lane behavior.
  • Thermo-nuclear file-size blocker is addressed for the Docker backend: docker.py is now below 1k LOC with lockdown logic split into focused helper modules (docker_network_lockdown.py, daytona_network_lockdown.py).
  • Current-head integration-light-jobs artifact passed benchflow-experiment-review: ACP + LLM trajectories and training-ready results.jsonl; reward 1.0, 254,043 tokens, 15 tool calls, total timing 215.3s.

Caveat: the bundled review-skill fixture artifacts clean-pass / no-skill-leak are not publishable under the current validator because they are missing results.jsonl. That is fixture debt, not a regression in this PR's network code path.

Metadata caveat: GLM artifact token/timing coverage is healthy, but result.json.agent_result.cost_usd / price_source are null. This is a shared GLM user-endpoint cost-accounting gap, not a network-policy regression.

Labels: status:ready; kept review:changes-requested because GitHub still reports reviewDecision=CHANGES_REQUESTED on this security-sensitive PR. Human re-review is required before merge.

@bingran-you bingran-you added the status:blocked Waiting on external dependency. Add a comment explaining why. label Jul 9, 2026 — with ChatGPT Codex Connector
@bingran-you bingran-you removed the status:ready Triaged, unassigned, available to claim. label Jul 9, 2026

Copy link
Copy Markdown
Collaborator

Daily scan follow-up (2026-07-09): restoring status:blocked under the strict review bar.

The original human decomposition/security blockers appear addressed on current head 2f087e2, and CI is green. However one non-outdated review thread remains live on src/benchflow/sandbox/_egress.py:81: egress proxy readiness. The exact depends_on: service_started suggestion is not sufficient for the current relock path, but the underlying race is still valid: after bf-egress is started, the run can receive proxy env before the proxy is actually accepting connections.

Please add a small readiness guard for the egress sidecar, preferably a healthcheck plus wait/poll before returning proxy env from relock, then rerun the focused network/runtime tests and rollout smoke. After that this can move back to status:ready while the stale CHANGES_REQUESTED review awaits human re-review.

@bingran-you

Copy link
Copy Markdown
Collaborator

Users Simulation automation review (2026-07-09): blocked on current head 2f087e2d70df2d2fd31a53a6a710bb8d0b848ab7.

What is healthy now:

  • GitHub CI on this head is green (test, pip-audit, manifest-parity, integration-light / rollout-smoke).
  • Focused local network/security validation passed: tests/test_network_runtime.py, tests/test_network_policy.py, tests/test_runtime_capabilities.py, tests/test_network_modes.py, and tests/test_connect_as_env.py (95 passed). Focused ruff check and ruff format --check on the touched network/runtime files also passed.
  • The previous Docker/Daytona >1k-line decomposition blocker is addressed: src/benchflow/sandbox/docker.py is 931 lines and src/benchflow/sandbox/daytona.py is 970 lines, with lockdown orchestration split into focused helper modules.

Remaining blocker:

  • The egress sidecar still has no readiness guard before proxy env is returned to the agent. build_egress_override() wires main.depends_on = ["bf-egress"], which only gives service-start ordering, and relock_docker_network() starts bf-egress, connects main to the internal network, disconnects the default bridge, verifies network attachment, then immediately returns HTTP_PROXY=http://bf-egress:8080. There is no healthcheck or port/proxy poll proving the sidecar is accepting connections. Under restrictive network mode that can hand a live run proxy env before the proxy is actually ready.

Please add a small readiness guard for bf-egress (healthcheck plus wait, or an explicit proxy/port poll in relock) and a regression test covering the wait before returning proxy env. Keeping status:blocked / review:changes-requested until that lands and focused network/runtime checks are rerun.

@bingran-you bingran-you temporarily deployed to pypi-internal-preview July 10, 2026 16:06 — with GitHub Actions Inactive
@bingran-you bingran-you added status:ready Triaged, unassigned, available to claim. and removed status:blocked Waiting on external dependency. Add a comment explaining why. labels Jul 10, 2026
@bingran-you

Copy link
Copy Markdown
Collaborator

Daily scan follow-up (2026-07-10): re-reviewed the new head 0c78eadd35647bc47fb7bb4536d64d8555696589, which directly addresses the prior egress sidecar readiness blocker.

What changed since the last blocked comment:

  • Added explicit bf-egress sidecar readiness handling before returning the restrictive-policy proxy env.
  • Added focused regression coverage in the network runtime/policy tests.
  • Kept the earlier decomposition intact: src/benchflow/sandbox/docker.py is 931 lines and src/benchflow/sandbox/daytona.py is 970 lines, both below the thermo-nuclear 1k-line threshold.

Validation on the PR head in /tmp/benchflow-daily-pr785:

  • uv run --extra dev python -m pytest tests/test_network_policy.py tests/test_network_runtime.py -q -> 50 passed
  • focused ruff check on the changed network modules/tests -> passed
  • focused ty check on src/benchflow/sandbox/_egress.py and src/benchflow/sandbox/docker_network_lockdown.py -> passed
  • GitHub CI is green on this head: test, pip-audit, parity, and rollout-smoke all passed.

Moved labels from status:blocked to status:ready, but kept review:changes-requested because GitHub still has an active CHANGES_REQUESTED review. This is ready for human re-review, not mergeable until that gate is cleared.

@bingran-you

Copy link
Copy Markdown
Collaborator

Users Simulation automation review (2026-07-10): blocked on current head 0c78eadd35647bc47fb7bb4536d64d8555696589.

What passed:

  • benchflow --help
  • benchflow eval run --help
  • benchflow tasks check src/benchflow/demo_task --level structural --sandbox docker
  • Python import smoke
  • focused sandbox/network suite: tests/test_network_runtime.py, tests/test_network_policy.py, tests/test_network_modes.py, tests/test_connect_as_env.py, tests/test_runtime_capabilities.py (96 passed)

Blocking issue:

  • allow_model_endpoint=false is not fully guarded. rollout._lock_down_network() correctly omits the provider lane, but providers/litellm_runtime.py still takes the direct-provider skip path whenever provider_host_for_model() resolves. In a restrictive Docker/Daytona run, that leaves the agent with no reachable model and no early rejection; the failure moves to a later CONNECT denial instead of failing closed up front.

Please either reject that configuration up front, or keep the runtime on a proxy path that is compatible with the declared policy so allow_model_endpoint=false and execution behavior stay aligned.

Additional hardening note: Docker restore is now blocked under restrictive policies, but the Daytona snapshot restore path still recreates from snapshot without an explicit re-lock step. I did not prove a bypass in the current suite, but that is the remaining restore path I would harden before calling this ready.

Moving back to status:blocked; keeping review:changes-requested.

@bingran-you bingran-you added status:blocked Waiting on external dependency. Add a comment explaining why. and removed status:ready Triaged, unassigned, available to claim. labels Jul 10, 2026
@bingran-you bingran-you temporarily deployed to pypi-internal-preview July 10, 2026 17:24 — with GitHub Actions Inactive
@bingran-you

Copy link
Copy Markdown
Collaborator

Users Simulation automation follow-up (2026-07-10): ready by simulation on current head ddc6e93b9059ada6053a8d98ebb570f55ba78dcf.

The fresh restrictive-model-lane blocker from #785 (comment) is addressed by ddc6e93b9:

  • restrictive Docker/Daytona model-backed runs now inspect the resolved network decision before taking the direct-provider skip path;
  • if allow_model_endpoint=false, BenchFlow fails closed before agent launch instead of returning a runtime that cannot reach the model endpoint;
  • added a PR feat(sandbox): Enforce network_mode allowlist on docker #785 regression covering restrictive allowlist + allow_model_endpoint=false with an otherwise resolvable provider host.

Validation:

  • uv run pytest tests/test_network_policy.py tests/test_network_modes.py tests/test_network_runtime.py -q -> 63 passed
  • focused ruff check / ruff format --check on touched runtime/network test files -> passed
  • uv run ty check src/benchflow/providers/litellm_runtime.py -> passed
  • git diff --check -> passed
  • GitHub CI is green on ddc6e93b9: test, pip-audit, manifest-parity, detect-scope, and integration-light / rollout-smoke.
  • The latest integration-light-jobs artifact validates healthy with benchflow-experiment-review: ACP trajectory, LLM trajectory, results.jsonl, reward present, 338168 tokens, 18 tool calls, and training_ready=1.

Moving the PR back to status:ready; keeping review:changes-requested because GitHub still reports the prior CHANGES_REQUESTED review and human re-review is required before merge.

@bingran-you bingran-you added status:ready Triaged, unassigned, available to claim. and removed status:blocked Waiting on external dependency. Add a comment explaining why. labels Jul 10, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:sandbox Issue / PR lives primarily in the "sandbox" subsystem. enhancement New feature or request P1 Important debt — must fix soon, but does not block the current release. review:changes-requested Author needs to push more commits before this can merge. status:ready Triaged, unassigned, available to claim.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants