fix(issue-5): address phase 2a round 2 findings (F-C, F-D, R1, R4)
Both reviewers sustained the round 1 fixes and found four more. Each was reproduced here before being accepted; none were rebutted. F-C (claude, blocking) -- my own round 1 fix caused this. Making refused writes exit non-zero also classified an idempotent ACK as a refusal, and the wrapper finalises an approved job with `status --set completed` on a record that is already completed. It runs under `set -e`, so the *success* path aborted with exit 4 before its validation hook and its final stdout line. Reproduced at the shell. An idempotent result is now distinguishable from a refusal and exits 0; a genuine conflict still exits 4. F-D (claude) -- routing the operator write around update_status dropped the status vocabulary check. Under the rollback flag an invalid status was written to the record with exit 0, where before it was refused with exit 1 and the record left alone. The check now runs first, in both modes. R1 (codex, blocking) -- the previous fix compared identifiers only when they happened to be present, so evidence carrying none at all was still approved on the dispatch record's own flags. That is the self-assertion the check exists to replace. Every identifier must now be supplied and agree, and a report manifest must associate with the attempt being judged. Identifiers are read from a collector envelope beside the payload rather than from inside it: a native Herdr payload may not carry MAM fields, so one appearing there is either fabricated or a contract violation, and either way it is not evidence. R4 (codex) -- a sequence number orders nothing unless the compare and the write are atomic. Two flushes could both read the old value, the newer land first, and the slower overwrite it. Reproduced deterministically. The mirror now serialises read-compare-write under a per-job audit lock, taken outside the registry transaction so a slow mirror still cannot lengthen it, and each write uses its own temp file instead of a shared name. All eight new regressions fail against the pre-fix code. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UrDNJkHmm18uu7n7JwV5Qd
This commit is contained in:
1 parent
3b134e3358
commit
cb575fa851
6 files changed
+298
-48
No files matched your search
@@ -327,23 +327,43 @@ def _build_parser() -> argparse.ArgumentParser:
|
||||
|
||||
|
||||
def _operator_write(job_id: str, registry_dir: str, **fields) -> int:
|
||||
"""Apply an operator-authority write and report refusal on the exit status.
|
||||
"""Apply an operator-authority write and report a refusal on the exit status.
|
||||
|
||||
Before phase 2a this path could not be refused, so it always exited 0. Once
|
||||
the CAS boundary can decline a write, exiting 0 anyway would hide it -- which
|
||||
is exactly how a refused loop re-dispatch looked like success to the wrapper.
|
||||
is how a refused loop re-dispatch looked like success to the wrapper.
|
||||
|
||||
Two things this must NOT do, both learned the hard way:
|
||||
|
||||
* treat an idempotent ACK as a refusal. The wrapper finalises an approved
|
||||
job with `status --set completed` on a record that is already completed,
|
||||
and it runs under `set -e`, so reporting failure there aborts the success
|
||||
path before the validation hook and the final stdout line.
|
||||
* skip the status vocabulary check. That validation used to live in
|
||||
`update_status`; routing around it let an invalid status through, and in
|
||||
the rollback mode it was actually written to the record.
|
||||
"""
|
||||
from lib_py.delegation import engine # same package: no sibling hop (see __init__)
|
||||
|
||||
status = fields.get("status")
|
||||
if status is not None and status not in VALID_STATUSES:
|
||||
print("invalid status {!r}; expected one of {}".format(status, VALID_STATUSES),
|
||||
file=sys.stderr)
|
||||
return 1
|
||||
|
||||
if not engine.cas_enabled():
|
||||
mqtt_common.update_job_status(job_id, registry_dir, **fields)
|
||||
return 0
|
||||
|
||||
result = mqtt_common.transition_job_status(
|
||||
job_id, registry_dir, _source="registry_cli", **fields)
|
||||
if not result.committed:
|
||||
print("refused: {}".format(result.reason), file=sys.stderr)
|
||||
return 4
|
||||
return 0
|
||||
if result.committed:
|
||||
return 0
|
||||
if result.idempotent:
|
||||
# The requested state already holds. Nothing to do, and nothing wrong.
|
||||
return 0
|
||||
print("refused: {}".format(result.reason), file=sys.stderr)
|
||||
return 4
|
||||
|
||||
|
||||
def main(argv: Optional[List[str]] = None) -> int:
|
||||
|
||||
Reference in new issue
Block a user