fix(agents): run the no-web JSON policy on the provisioned Node runtime - #1048
Open
tulerfeng wants to merge 1 commit into
Open
fix(agents): run the no-web JSON policy on the provisioned Node runtime#1048tulerfeng wants to merge 1 commit into
tulerfeng wants to merge 1 commit into
Conversation
Agent-owned pre-launch setup executes inside the task image, which owes BenchFlow no interpreter. The no-web JSON merge shelled out to python3 for four JS agents whose installs guarantee Node, so a valid Python-free task with allow_internet = false aborted at exit 127 before ACP launch. Reimplement the merge on /opt/benchflow/node/bin/node, the absolute path _js_agent_install already provisions, and translate the four mutators to JavaScript. Output stays byte-identical: the escape pass reproduces the ensure_ascii behaviour of json.dumps, without which upgrading would rewrite non-ASCII values in a settings file copied in from the host via subscription_auth and break the idempotence this merge promises. Fixes benchflow-ai#1047
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
Agent-owned pre-launch setup runs inside the task image, but the no-web JSON settings merge shelled out to
python3— something no task image is required to ship. On a Python-free task withallow_internet = false, all four JS agents (claude-agent-acp,gemini,opencode,mimo) died at exit 127 before ACP ever started. I moved the merge onto the Node runtime BenchFlow provisions itself and kept the output byte-identical, verified across 40 base-vs-head combinations and end-to-end on bothdockeranddaytonawith a real Gemini key.Fixes #1047
Reproduction
I reproduced it with a real
bench eval run.tests/examples/hello-world-taskalready ships a Python-free image (ubuntu:24.04+curl), so all I had to change wasnetwork_mode = "no-network":I got a byte-identical failure on
--sandbox dockerand--sandbox daytona, which matches the report: an infrastructure fault surfacing as a rollout error rather than a model failure.The fix
I moved the merge onto
/opt/benchflow/node/bin/node— the absolute path_js_agent_installalready provisions — and translated the four mutators to JavaScript. The ordering holds by construction:install_agent()isrollout/__init__.py:1195,apply_web_tool_policyis:1228, so the runtime is always in place before the policy needs it. I left the path as an unquoted shell word so the surrounding bash expands$BENCHFLOW_AGENT_HOME, which is whatos.path.expandvarsdid before.Output stays byte-identical, including non-ASCII. This is the one place I could have made the change silently lossy.
json.dumpsdefaults toensure_ascii=True;JSON.stringifyemits raw UTF-8. Semantically equivalent, but not the same bytes — and an agent home can arrive pre-populated from the host, since gemini copies~/.gemini/settings.jsonin viasubscription_auth. A user with non-ASCII values would have had that file rewritten on their first upgraded run, breaking exactly the idempotence this merge promises. So I re-escape from U+007F up, matchingjson.dumpsbyte for byte. I start at U+007F rather than U+0080 becauseJSON.stringifyhas already escaped in-string control characters, while the newlines and indent it emits must stay literal.I also kept the semantics strict rather than quietly "improving" them:
if(!('k' in d))reproducessetdefaultinstead ofd.k ||= {}, so a config with"tools": nullstill fails loudly rather than being silently repaired. Same fail-closed behaviour, same error surface.The ownership boundary is now asserted, not implied. The reason a
python3dependency could sit in a JS agent's policy unnoticed is that nothing tied the two together. I addedtest_node_backed_policies_are_guaranteed_by_their_own_install, which walks the whole registry and requires that any policy reaching for the Node prefix belongs to an agent whose owninstall_cmdprovisions it — so the next agent that grows a JSON policy either brings its runtime or fails in CI.End-to-end verification
Both canaries are live runs, not stubs: I used a real Gemini API key on
gemini-3.5-flash-liteand a real Daytona cloud sandbox. Same task and flags throughout —tests/examples/hello-world-taskwithnetwork_mode = "no-network",--agent gemini,--trials 1.docker62cc7e41(previous version)apply_web_tool_policy—exit code 127,python3: command not found. Never reaches ACPdocker_configure_acp_sessioncompleted. Stops further downstream atenforce_agent_egress_firewall(acp/runtime.py:692) —iptables: Permission denied, the container lackedNET_ADMIN. Unrelated to this change and strictly after the step it fixesdaytona62cc7e41(previous version)daytona1/1 passed, mean reward 1.00, errors=0, 56.6k tokens, telemetry 100%— the agent actually solved the task, with model traffic flowing through the in-sandbox LiteLLM proxyBeyond the canaries I A/B'd the merge itself directly: 4 agents × 10 scenarios, previous version vs this PR, byte-for-byte identical in all 40 — absent file, empty file, unrelated keys, partly-applied config, already-applied (idempotence), Chinese text, emoji (surrogate pair), mixed non-ASCII with quotes and backslashes,
nullwhere an object is expected (both fail), malformed JSON (both fail). I ran the previous version inpython:3.12-slimand this PR innode:22-slim, with no Python present at all.Acceptance criteria
python3and start with the provisioned Node prefix; a registry-wide invariant test ties the dependency to the installerchownthatapply_web_tool_policyappends with&&— so I exercised it separately: withBENCHFLOW_AGENT_HOME=/home/agentand a root-owned pre-existing config, the policy leaves both the directory and the fileagent:agentand writable by the sandbox usertest_no_web_policies_apply_in_a_python_free_task_image: assertsubuntu:24.04ships nopython3, runs_NODE_INSTALL, applies each policy twice, checks the resulting config content. Fails on the previous version withexit 127 python3: command not found, passes on this PRdocker/ this PR row abovedaytona/ this PR row above. Parity holds in both directions: the previous version fails identically on both backends, this PR clears the policy on bothRegression coverage
I added three tests to
tests/test_internet_policy.py: the two invariants above plus the containerised regression (skipifno Docker, in line with how the repo guardscontainer/dockertests).I also had to fix six existing tests, which is the report's own point —
_run_setup_cmdexecutes policy commands on the developer host, where Python is always present, so it could never have caught this. It now retargets the sandbox Node prefix at the host'snodeand skips when there is none; real task-image coverage lives in the new container test rather than in a host shell.Full suite: 5742 passed, zero regressions against
62cc7e41— I checked that the failure set is identical with the change stashed.ruff checkandruff format --checkclean.