Skip to content

fix: address HIGH and MEDIUM findings from the #19 reviews - #21

Merged
datj9 merged 7 commits into
feat/delegation-scopefrom
fix/review-findings
Jul 26, 2026
Merged

datj9 merged 7 commits into
feat/delegation-scopefrom
fix/review-findings

Conversation

@datj9

@datj9 datj9 commented Jul 26, 2026

Copy link
Copy Markdown
Owner

Fixes every finding from the two reviews on #19. Stacked on feat/delegation-scope, so merging this lands them in #19.

HIGH (3)

Finding Fix
Escalation fired on a cancelled job — maybe_escalate gated on delegation_verdict != "failed", but that returns "failed" for any non-SUCCEEDED terminal, so crossagent cancel silently re-dispatched the task to a larger, costlier advisor escalate.py now gates on SUCCEEDED first: only a declared gate failure (check/scope/verify) escalates
tempfile.mkstemp() outside its own try broke the documented "never raises" contract. On a read-only /tmp or full disk the OSError escaped into worker_main after the check and scope ran but before the terminal record was persisted — job wedged non-terminal forever, completed work discarded verify.py extracts a _schema_file() context manager with setup in its own try/except; failure degrades to an error outcome. Same OSError hardening applied to escalate.py
Credential scrubbing missed the default path — cli.py's _run_advisor called runner.run(...) with no env=, so crossagent --agent … --prompt … handed the advisor the full unscrubbed environment, with no --pass-env escape hatch on that path cli.py now passes a scrubbed env=, sharing one policy with the job path

TIMED_OUT was decided deliberately, not inherited. CANCELLED is unambiguous — explicit user intent, never escalate. TIMED_OUT cut both ways; the call was do not escalate, because a timeout yields no gate verdict and a larger model is typically slower, so escalating tends to burn budget timing out again. Reasoning is in-code at escalate.py:122-133 so a future reader can disagree with the decision rather than rediscover the behaviour.

MEDIUM (4)

  • Forbidden placeholder names obj and info renamed.
  • run_verification 71 → under 50 lines (_interpret_run_outcome extracted).
  • maybe_escalate 135 → 94 lines (eligibility guard, child-Job build, and disk staging extracted). Still above the 50-line guideline — see Known gaps.
  • New dependency-free types.py holds the shared gate-result shapes; jobs.py 1061 → 985. The review suggested moving these into scope.py/verify.py, but those already import both the types and Job from jobs.py, so that direction creates a circular import — a neutral module is the clean route and it also removes the awkward back-import.

Verification

  • 513 passed, ruff check and format --check green.
  • The HIGH regression tests were confirmed genuine: reverting only the three source files to their pre-fix state while keeping the new tests makes 6 tests fail. A test written after a fix that passes either way documents behaviour without proving the bug is gone.
  • The MEDIUM work is a pure refactor: the test suite is byte-for-byte unchanged and still at 513. Had a test needed editing to accommodate the refactor, that would have meant behaviour changed, not code tidied.

Known gaps (deliberate)

  • maybe_escalate is still 94 lines. Further splitting would have produced helpers with long parameter lists called once — worse than a linear function that reads cleanly.
  • jobs.py (985) and cli.py (1021) remain over the 800-line guideline. Both predate this work; decomposing them is out of scope for a fix PR on a branch already under review.
  • The two security findings from the audit on feat(security): diff-scope assertion and credential withholding for delegates #19 — .git/hooks/* writes and out-of-repo writes both reporting scope ok — are not addressed here. They are gaps in enumeration rather than defects in the matcher, and warrant their own change.

datj9 added 7 commits July 27, 2026 06:24
maybe_escalate gated only on delegation_verdict != "failed", but that verdict
is "failed" for ANY non-success terminal status, so a CANCELLED job (explicit
user intent to stop) or a TIMED_OUT job was silently re-dispatched to a larger,
costlier peer. Gate on JobState.SUCCEEDED first so only a declared-gate failure
(check / scope / verify) escalates; a delegate that did not finish cleanly does
not. TIMED_OUT does not escalate: it produced no graded artifact and a bigger,
slower peer is at least as likely to time out again.

Also guard the child-staging writes (create_job_dir, prompt/command writes,
save_state) against OSError so the module's "never raises" contract holds on a
read-only or full disk.
tempfile.mkstemp() ran before run_verification's try block, so a read-only
/tmp, a full disk, or a restricted TMPDIR raised OSError out of the verifier —
which both docstrings promise never happens — past the worker's unguarded
caller, leaving the job non-terminal forever after the check and scope gates
already ran and destroying the delegate's real work.

Extract the schema file into a _schema_file context manager that degrades to
non-structured mode when the temp file cannot be created and always unlinks on
exit, so verification never propagates OSError. This also trims run_verification
back under the 50-line guideline.
Credential scrubbing was wired into the durable-job worker but not the default
`crossagent --agent ... --prompt ...` invocation: _run_advisor ran the advisor
subprocess with no env=, so it inherited the caller's full unscrubbed
os.environ, and --pass-env was registered only for `start`. The security claim
("credential withholding for delegates") therefore exceeded the implementation
for the original dispatch mode.

Scrub via the same credentials.scrub_env helper the worker uses (one shared
policy, no drift) and add the --pass-env escape hatch to the foreground parser.
Relocates CheckResultDict/ScopeResultDict/ScopeStatus/VerifyResultDict/VerifyVerdict
out of jobs.py into a new dependency-free types.py. This removes the inverted
back-import where the gate producers (check/scope/verify) imported their own
persisted-record shapes from their consumer (jobs). jobs re-exports the three it
uses as Job field annotations for jobs_mod.* callers (worker). Behaviour
unchanged; 513 tests green.
@datj9
datj9 merged commit c6a81ea into feat/delegation-scope Jul 26, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant