Harden agent runs: work-practice policy, actionable headless block, unhelped SWE-bench measurement / 加固 agent 运行:工作实践策略、可行动的 headless 拦截、不加特殊照顾的 SWE-bench 测量 - #7779
Merged
SivanCola merged 10 commits intoAug 7, 2026
Conversation
Problem: benchmark and real-world runs showed recurring, avoidable agent behavior failures: substituting a "better" approach for the one the issue explicitly named, leaving scratch/probe files in the repo, declaring done with known regressions, re-running full test suites until the time budget died, and retrying dead network paths dozens of times. Root cause: the default system prompt is three lines of persona; no stable guidance covers baseline engineering discipline, and custom personas could not inherit any either. Fix: add config.WorkPracticePolicy and append it to every system prompt next to UserDecisionPolicy, so custom personas cannot drop it either. The rules are general engineering practice, not benchmark hints: implement the requested approach exactly (state disagreements explicitly), keep scratch out of the repo and review the final diff, scale verification to the change, and treat proxy failures as an offline environment. Verification: new boot test asserts the policy is appended to custom prompts and keeps deterministic order; golden provider-request and prefix-shape baselines regenerated (REASONIX_UPDATE_GOLDEN=1) since the stable prefix deliberately changes. go test ./... green. Cache impact: one-time prefix change at upgrade; per-turn cache stability is unaffected.
…shell block Problem: in non-interactive auto mode, a blocked inline interpreter call (python -c, node -e) told the agent only to "use an interactive session or YOLO mode" — something a headless run cannot do. Agents burned dozens of calls retrying inline-code variants before stumbling onto the allowed path on their own (88 blocked retries in one SWE-bench arm). Root cause: the block message offered only mode switches, not the in-session alternative the policy actually allows. Fix: keep the gate exactly as strict (inline code stays blocked because the host cannot audit it), but make the message actionable: write the code to a file with write_file and run that file instead, or use read_file/grep for inspection. Verification: new gate-level test pins that python -c stays blocked, that the reason names the script-file workaround, and that running the script file passes the same gate. go test ./internal/control and ./... green.
…atch Problem: a SWE-bench submission was zeroed when the agent's Sphinx repro build left a binary .pickle in /testbed: git add -A followed by git diff --cached degraded it to a "Binary files differ" placeholder, which git apply rejects wholesale — the correct source fix in the same patch never reached the grader. Root cause: patch extraction swept every untracked file into the diff, including content a text patch cannot represent and build output that is never the fix. Fix: extract via numstat, then diff only the paths a text patch can honestly carry: drop binary entries (unrepresentable) and well-known generated trees (_build, __pycache__, *.egg-info). Plain text scratch files stay: they apply cleanly, do not affect grading, and dropping them by name would risk dropping a legitimate new source file. Paths are passed as plain argv, never through a shell. Verification: unit tests cover keep/drop classification (fix, text deletion, binary, build output, egg-info, cache tree, scratch script) and malformed input. go test ./cmd/e2ebench green.
…ce policy
The work-practice policy is appended to every system prompt, so every clause
has to hold for any user on any machine. Two of them did not. The verification
budget ("avoid repeated full-suite runs", "confirm once that they pre-exist and
move on") and the network rule ("treat the environment as offline: stop
retrying") describe a benchmark container, not a developer's laptop: told to
everyone they push ordinary sessions toward under-verifying, and toward
abandoning a network call that a retry would have completed.
Drop both from the global policy and add `[environment] offline`, which the
deployment that knows its egress is blocked declares for itself. Deliberately a
declaration and not a probe: reaching out to test connectivity on every boot
would cost a request, leak the startup, and let one transient flap rewrite the
cache-stable prompt prefix. The note sits outside EnvironmentEnabled because
that switch only turns off tool probing, while a blocked network is a fact the
agent still needs.
The SWE-bench harness declares it in the container config, so the benchmark
keeps exactly the behavior the dropped clause provided: runSwebench already
requires -network with no off-box route, so egress is blocked there by
construction.
Tests pin the split in both directions: the global policy may not assert
offline/proxy/full-suite claims, and the note appears only when declared.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…t's favor The run is meant to read what a default install of the software actually does, so anything that helps the agent only inside the harness makes the score unreadable. Two such helps existed. The container config declared `[environment] offline = true`. It is true — runSwebench requires -network with no off-box route — but a user whose network is blocked does not know to set that flag, so the benchmark was measuring a better-configured agent than the one that ships. Removed. An agent that burns its budget retrying dead requests is a real weakness, and surfacing it is the benchmark's job; the fix for it belongs in the product, where it would help every user. `[environment] offline` itself stays for genuinely air-gapped installs. `[sandbox] bash = "off"` stays too: bubblewrap is absent from the official images and Reasonix would otherwise refuse to run bash at all, so it is a necessity rather than a favor. Patch extraction dropped `_build`, `__pycache__` and `*.egg-info` paths by name. Those apply cleanly and do not change the grade, so removing them only hid the agent's leftovers from the submitted diff. Now only binary entries are dropped, which is the one thing a text patch genuinely cannot carry and the defect that zeroed sphinx-11510 in the first place. Tests pin both: the container config may carry no setting other than sandbox.bash, and generated-looking text paths must survive into the patch. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Problem: retaining every text path in one docker exec command can exceed the host's argv limit and lose the entire SWE-bench patch.\n\nRoot cause: patch extraction had no byte budget for the pathspec list.\n\nFix: split literal pathspecs into conservative 16 KiB batches, run git diff once per batch, and concatenate the patch output. Oversized single paths fail explicitly.\n\nVerification: go test ./cmd/e2ebench and go test ./... pass; batching tests cover size limits, order preservation, and oversized paths.
SivanCola
marked this pull request as ready for review
August 7, 2026 09:22
This was referenced Aug 7, 2026
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
Three small, independently reviewable hardening changes, each grounded in failures observed in real SWE-bench Verified runs (50-instance subset, official images, official harness) — and each one applies to ordinary headless/interactive runs just as much as to the benchmark.
The benchmark is an instrument, not a target. It exists to read what the shipped software actually does, so nothing here is tuned for the score:
A consequence worth stating plainly: some of this is expected to lower the measured score relative to earlier runs, because earlier runs were quietly helped. That is the point — the lower number is the honest one.
1.
feat(agent): work-practice policy in the stable system prompt, environment facts kept out of itThe default system prompt is three lines of persona. Runs repeatedly showed the same avoidable behavior failures: substituting a "better" approach for the one the issue explicitly named (the agent's own tests all passed; the requested behavior was never delivered), leaving repro/probe files and build output in the repo, and declaring done with known regressions.
Adds
config.WorkPracticePolicy, appended to every system prompt next toUserDecisionPolicy(so custom personas cannot drop it either): implement the requested approach exactly and state disagreements explicitly; keep scratch out of the repo and review the final diff before declaring done; scale verification to the change.Every clause in the global policy has to hold for any user on any machine. Two candidate clauses did not, and are deliberately not in it:
Both are properties of where the agent is running, not of good engineering. The second becomes
[environment] offline, a declaration an air-gapped deployment makes for itself, rendered asconfig.OfflineEnvironmentNotenext to the environment block. It is deliberately a declaration and not a probe: testing connectivity on every boot would cost a request, leak the startup, and let one transient flap rewrite the cache-stable prompt prefix. It sits outsideEnvironmentEnabledbecause that switch only turns off tool probing, while a blocked network is a fact the agent still needs. The run budget gets no prompt clause at all — that belongs to the harness (timeouts, step caps).The SWE-bench containers do not set it; see change 3.
Cache impact (deliberate): the stable prefix changes once at upgrade; per-turn cache stability is unaffected. The offline note is off by default, so it never enters an ordinary user's prefix. Golden provider-request / prefix-shape baselines regenerated via
REASONIX_UPDATE_GOLDEN=1.2.
fix(control): actionable headless dynamic-shell blockIn non-interactive auto mode, a blocked
python -c/node -etold the agent only to "use an interactive session or YOLO mode" — impossible in a headless run. Agents burned dozens of calls retrying inline-code variants (88 blocked retries in one benchmark arm). The gate stays exactly as strict (inline code is not auditable); the message now names the in-session workaround: write the code to a file withwrite_fileand run that file, or useread_file/grepfor inspection.This one is not benchmark-specific in any way: the same gate serves ordinary
-pruns (internal/control/controller.go) and desktop subagents (desktop/subagents_app.go), which hit the identical dead end. The benchmark only made it countable.3.
fix(e2ebench): keep unappliable binaries out of the SWE-bench patch — and nothing elseA submission was zeroed when a Sphinx repro build left a binary
.picklein/testbed:git add -A+git diff --cacheddegraded it to aBinary files differplaceholder thatgit applyrejects wholesale, so the correct source fix in the same patch never reached the grader. That is a defect in the submission format rather than in the agent, and repairing it is the only reason the extractor exists. Paths are passed as plain argv, never through a shell.Binary entries are the only thing dropped. Build output,
__pycache__,.egg-infoand leftover repro scripts all go into the patch as-is: they apply cleanly and do not change the grade, so filtering them would only hide the agent's mess from its own diff and report a cleanliness the run did not have. (An earlier revision of this PR dropped them by name; that was harness cleanup and has been removed.)The container config also drops
[environment] offline = truefor the same reason. Egress genuinely is blocked —runSwebenchrequires-networkwith no off-box route — but a real user whose network is blocked does not know to set that flag, so declaring it measured a better-configured agent than the one that ships. An agent that burns its budget retrying dead requests is a real weakness; the benchmark's job is to surface it, and the fix belongs in the product where it helps everyone.[sandbox] bash = "off"stays: bubblewrap is absent from the official images and Reasonix would otherwise refuse to run bash at all, so it is a necessity, not a favor.Two consequences for reading results: scores from before the binary fix are not comparable with scores after it (and the bias it removes is uneven — repos whose builds emit artifacts were penalized far more than pure-Python libraries), and any movement in a run that also carries change 1 must not be attributed to behavior alone.
Verification
enabled = falsewhile the probed section does not; gate-level test pinspython -cstill blocked, workaround named, and script-file execution actually allowed by the same gate; the SWE-bench container config decoded with the realconfig.Configand asserted to carry no setting other thansandbox.bash; patch keep/drop classification incl. binaries dropped, generated-looking text kept, and malformed numstat.go build ./...,gofmt, andgo test ./...(root module) green apart from three failures that reproduce identically on the base commit in this container: two depend onchmod-based unwritability (bypassed when running as root) and one needs an X display.Not changed, deliberately
update_goaltool gating: goal turns are stamped per-turn within ordinary sessions, so per-turn tool-list changes would break the prompt-cache prefix on every transition; the existing absorb-and-repair path already bounds the cost of a stray call.CI Metadata
Documentation-impact: updated - documented [environment].offline in reasonix.example.toml and docs/GUIDE.md, docs/GUIDE.zh-CN.md, and docs/SPEC.md.
Cache-impact: medium - WorkPracticePolicy changes the stable system prompt once; the optional offline note is excluded by default; per-turn prefix stability is unchanged and golden baselines are updated.
Cache-guard: focused prompt/config tests in internal/boot and internal/config; go test ./internal/boot ./internal/config; go run ./tools/repolint on the PR merge tree.
System-prompt-review: self-review completed; policy ordering and custom/offline prompt isolation are covered by tests.