Skip to content

Commit 7447be4

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

9 files changed

Lines changed: 1249 additions & 55 deletions

File tree

docs/receipt-schema-audit.md

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

0 commit comments

Comments
 (0)