|
| 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