Skip to content

Commit daae8de

Browse files
solomonneascodex
andcommitted
chore(receipts): audit and canonicalize receipt schemas
Co-Authored-By: Codex <codex@openai.com>
1 parent b08840f commit daae8de

9 files changed

Lines changed: 1256 additions & 56 deletions

File tree

docs/receipt-schema-audit.md

Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
# Receipt JSON SIEM-readiness audit (#506)
2+
3+
Audit date: 2026-07-26. Scope: the three sanctioned receipt families on `main`:
4+
work verification (`src/brigade/work_cmd/verification.py`), Brigade run receipts
5+
(`src/brigade/run_receipts.py` serializers plus `run.json` and run sidecars in
6+
`aboyeur.py` / `run_resume.py`), and outcome records (`src/brigade/outcome_cmd.py`).
7+
Out of scope: route-decision, runbook, tool-call, daily, skills, and other
8+
receipt producers.
9+
10+
Criteria:
11+
12+
1. **Deterministic field names**: stable string keys, no positional-only meaning.
13+
2. **Stable key ordering**: canonical sorted-key JSON on disk.
14+
3. **Append-only semantics**: new evidence is appended or written once. No silent
15+
rewrite of historical facts.
16+
4. **Schema version**: machine-readable `schema_version` (and/or documented
17+
`schema` string) for evolution.
18+
19+
Patch identity from **#485** is present on the audited `main`. Lane **#491**
20+
(telemetry projection) may add fields concurrently. Within a given
21+
`schema_version`, **additive fields only** are permitted. Consumers must ignore
22+
unknown keys.
23+
24+
Tag legend: `compliant`, `fixed-here`, `needs-follow-up-issue`.
25+
26+
## Writer-site inventory
27+
28+
### Work verification (`src/brigade/work_cmd/verification.py`)
29+
30+
| Function | Receipt | Behavior |
31+
| --- | --- | --- |
32+
| `_run_verify_commands` | `receipt.json` | Builds in-memory receipt. Disk write deferred to finalize |
33+
| `_finalize_verify_receipt` | `receipt.json`, `summary.md` | **Single** `helpers._write_json` for `receipt.json`. Markdown is best-effort. Retention pruning follows |
34+
| `_safe_finalize_verify_receipt` | `receipt.json` | Exception wrapper. Emergency write only when finalize itself raises |
35+
| `_write_reused_receipt` | `receipt.json` | New verify-run directory. Copies prior commands. Retention pruning follows |
36+
| `_prune_verify_runs` | verify-run directories | Deletes directories older than the newest 50, including their written receipts |
37+
| `_work_closeout_payload` | `closeout.json` | One write per closeout via `helpers._write_json` |
38+
| `verify_run` | (dispatch) | Entry point for `_run_verify_commands` |
39+
40+
### Run lifecycle `run.json`
41+
42+
| Module / function | Behavior |
43+
| --- | --- |
44+
| `aboyeur.record_run_start` | Initial `run.json` via `_run_payload` + `_write_json`. Detached and regular paths may run before the main loop writes the latest snapshot |
45+
| `aboyeur` run loop (`run`, `plan`, dispatch/synthesis paths) | **In-place** `run.json` rewrites on status transitions (`planning`, `dispatching`, `synthesizing`, terminal states) |
46+
| `aboyeur.record_run_termination` | Merge terminal failure/success into existing `run.json` |
47+
| `aboyeur.record_artifact_collection` | Merge `artifact_collection` block into `run.json` |
48+
| `aboyeur.record_dispatch_stage` | Merge dispatch stage metadata |
49+
| `aboyeur.record_result_processing` | Merge result-processing seat metadata |
50+
| `aboyeur.terminal_sigterm_handler` | SIGTERM path calls `record_run_termination` |
51+
| `run_resume._resume_locked` | Rewrites `run.json` after resume synthesis |
52+
| `runguard._recover_run_artifact` | Stale-lock recovery rewrites `run.json` via `localio.write_json` |
53+
| `cli/run.py` | Error/exit paths call `record_run_termination` |
54+
55+
Shared serializer: `aboyeur._run_payload` (schema + `schema_version`). Shared writer:
56+
`aboyeur._write_json` (`sort_keys=True`, atomic).
57+
58+
### Run sidecars (`.brigade/runs/<id>/`)
59+
60+
| Module / function | File | Builder |
61+
| --- | --- | --- |
62+
| `aboyeur.record_run_start` | `roster.json` | `aboyeur._roster_payload` |
63+
| `aboyeur` planning phase | `plan.json` | `receipt_schema.run_plan_document` |
64+
| `aboyeur` post-dispatch | `worker-results.json` | `receipt_schema.worker_results_document` |
65+
| `aboyeur` post-synthesis | `synthesis.json` | `receipt_schema.synthesis_document` |
66+
| `aboyeur.set_artifact_patch_ref` | `worker-results.json`, `synthesis.json` | Rewrites `ground_truth.patch_ref` on existing sidecars |
67+
| `run_resume._resume_locked` | `worker-results.json`, `synthesis.json` | Same builders after resume |
68+
69+
Entry serialization for worker rows: `run_receipts.worker_payload`,
70+
`run_receipts.assignment_payload`, `run_receipts.agent_result_payload`.
71+
72+
The run directory also contains support artifacts that are not receipts:
73+
74+
| Artifact | Classification |
75+
| --- | --- |
76+
| `pre-run-snapshot.json` | Run-guard input snapshot, not an event receipt |
77+
| `plan-attempts.json` | Planner retry trace, not the accepted `plan.json` receipt |
78+
| `read-only-enforcement.json` | Enforcement evidence sidecar, not a run-state receipt |
79+
| GraphTrail before/after/delta JSON | Code-graph evidence referenced by receipts, not a Brigade receipt type |
80+
81+
### Outcome (`src/brigade/outcome_cmd.py`)
82+
83+
| Function | Receipt | Behavior |
84+
| --- | --- | --- |
85+
| `append_records` | `memory/outcome/records.jsonl` | JSONL append, one sorted object per line |
86+
| `record` | (dispatch) | Builds `OutcomeRecord`, calls `append_records` |
87+
| `_record_payload` | row shape | Adds `schema_version` before append |
88+
| `reconcile` | `memory/outcome/decisions/<stamp>-<slug>.json` | One decision file per applied transition via `localio.write_json` |
89+
| `_decision_path` | decision filename | Second-resolution timestamp plus a lossy artifact slug can collide |
90+
91+
Readers: `outcome_cmd._read_run_receipt` (run.json for capture), `load_records`
92+
(legacy rows without `schema_version` accepted).
93+
94+
## Verification family
95+
96+
| Receipt type | Path | Names | Ordering | Append-only | Schema version | Tag |
97+
| --- | --- | --- | --- | --- | --- | --- |
98+
| Work verify run | `.brigade/work/verify-runs/<id>/receipt.json` | Stable snake_case, command objects use fixed keys and a fixed identity tuple | `helpers._write_json` (`sort_keys=True`) | Finalization now writes once, but `_prune_verify_runs` deletes receipts beyond the newest 50 | `schema_version: 2` | names **compliant**, ordering **compliant**, write-once **fixed-here**, retention **needs-follow-up-issue**, version **compliant** |
99+
| Work verify reuse | same layout | Copies prior `commands`, same key set and a fresh identity tuple | same | New directory per reuse, followed by the same retention pruning | `schema_version: 2` | names **compliant**, ordering **compliant**, append-only **needs-follow-up-issue**, version **compliant** |
100+
| Work closeout | `.brigade/work/closeouts/<id>/closeout.json` | Stable keys, nested session/verification summaries | `helpers._write_json` | Written once per closeout | `schema_version: 1` | names **compliant**, ordering **compliant**, append-only **compliant**, version **fixed-here** |
101+
102+
Notes:
103+
104+
- Verify `commands[].env` is a sorted list of `KEY=value` strings (**compliant**).
105+
- `digests.receipt_sha256` uses `localio.canonical_json_digest` (sorted keys)
106+
(**compliant**).
107+
- The 50-run retention cap is operationally useful, but deletion is not
108+
append-only evidence storage (**needs-follow-up-issue**).
109+
110+
## Brigade run family
111+
112+
| Receipt type | Path | Names | Ordering | Append-only | Schema version | Tag |
113+
| --- | --- | --- | --- | --- | --- | --- |
114+
| Run lifecycle | `.brigade/runs/<id>/run.json` | Stable keys, status values enumerated | `aboyeur._write_json` (`sort_keys=True`) | **In-place lifecycle mutation** across statuses | `schema` + `schema_version: 1` | names **compliant**, ordering **fixed-here**, append-only **needs-follow-up-issue**, version **fixed-here** |
115+
| Roster snapshot | `roster.json` | `schema` + roster fields | sorted writer | Write-once at run start | `schema` + `schema_version: 1` | names **compliant**, ordering **fixed-here**, append-only **compliant**, version **fixed-here** |
116+
| Run plan | `plan.json` | `schema` + `assignments` | sorted writer | Write-once per planning phase | `schema` + `schema_version: 1` | names **compliant**, ordering **fixed-here**, append-only **compliant**, version **fixed-here** |
117+
| Worker results | `worker-results.json` | `schema` + `results` from `run_receipts.py` | sorted writer | `run_resume._resume_locked` and `aboyeur.set_artifact_patch_ref` rewrite the sidecar | `schema` + `schema_version: 1` | names **compliant**, ordering **fixed-here**, append-only **needs-follow-up-issue**, version **fixed-here** |
118+
| Synthesis | `synthesis.json` | `schema` + orchestrator/result | sorted writer | `run_resume._resume_locked` and `aboyeur.set_artifact_patch_ref` rewrite the sidecar | `schema` + `schema_version: 1` | names **compliant**, ordering **fixed-here**, append-only **needs-follow-up-issue**, version **fixed-here** |
119+
120+
Notes:
121+
122+
- `run.json` omits explicit JSON `null` for absent `cwd` (**fixed-here**).
123+
- `record_run_termination` / `runguard._recover_run_artifact` merge into existing
124+
`run.json`. The latest-snapshot file supports `brigade runs watch` polling. Append-only lifecycle events
125+
deferred (**needs-follow-up-issue**).
126+
- Resume salvage and patch-ref binding both overwrite `worker-results.json` and
127+
`synthesis.json`. Preserving each attempt as a new sidecar is deferred
128+
(**needs-follow-up-issue**).
129+
- Patch identity from #485 uses verify `schema_version: 2`. Future #491 telemetry
130+
cross-references must remain additive within the version used by each receipt
131+
family.
132+
133+
### Null and absent-field evidence
134+
135+
These cases support the deterministic-names findings in the family tables:
136+
137+
| Shape | Policy |
138+
| --- | --- |
139+
| Verify `baseline_commit`, `tree_fingerprint`, `changes_patch_sha256` | Explicit `null` tuple when identity capture is unavailable in version 2 |
140+
| Verify `reused_from` | Omitted when no source run id is available |
141+
| Verify command `exit_code` | Explicit `null` means no child exit status exists |
142+
| Run `cwd` | Omitted when unavailable |
143+
| Roster agent `env`, model metadata | Fixed snapshot rows retain explicit `null` |
144+
| Synthesis `orchestrator` | Explicit `null` identifies direct-worker mode |
145+
| Work closeout `task`, `verification` | Explicit `null` records that no item was available |
146+
| Outcome `prev_digest` | Explicit `null` identifies the first chain row |
147+
148+
## Outcome family
149+
150+
| Receipt type | Path | Names | Ordering | Append-only | Schema version | Tag |
151+
| --- | --- | --- | --- | --- | --- | --- |
152+
| Outcome ledger row | `memory/outcome/records.jsonl` | Stable snake_case from `OutcomeRecord` | `json.dumps(..., sort_keys=True)` per line | JSONL append with digest chain. Concurrent writers can select the same predecessor | `schema_version: 1` | names **compliant**, ordering **compliant**, append-only **needs-follow-up-issue**, version **fixed-here** |
153+
| Reconcile decision | `memory/outcome/decisions/<stamp>-<slug>.json` | Stable keys, nested `score` | `localio.write_json` | Second-resolution, lossy-slug filenames can overwrite a prior decision | `schema_version: 1` | names **compliant**, ordering **compliant**, append-only **needs-follow-up-issue**, version **fixed-here** |
154+
| Status cache | `memory/outcome/status.json` | `version` + `artifacts` map | sorted write | Regenerated from decisions (derived cache) | `version: 1` (file format, not receipt `schema_version`) | names **compliant**, ordering **compliant**, append-only **compliant**, version **compliant** (documented derived cache) |
155+
156+
Notes:
157+
158+
- Legacy rows without `schema_version` still load via `_record_from_dict` and
159+
`_read_run_receipt` (**fixed-here** backward-compat tests).
160+
- `append_records` does not lock the last-digest read and append as one
161+
transaction (**needs-follow-up-issue**).
162+
163+
## Follow-up issues (draft titles)
164+
165+
1. **`receipts: replace mutable run.json snapshots with append-only lifecycle events`**
166+
2. **`receipts: preserve worker and synthesis sidecars across resume and patch-ref updates`**
167+
3. **`receipts: archive verification evidence before retention pruning`**
168+
4. **`outcome: make decision receipt filenames collision-safe and write-exclusive`**
169+
5. **`outcome: serialize digest-chain appends across concurrent captures`**
170+
171+
## Finding counts
172+
173+
Counts are tag occurrences across the four criterion findings for each receipt
174+
type. The work-verify append-only cell has both a fixed write-once defect and a
175+
remaining retention follow-up.
176+
177+
| Tag | Count |
178+
| --- | --- |
179+
| **compliant** | 24 |
180+
| **fixed-here** | 14 |
181+
| **needs-follow-up-issue** | 7 |
182+
183+
See [`receipt-schemas.md`](receipt-schemas.md) for field-level contracts and
184+
evolution rules.

0 commit comments

Comments
 (0)