Repository navigation
Description
Summary
infra/cargo-target-janitor/bin/cargo_target_janitor.py: the janitor's deletion-safety readers disagree about malformed input. The two gh-based readers convert shape errors to SafetyError — labelled_claimed_issues (:400, conversion at :427) and open_pr_branches (:486, conversion at :515) both catch (ValueError, KeyError, TypeError) and re-raise as SafetyError("invalid gh ... response"). The other three readers do not:
parse_fallback_scan(:384) — the LIVE fallback claims path (cargo-target-janitor: queue helper path is dead (~/hatch/tools/repo-queue.py) — every hourly run refuses cleanup while in-scope targets sit at 275G vs the 150G cap #6812 made it the primary source when the queue helper is absent):scan.get,item.get,claim.get, andint(item["number"])are all unguarded;queue_claims(:277) —rows.extend(page),int(row["id"]),(row.get("payload") or {}).get(...)unguarded;queue_task_states(:323) —int(row["id"])/row["status"]unguarded.
That matters because of how main ends (:963): it catches (SafetyError, OSError, CalledProcessError, TimeoutExpired, SystemExit), writes the refused run record to the state-dir log, and exits 2. The comment there records the #6812 lesson verbatim: a refusal is a run too — until it left a record, an hourly-dead janitor was visible only in the systemd journal. A malformed scan document or broker row raises AttributeError/KeyError/TypeError/ValueError instead, which is NOT in that tuple: the process dies with a raw traceback, no refused record is written, and the janitor is hourly-dead invisibly again — the exact failure mode #6812 fixed, through a different door. (The crash still happens before any deletion, so this is an observability/robustness defect, not a wrongful-deletion one.)
Evidence (verified this run, laptop @ origin/main 07ecb67)
Probe: ~/git/fleet/probe-janitor-shapes/probe.py (laptop), importing the real cargo_target_janitor.py from the laptop primary and driving the three readers directly. Control plus 10 malformed shapes:
- control scan
{"items":[{"number":123,"claim":{"state":"live"}}]}→{123}OK; - scan item is a string →
AttributeError: 'str' object has no attribute 'get'; - scan claim is a string →
AttributeError(same); - scan item missing
number→KeyError: 'number'; - scan
numberis a dict →TypeError: int() argument ... not 'dict'; - scan
itemsis a dict / scan itself is a list →AttributeError; queue_claimspage is a PostgREST-style error dict →AttributeError: 'str' object has no attribute 'get'(the dict's keys are extended as rows);queue_claimsrow id"abc"→ValueError: invalid literal for int();queue_claimsrowpayloadis a string →AttributeError;queue_task_statesrow missingid→KeyError: 'id'.
Every malformed case: SafetyError=False — none is the exception main records as a refusal.
Why it matters
The janitor is the only thing standing between the fleet worktree cargo targets and the disk-full failures in #6797/#3372, and its safety model is "fail closed AND leave a record". A shape change in the fleet queue fallback scan output (a binary that is actively evolving, #6658) or a broker error payload returned as a 200-shaped dict silently reverts the janitor to dying hourly with no state-dir record — the operator signal #6812 deliberately built disappears, while the systemd journal is the only witness, again.
Acceptance criteria
-
parse_fallback_scan,queue_claims, andqueue_task_statesvalidate the shapes they consume (scan is a dict with a listitems; each item/claim is a dict;number/idare int-convertible;payloadis a dict or null; page/rows are lists of dicts) and raiseSafetyErrornaming the reader and the bad shape — mirroring thelabelled_claimed_issues/open_pr_branchesconversion pattern. - A malformed scan or broker response produces the
refusedrun record and exit 2 frommain, never a raw traceback. - Well-formed inputs behave byte-identically: the control scan, a normal claimed set, and normal task states return the same results as today.
- Tests in
infra/cargo-target-janitor/tests/test_cargo_target_janitor.pycover the malformed shapes above for all three readers plus the well-formed controls. - No change to what the janitor deletes, its caps, or its claim semantics — protection sets are computed exactly as before for valid inputs.
🤖 Filed by the fleet new-issue-creator (lane B) — verified with a probe against the real module this run; see the probe path in the evidence section.
Activity
developing-today commentedon Nov 7, 2026
CODE-2112
dezren39 commentedon Nov 7, 2026
🤖 Agent note · filed
dezren39 commentedon Nov 8, 2026
🤖 Agent note · picked up
20 remaining items
dezren39 commentedon Nov 8, 2026
🤖 Agent note · picked up (resume)
dezren39 commentedon Nov 8, 2026
🤖 Agent note · parked — CI pending on PR #7005
fix(cargo-target-janitor): readers raise SafetyError on malformed sha…
dezren39 commentedon Nov 8, 2026
Fixed and merged. PR #7005 squash-merged as
daa445170d2668183e05c0ab23fd34c934f61487.queue_claims,queue_task_statesandparse_fallback_scannow raiseSafetyErrornaming the reader and field on malformed page/row/payload/status shapes, instead of dying on rawTypeError/ValueError— closing the second door of the cargo-target-janitor: queue helper path is dead (~/hatch/tools/repo-queue.py) — every hourly run refuses cleanup while in-scope targets sit at 275G vs the 150G cap #6812 invisible-death mode._shape_intvalidates shape and rejects bools and non-int/non-str values with a namedSafetyError.c1c894287b: 35 janitor tests OK, including the three new cargo-target-janitor: parse_fallback_scan, queue_claims and queue_task_states raise raw shape errors, not SafetyError — a malformed scan/row kills the janitor with no refused record (the #6812 invisible-death mode, second door) #6844 shape tests and the well-formed-rows control. Codex review completed with no findings.basic:checks sat queued ~18h with no runner pickup (fleet-wide stall); merged on local evidence under the standing criteria.The issue is satisfied by this merge; closing it is left to the next reconciliation pass so the merge is visible on the timeline first.