Skip to content

Rectify: canonical JSON producer/consumer registry ratchet - #4413

Merged
Trecek merged 6 commits into
developfrom
audit-impl-writes-authority-json-pretty-printed-but-the-veri/4406-2
Jul 29, 2026
Merged

Rectify: canonical JSON producer/consumer registry ratchet#4413
Trecek merged 6 commits into
developfrom
audit-impl-writes-authority-json-pretty-printed-but-the-veri/4406-2

Conversation

@Trecek

@Trecek Trecek commented Jul 29, 2026

Copy link
Copy Markdown
Collaborator

Summary

Part A adds a server-side MCP tool (write_audit_cycle_artifact) that performs construction, digest computation, dataclass validation, and canonical serialization entirely server-side for the four hash-bound audit-cycle artifacts — closing the gap where LLM-agent skills (audit-impl, make-plan) had no mechanical way to produce the byte-exact canonical JSON that the strict verifier (core/audit_cycle_verifier.py) requires. Part B adds the systemic guard: an AST-scan test ratchet, mirroring the existing write_versioned_json convention tests, that fails task test-all immediately if a future require_canonical=True consumer is added with no registered, verified producer — plus corrects a module docstring that had pointed developers at the wrong JSON-writing helper.

Individual Group Plans

Part A — new MCP tool write_audit_cycle_artifact

audit-impl and make-plan are markdown-driven LLM-agent skills. Their SKILL.md prose
instructs the agent to write four JSON artifacts — authority.json, inventory.json,
the PlanDispositionReport, and plan-association files — that a strict Python verifier
(core/audit_cycle_verifier.py for the first three, recipe/_cmd_rpc_guards.py:284-288
for the fourth) later reads via decode_versioned_json_bytes(..., require_canonical=True).
That call demands byte-exact canonical JSON (sorted keys, compact separators, no
whitespace). No SKILL.md names a serializer that produces such bytes; the one helper that
does (write_canonical_versioned_json, core/io.py:392-401) has zero call sites
anywhere in the codebase. The LLM naturally emits ordinary pretty-printed JSON, which is
rejected outright.

The root defect is not "wrong helper named" — it is that these four artifacts are the
only hash-bound, tamper-evident artifacts in the codebase for which no Python function
ever constructs or serializes an instance in production code.
Every dataclass in this
family (AuditCycleAuthority, PlanDispositionReport, their row/ref types) has a
.create() classmethod that computes its own content digest from semantic fields, and a
strict __post_init__ that re-derives and checks that digest — but grep -rn "AuditCycleAuthority(\|AuditCycleAuthority\.create(\|PlanDispositionReport(\|PlanDispositionReport\.create(" src/ returns zero matches. The only place these types are ever instantiated is
.from_dict(), on read. The "producer" is 100% LLM prose asking a language model to
hand-assemble byte-exact JSON and correctly compute SHA-256 domain-separated digests by
hand — a task with no mechanical verification available to the model before it writes.

The architectural fix is to give the LLM producer session a single, narrow, MCP-tool call
that performs construction, digest computation, dataclass validation, and canonical
serialization entirely server-side — removing both failure modes (bad JSON formatting
and bad digest arithmetic) from the token-by-token generation path, the same way
commit_files (server/tools/tools_git.py:634-660) already removes "the LLM must
correctly stage/commit/run pre-commit hooks" from the git-write path for these same
sessions. A closely related but architecturally invalid candidate — routing the write
through the existing run_python RPC mechanism (recipe/_cmd_rpc.py), as several other
skills already do for precision-sensitive writes — was investigated and rejected: run_python
is hard-gated to SessionType.ORCHESTRATOR/FLEET only (_require_orchestrator_or_higher,
server/_guards.py:59-79), and audit-impl/make-plan run as child SessionType.SKILL
sessions dispatched via run_skill from the implementation.yaml orchestrator recipe.
run_python is tag-invisible to a SKILL+HEADLESS session by default (its tags are
{"autoskillit", "kitchen", "kitchen-core"} — no headless tag) and, even if
AUTOSKILLIT_HEADLESS_AUTO_GATE=1 revealed the kitchen-core tag, the tool would still be
rejected by its own Python-layer gate because session_type() resolves to SKILL, not
ORCHESTRATOR/FLEET. A new MCP tool tagged headless (mirroring commit_files, which
carries no _require_enabled()/_require_orchestrator_or_higher() gate at all) is the only
mechanism in this codebase's existing tool-gating architecture that a SKILL+HEADLESS
session can reach directly and unconditionally.

Part B — systemic AST-scan ratchet

The underlying investigation (GitHub #4406) found that a strict Python verifier
(core/audit_cycle_verifier.py, recipe/_cmd_rpc_guards.py:274-278) requires byte-exact
canonical JSON (decode_versioned_json_bytes(..., require_canonical=True)) for four
hash-bound artifacts, while the LLM-agent producers of those artifacts (audit-impl,
make-plan SKILL.md prose) had no mechanical way to guarantee that byte shape. A prior task
fixed the four known instances by routing the write through a new server-side MCP tool. This
part adds the systemic guard: a structural test ratchet that makes a fifth, future
instance of this same bug class (a new require_canonical=True consumer with no registered,
verified producer) fail task test-all immediately, instead of surfacing only in
production the way #4406 did — plus corrects a module docstring that contributed to the
original gap by pointing developers at the wrong JSON-writing helper.

This mirrors an existing, working pattern already in this codebase:
tests/infra/test_schema_version_convention.py AST-scans src/autoskillit/ for every
atomic_write(path, json.dumps({...})) call site and requires it to either use
write_versioned_json or be in an explicit, curated allowlist; a companion file,
tests/infra/test_schema_read_convention.py, checks that every write_versioned_json
caller has matching read-side validation. No equivalent ratchet exists for the canonical
family
(grep -rn "require_canonical" tests/ returns zero hits before this part) — this
is the load-bearing gap this part closes, for the canonical family specifically, since that
is the family where drift silently breaks tamper-evidence rather than just schema-version
detection.

Closes #4406

Implementation Plan

Plan files:

  • /home/talon/projects/autoskillit-runs/remediation-20260728-224547-129022/.autoskillit/temp/rectify/rectify_audit_cycle_canonical_json_producer_2026-07-28_232500_part_a.md
  • /home/talon/projects/autoskillit-runs/remediation-20260728-224547-129022/.autoskillit/temp/rectify/rectify_audit_cycle_canonical_json_producer_2026-07-28_232500_part_b.md

🤖 Generated with Claude Code via AutoSkillit

Token Usage Summary

Step Model count uncached output cache_read peak_ctx turns cache_write time
investigate* sonnet 1 37.8k 29.2k 3.0M 143.1k 66 204.8k 5m 42s
rectify* sonnet 1 290 25.1k 3.8M 349.8k 194 49.2k 29m 33s
review_approach* sonnet 1 26.3k 15.7k 2.4M 91.5k 57 124.1k 3m 48s
dry_walkthrough* sonnet 2 75.2k 118.8k 10.6M 224.8k 171 320.8k 25m 23s
implement* sonnet 2 164.2k 208.2k 70.6M 519.8k 454 858.1k 40m 31s
assess* sonnet 1 53.2k 47.4k 18.3M 240.5k 224 402.3k 24m 58s
audit_impl* sonnet 2 130.8k 115.3k 26.0M 350.9k 320 360.5k 35m 44s
prepare_pr* sonnet 1 16.5k 10.2k 430.3k 91.8k 17 92.8k 1m 44s
compose_pr* sonnet 1 15.2k 6.4k 305.6k 64.4k 12 40.0k 1m 12s
Total 519.6k 576.2k 135.4M 519.8k 2.5M 2h 48m

* Step used a non-Anthropic provider; caching behavior may differ.

Token Efficiency

Step LoC Changed cache_read/LoC cache_write/LoC output/LoC
investigate 0
rectify 0
review_approach 0
dry_walkthrough 0
implement 1403 50289.5 611.6 148.4
assess 52 350978.9 7736.7 911.1
audit_impl 0
prepare_pr 0
compose_pr 0
Total 1455 93048.2 1685.6 396.0

Model Usage Breakdown

Model steps uncached output cache_read cache_write time
sonnet 9 519.6k 576.2k 135.4M 2.5M 2h 48m

Trecek and others added 6 commits July 29, 2026 00:19
audit-impl and make-plan SKILL.md prose instructed the LLM to hand-assemble
byte-exact canonical JSON and hand-compute SHA-256 domain-separated digests
for four hash-bound artifacts (authority.json, inventory.json,
PlanDispositionReport, plan-association files), all read back with
decode_versioned_json_bytes(..., require_canonical=True). No Python function
ever constructed these artifacts in production code — write_canonical_versioned_json
had zero call sites — so the LLM producer path had no mechanical verification
before writing, and ordinary pretty-printed JSON was rejected outright.

Adds write_audit_cycle_artifact, a narrow MCP tool (mirroring commit_files'
headless-reachable, gate-free registration) that performs construction,
digest computation via the existing .create() classmethods, dataclass
validation, and canonical serialization entirely server-side for all four
artifact kinds. Relocates the plan-association domain/keys/file-count
constants to core/types/_type_audit_cycle.py (IL-0) so the new write-side
tool and the existing read-side _resolve_plan_disposition share one
definition. Adds an exclusive=True write-once guard to atomic_write/
write_canonical_versioned_json to close a TOCTOU window between an
existence check and the write. Wires both SKILL.md files to call the new
tool by name instead of describing the artifact only by filename.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The new MCP tool's module was never imported in server/__init__.py, so
@mcp.tool() registration never ran and the tool was invisible to every
session. Import it, add the missing broad-except logger call (ARCH-003),
fix an import-sort violation in core/__init__.pyi, and update every
downstream ratchet (tool counts, doc counts, cascade maps, display
categories, layer-boundary allowlist, path-guard patterns, subpackage
file-count exemption, and the make-plan disposition-report contract
test) to reflect the tool's existence.
_SERVER_TOOL_MODULES in test_tool_params_matches_mcp_handler_signatures
enumerates server tool modules by hand rather than discovering them —
the new tools_audit_cycle.py module was missing, so its handler was
invisible to the TOOL_REGISTRY parity check.
…t B)

Adds tests/infra/test_canonical_json_producer_convention.py, an AST-scan
ratchet mirroring test_schema_version_convention.py: every
decode_versioned_json_bytes(require_canonical=True) consumer site in
src/autoskillit/ must be registered against a verified server-side producer
(write_audit_cycle_artifact) and a SKILL.md section naming it, closing the
gap that let #4406's canonical/non-canonical mismatch land undetected.

Also corrects core/io.py's module docstring, which recommended
write_versioned_json unconditionally with no carve-out for artifacts
consumed with require_canonical=True.
…failure

atomic_write's exclusive=True path claims path via O_CREAT|O_EXCL before the
try block. If the subsequent temp-file write/fsync/os.replace fails, the
except handler now also unlinks the placeholder at path (in addition to the
temp file), preventing a permanently poisoned path that would raise
FileExistsError on every future retry.
…der cleanup

mkstemp() ran outside the try/except, so a mkstemp failure (ENOSPC,
permission error) would leave the O_CREAT|O_EXCL placeholder behind,
permanently poisoning the path with FileExistsError on every retry.
@Trecek
Trecek added this pull request to the merge queue Jul 29, 2026
Merged via the queue into develop with commit 1bab316 Jul 29, 2026
3 checks passed
@Trecek
Trecek deleted the audit-impl-writes-authority-json-pretty-printed-but-the-veri/4406-2 branch July 29, 2026 15:22
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