fix(tests): make the stdin-write/unregister guard discover, and record two residuals (#2448) - #2450
Conversation
f329a1b to
d779147
Compare
…d two residuals Follow-up to #2435, whose re-review landed after the PR had already auto-merged. Three things, all of them corrections to claims that shipped wider than the code. 1. The guard enumerated two files, so it did not guard. Proven by dropping a `mistral_runtime.py` into the tree with an unpaired `stdin.write`: "2 passed, 13 deselected". It now DISCOVERS — every `agent_server/**/*.py` that drives the process registry — and requires each `stdin.write` to be covered. A `*_runtime.py` glob would not have been enough either: it misses `claude_code.py` AND `headless_executor.py`, and the latter is a real fourth site the enumeration had never counted. `headless_executor` is safe by a DIFFERENT shape — `_run_headless_subprocess` registers and writes stdin with no local guard, but it is handed to `run_in_executor` inside `execute_headless_task`'s guarded try, so an exception reaches that `finally: unregister()`. The guard models both shapes, and a companion test pins WHICH shape covers each known site, because caller-pairing is name-based and a collision would otherwise mask a real offender. Also fixes the vacuity bug that blocked this move: the old helper returned `total > 0 and guarded == total`, so a discovered file with no stdin write (codex, `stdin=DEVNULL`) would have read as a FAILURE the moment enumeration stopped. It now returns an offender list, and a separate test pins the discovery floor so a broken walk fails loudly instead of passing on an empty set. 2. `_publish_calling_and_check_cancel_sync` said "fails soft", which is true of the read half it replaced and NOT of the write half it adds. If the publish pipeline raises transiently while the other worker's connection is healthy, the marker keeps saying `parked`, so the remote can still finalize CANCELLED under a live POST — the original #378 symptom on the Redis-error path. Documented rather than mechanised: bounded by the 30s negative cache plus one tick, a hard outage is safe by construction (a process whose client is None never wrote a marker, so the remote routes through the agent), and failing closed was rejected because every other Redis touch here is fail-open. architecture.md and chat-turn-cancellation.md had inherited the too-wide "closed" claim and are qualified to match. 3. `restamp_execution_dispatch` records WHY the re-stamp reaches `duration_ms` — it is computed from that DB column, not from the in-coroutine `start_time`, which is taken before the capacity acquire and is why the sibling `execution_time_ms` still spans the park and is not a bug. No behaviour change outside the docstrings; the guard is the substantive part. Tests: tests/unit/test_2433_review_fixes.py 15 -> 20, verified against the PoC above by putting a real file in the tree (caught by file:line, then removed). Full unit suite under CI conditions on this base: 13098 passed, 0 failed. Fixes #2448 Refs #2433, #2435 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
d779147 to
87fd32c
Compare
dolho
left a comment
There was a problem hiding this comment.
/review — #2450 (#2448, follow-up to my #2435 re-review)
Built a worktree off 87fd32c0 and tried to defeat the guard rather than read it. This does what I asked and one thing better — the discovery found a site my own suggestion would have missed.
It works, and the mutations prove it
| mutation | result |
|---|---|
| a NEW fourth runtime that registers then writes stdin unpaired | FAIL — mistral_runtime.py:5, exact file:line |
strip the local pairing from gemini_runtime.py |
FAIL ×2 (offender + mechanism pin) |
strip the local pairing from claude_code.py |
FAIL ×2 |
20 pass on the real tree. That first row is the whole point of the PR — the enumerating predecessor could not produce it.
The correction I should record: my suggested fix was wrong
I said "glob agent_server/services/*_runtime.py plus claude_code.py". That misses headless_executor.py, which is a live fourth site (stdin.write at line 1085), and it is caller-paired rather than locally paired — a shape I had not considered at all. Discovering on "drives the process registry" instead of on filename is the right axis, and the four sites it finds are exactly right:
claude_code.py stdin writes at [343]
codex_runtime.py stdin writes at - (vacuously clean, stdin=DEVNULL)
gemini_runtime.py stdin writes at [264, 710]
headless_executor.py stdin writes at [1085] (caller-paired)
The total > 0 and guarded == total vacuous-failure trap I flagged is gone too — a module with no write returns [].
The residual docstrings are better than what I asked for
I asked only that the fail-soft claim stop covering both halves. This also quantifies the window, and I verified the numbers rather than taking them: INFLIGHT_REDIS_RETRY_SECONDS = 30.0, INFLIGHT_TICK_SECONDS = 15.0, _note_redis_failure arms _REDIS_UNAVAILABLE_UNTIL = now + 30s, and _get_client's default honours it — so the refresher genuinely cannot republish until it lapses plus one tick. The bound is real.
The duration_ms / execution_time_ms two-clocks note in executions.py is also correct — that matches what I traced end-to-end in the first review.
One finding
The caller-pairing exemption is name-based over ATTRIBUTES too, and that is reachable (Confidence 9/10)
_unguarded_stdin_writes builds covered_names from every identifier inside a guarded try:
if isinstance(n, ast.Name):
covered_names.add(n.id)
elif isinstance(n, ast.Attribute):
covered_names.add(n.attr) # <-- this halfThe docstring flags a possible collision, but the ast.Attribute half makes it ordinary rather than theoretical: any method call inside any guarded try exempts every same-named function in the module. Planted this — a plain shape, not a contrived one:
class Runtime:
def execute(self, prompt, process, execution_id): # unpaired: a real leak
get_process_registry().register(execution_id, process, metadata={})
process.stdin.write(prompt)
process.stdin.close()
def dispatch(self, runtime, execution_id, prompt, process):
try:
return runtime.execute(prompt, process, execution_id) # <-- exempts `execute`
finally:
get_process_registry().unregister(execution_id)→ 2 passed. The leak is invisible. execute / run / send / close are exactly the names that appear as attribute calls inside a dispatch try.
This matters more than a normal false negative because it lands on the case the PR exists for: test_each_known_site_is_pinned_to_its_pairing_mechanism protects the four known sites from silently switching path, but a new module — the thing discovery was added for — gets no such pin.
Fix, verified both directions: delete the ast.Attribute branch.
real tree with the branch removed -> 20 passed (unchanged)
the planted false negative, branch removed -> FAIL (caught)
It stays green because the one legitimate caller-paired site is a bare Name — run_in_executor(_HEADLESS_EXECUTOR, _run_headless_subprocess, ctx) at headless_executor.py:1661. So the attribute half buys nothing today and costs the guard its teeth on new code. Two lines.
Minor
_is_registry_module keys on n.func.attr == "register", so a module that only calls register_pending and writes stdin is out of scope. unregister() discards pending too, so the pairing would be meaningful there. No such module exists today — worth a sentence in the docstring rather than a code change, so the next reader knows the boundary was chosen.
Clean
- Source changes are docstrings only — correct. I explicitly asked for no mechanism here, and none was added; the ordering fix from #2435 is untouched.
- Docs are honest, including the part that corrects me: the flow doc and
architecture.mdnow name the fourth site and say why a glob misses it. tests/registry.jsonupdated with the newstatic-guardtag and a description matching what the tests now do.- Test-merges clean against current
dev(13 behind). - Suite:
2433/2435/limiter/registry/headless/cleanup→ 578 passed, 1 skipped. The 4 failures in that run aretest_ent14_registry_url_ssrf.py::test_the_boundaries_either_side_stay_public— pre-existing ondev(verified on13272199with the same four params), unrelated to this PR.
Recommendation
Approve after the two-line ast.Attribute removal. Everything else is done, and done more carefully than the review asked for.
…2450 review) `_unguarded_stdin_writes` collected both `ast.Name` ids and `ast.Attribute` attrs into the caller-pairing set, so ANY method call inside ANY guarded try exempted every same-named function in the module. An ordinary dispatch shape try: return runtime.execute(prompt, process, execution_id) finally: get_process_registry().unregister(execution_id) therefore exempted a `def execute(...)` that registered and then wrote stdin unpaired — a real leak, invisible. `execute` / `run` / `send` are exactly the names a dispatch try calls, so this was reachable rather than theoretical, and it landed on the one case discovery exists for: the four known sites are pinned by the mechanism test, but a NEW module has no such pin. Verified both directions before and after: the planted shape is a false negative with the attribute half and is caught without it, while the real tree is unchanged — the single legitimate caller-paired site passes a bare name (`run_in_executor(_HEADLESS_EXECUTOR, _run_headless_subprocess, ctx)`), so the attribute half bought nothing. Removed from both copies of the logic (`_unguarded_stdin_writes` and `_pairing_mechanisms`), with the boundary and its remedy stated in the docstring: a future attribute-paired site is reported rather than silently exempted, and the fix is to reference the function by name or add a justified allowlist entry — never to re-add the attribute half. Also records why scope keys on `register(` and not `register_pending(` — a chosen boundary (no such module exists today), not an oversight. Mutation battery, all as expected: real tree 21 passed; a new unpaired runtime, gemini with its local pairing stripped, and claude_code with its local pairing stripped each fail. Refs #2448, #2435 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Fixed in And your reasoning for why it's safe to delete holds structurally, not just empirically — the one legitimate caller-paired reference is a bare So the attribute half genuinely bought nothing. Removed from both copies of the logic — Your mutation battery reproduces on my side after the change:
The planted shape is now pinned as its own test, so the hole can't reopen quietly. I also wrote the boundary and its remedy into the docstring, because the next person to hit an attribute-paired site will be tempted to put the branch back: such a site is now reported, and the fix is to reference the function by name or add a justified allowlist entry — never to re-add the attribute half. That keeps the failure direction loud, which is the property the whole guard rests on. Minor — taken as you suggested, docstring not code: scope keys on On Thanks for the correction on your own suggested fix, too — recording that the glob would have missed |
Summary
Follow-up to #2435. Its re-review arrived after the PR had already auto-merged, so two items never landed — and one of them is the item the reviewer said was the only thing standing between the fix and its own recurrence. All three changes here are corrections to claims that shipped wider than the code.
mistral_runtime.pyinto the tree with an unpairedstdin.write:2 passed, 13 deselected. It now discovers — everyagent_server/**/*.pythat drives the process registry — and requires eachstdin.writeto be paired withunregister()._publish_calling_and_check_cancel_syncsaid "fails soft", which is true of the read half it replaced and not of the write half it adds. The residual is now documented at the helper, inarchitecture.mdand inchat-turn-cancellation.md, all of which had inherited the too-wide "closed" claim.restamp_execution_dispatchrecords why the re-stamp reachesduration_ms— and why its siblingexecution_time_msstill spans the park and is not a bug.No behaviour change outside the docstrings. The guard is the substantive part.
The fourth site
A
*_runtime.pyglob would not have been sufficient either: it missesclaude_code.pyandheadless_executor.py— and the latter is a real fourth site the enumeration had never counted. It is safe, but by a different shape:_run_headless_subprocessregisters and writes stdin with no local guard, yet it is handed torun_in_executorinsideexecute_headless_task'strywhosefinallyunregisters, so an exception propagates there.So the guard models both accepted shapes:
trythat unregistersclaude_code.py,gemini_runtime.py×2tryheadless_executor._run_headless_subprocessA companion test pins which shape covers each known site, because caller-pairing is name-based (no call graph) and a name collision could otherwise mask a real offender — a false negative. Cross-module caller pairing is deliberately not modelled: it fails loudly rather than passing silently, which is the safe direction for a guard.
Two traps the reviewer flagged for exactly this move, both handled:
_stdin_write_is_guardedreturnedtotal > 0 and guarded == total, so a discovered file with no stdin write would read as a FAILURE the moment enumeration stopped (codex_runtime.pyregisters but usesstdin=DEVNULL). It now returns an offender list, so "no writes" is vacuously clean.The documented residual
If the owner's publish pipeline raises transiently while the other worker's connection is healthy, the marker keeps saying
parked, so the remote can still finalize CANCELLED under a live POST — the original #378 symptom on the Redis-error path. Bounded by the 30s negative cache plus one tick. A hard outage is safe by construction: a process whose client isNonenever wrote a marker, so the remote's first read returnsNoneand routes through the agent. Failing closed was rejected — every other Redis touch in this subsystem is fail-open, so a blip would fail every dispatch — and a retry cannot close it either.Changes
tests/unit/test_2433_review_fixes.py— 15 → 20 tests; the enumeratingparametrizereplaced with discovery + mechanism pinningsrc/backend/services/agent_call_limiter.py— residual docstringsrc/backend/db/schedules/executions.py—duration_msvsexecution_time_msdocstringdocs/memory/architecture.md,feature-flows/chat-turn-cancellation.md,feature-flows/parallel-headless-execution.md— qualify the "closed" claim; document the fourth site and the discovering guardtests/registry.jsonTest Plan
file:line, then removedtests/unit/test_2433_review_fixes.py→ 20 passed2433 / cleanup / watchdog / slot / limiter / terminate / 679 / 1332 / 1094 / 2127 / headless) → 220 passeddevworktree, no submodules, seed 20260831) → 13098 passed, 0 failedFixes #2448
Refs #2433, #2435
Generated with Claude Code