PRD: canonical tamper-evident audit-event schema (#487) #495

Closed
didericis wants to merge 5 commits from didericis/prd-audit-event-schema AGit into main
Owner

closes #487

Highlights

  • Versioned AuditEvent envelope with a structural trusted-vs-untrusted field split (host-supplied attribution vs agent-claimed data — the distinction today's stores lack).
  • Canonical JSON serialization (sort_keys, tight separators) + a per-writer hash chain (sha256(prev || canonical(event))) for offline tamper-evidence.
  • Append-only JSONL journal as the source of truth, with a fully rebuildable SQLite index for local search — no paid platform, no egress.
  • Initial event registry: lifecycle.*, decision.*, egress.*, auth.*, forge.*.
  • Redaction rule enforced at the envelope boundary so a producer can't write a raw secret into the audit log.

Sequenced per the directive on this issue: lands immediately after the host controller (#468), which becomes the first producer (lifecycle.*); existing supervise (PRD 0013 AuditStore), egress, git-gate, and control-plane producers migrate behind the same envelope. This PR is PRD-only and stays Draft.

Consolidates the audit threads in #468 / #324 / #480 onto one contract.

closes #487 **Highlights** - Versioned `AuditEvent` envelope with a **structural** trusted-vs-untrusted field split (host-supplied attribution vs agent-claimed data — the distinction today's stores lack). - Canonical JSON serialization (`sort_keys`, tight separators) + a per-writer **hash chain** (`sha256(prev || canonical(event))`) for offline tamper-evidence. - Append-only **JSONL journal as the source of truth**, with a fully **rebuildable SQLite index** for local search — no paid platform, no egress. - Initial event registry: `lifecycle.*`, `decision.*`, `egress.*`, `auth.*`, `forge.*`. - Redaction rule enforced at the envelope boundary so a producer can't write a raw secret into the audit log. Sequenced per the directive on this issue: lands **immediately after the host controller (#468)**, which becomes the first producer (`lifecycle.*`); existing supervise (PRD 0013 `AuditStore`), egress, git-gate, and control-plane producers migrate behind the same envelope. This PR is PRD-only and stays Draft. Consolidates the audit threads in #468 / #324 / #480 onto one contract.
didericis added 1 commit 2026-07-26 03:12:56 -04:00
docs(prd): canonical tamper-evident audit-event schema (#487)
prd-number-check / require-numbered-prds (pull_request) Failing after 8s
tracker-policy-pr / check-pr (pull_request) Successful in 6s
bc42836327
Draft PRD for a unified, versioned audit-event envelope with a
trusted/untrusted field split, canonical JSON + per-writer hash chain,
an append-only JSONL journal as source of truth, a rebuildable SQLite
index for local query, and an initial event registry. Scheduled to land
immediately after the host controller (#468), which becomes its first
producer.

Refs #487

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
didericis changed title from docs(prd): canonical tamper-evident audit-event schema (#487) to PRD: canonical tamper-evident audit-event schema (#487) 2026-07-26 03:14:21 -04:00
didericis reviewed 2026-07-26 03:31:07 -04:00
@@ -0,0 +125,4 @@
"type": "egress.decision", // dotted event type from the registry
"seq": 1287, // per-writer monotonic sequence (gap-detectable)
"ts": {
"wall": "2026-07-26T18:22:04.113Z", // host wall-clock, RFC3339 UTC (TRUSTED)
Author
Owner

why not put this in trusted?

why not put this in trusted?
@@ -0,0 +126,4 @@
"seq": 1287, // per-writer monotonic sequence (gap-detectable)
"ts": {
"wall": "2026-07-26T18:22:04.113Z", // host wall-clock, RFC3339 UTC (TRUSTED)
"mono": 90142.55 // host monotonic seconds since writer start (ordering)
Author
Owner

How does this account for host controller restarts?

How does this account for host controller restarts?
@@ -0,0 +130,4 @@
},
"producer": "host-controller", // TRUSTED: which host component wrote this
"trusted": { // host-supplied, authoritative
"bottle": "amber-fox-12", // slug from source-IP attribution (may be null for host-level events)
Author
Owner

should be precise/label this a "bottled-agent"

should be precise/label this a "bottled-agent"
@@ -0,0 +197,4 @@
required `untrusted` keys so producers and the verifier agree on shape:
- **lifecycle.*** — `lifecycle.bottle_start`, `lifecycle.bottle_stop`,
`lifecycle.bottle_crash` (producer: host-controller, #468).
Author
Owner

annoying, but should be consistent: bottled_agent_start, bottled_agent_stop, bottled_agent_crash

annoying, but should be consistent: bottled_agent_start, bottled_agent_stop, bottled_agent_crash
@@ -0,0 +215,4 @@
The envelope constructor enforces a deny-list at write time: a field whose
key matches known credential shapes (`token`, `secret`, `password`,
`authorization`, `*_key`, JWT-shaped values) is rejected — the producer
Author
Owner

Would be nice if we use the same credential shape detectors we use on egress, but not sure whether it's worth it or if that would be brittle/concerns would diverge. Evaluate how reasonable that is.

Would be nice if we use the same credential shape detectors we use on egress, but not sure whether it's worth it or if that would be brittle/concerns would diverge. Evaluate how reasonable that is.
didericis marked this conversation as resolved
@@ -0,0 +247,4 @@
being up. Alternative: one chain *per producer* (independent `seq`/chain
head), merged at query time by `(wall, mono)`. Leaning single-writer for
v1 (simpler verification, matches the host controller owning lifecycle),
with per-producer chains noted as the scaling path. — feedback wanted.
Author
Owner

Single writer, definitely

Single writer, definitely
@@ -0,0 +259,4 @@
flag, so a producer bug can't drop an event entirely in the field.
- **Retention interaction with the chain.** When an old journal segment is
rotated out, the verifier must still trust the current head. Carry the
rotated-out head as the new segment's genesis `prev`? (Tracked with the
Author
Owner

Yes

Yes
didericis added 1 commit 2026-07-26 03:35:23 -04:00
docs(prd): address review #495 on audit-event schema
prd-number-check / require-numbered-prds (pull_request) Failing after 7s
tracker-policy-pr / check-pr (pull_request) Successful in 8s
60039f2eb3
- Move ts_wall/ts_mono and producer inside the trusted block (host-supplied).
- Rename subject to bottled_agent everywhere (field + lifecycle.bottled_agent_* leaves).
- Add explicit epoch (writer-boot) counter + chain-head carry for ordering across host-controller restarts.
- Commit to a single writer per host (host controller owns it).
- Reuse egress dlp_detectors (scan_token_patterns/redact_tokens) for redaction; exclude scan_entropy as brittle on structured audit values.
- Retention: carry rotated-out chain head as new segment genesis prev.
- Fold resolved points into design; trim open questions.

Refs #487

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
didericis force-pushed didericis/prd-audit-event-schema from bc42836327 to 60039f2eb3 2026-07-26 03:35:23 -04:00 Compare
didericis-claude reviewed 2026-07-26 03:35:47 -04:00
@@ -0,0 +197,4 @@
which is acceptable because the host controller already gates every
lifecycle transition; per-producer chains are noted only as a future
scaling path, not built now.
Collaborator

Done: lifecycle.bottled_agent_start / _stop / _crash, matching the bottled_agent field name.

Done: `lifecycle.bottled_agent_start` / `_stop` / `_crash`, matching the `bottled_agent` field name.
didericis-claude reviewed 2026-07-26 03:35:48 -04:00
@@ -0,0 +247,4 @@
`forge.pr_opened`.
New types are additive; adding one does not bump `v`. Removing or
re-typing a field bumps `v`.
Collaborator

Locked in as decided — single writer, host controller owns it. Removed the equivocation; per-producer chains are noted only as a future scaling path.

Locked in as decided — single writer, host controller owns it. Removed the equivocation; per-producer chains are noted only as a future scaling path.
didericis-claude reviewed 2026-07-26 03:35:49 -04:00
@@ -0,0 +259,4 @@
`*_key`) is refused — the producer must pass a reference (a token *id*
or `sha256` fingerprint), never the raw value. `auth.token_minted`
therefore records the token id and role, not the JWT. This is the
primary guard: it is cheap, deterministic, and catches the intended
Collaborator

Adopted — the rotated-out head becomes the new segment's genesis prev, so the verifier still trusts the current head across a rotation. Folded into the retention follow-up.

Adopted — the rotated-out head becomes the new segment's genesis `prev`, so the verifier still trusts the current head across a rotation. Folded into the retention follow-up.
didericis-claude reviewed 2026-07-26 03:35:51 -04:00
@@ -0,0 +215,4 @@
### Journal (source of truth) + SQLite index (rebuildable)
- **Journal:** one append-only JSONL file per host (path from `paths.py`,
Collaborator

Evaluated: reuse the deterministic detectors in gateway/egress/dlp_detectors.py (scan_token_patterns / redact_tokens, plus scan_known_secrets) — pure-Python and already the project's source of truth for credential shapes, so wire-egress and journal-redaction can't drift. Excluded scan_entropy: it's tuned for large streamed bodies and would false-positive on the short high-entropy values audit records legitimately carry (hashes, uuids, base64 ids), redacting the very fingerprints we want to keep. So: share the deterministic detectors, keep entropy egress-only.

Evaluated: reuse the deterministic detectors in `gateway/egress/dlp_detectors.py` (`scan_token_patterns` / `redact_tokens`, plus `scan_known_secrets`) — pure-Python and already the project's source of truth for credential shapes, so wire-egress and journal-redaction can't drift. **Excluded `scan_entropy`**: it's tuned for large streamed bodies and would false-positive on the short high-entropy values audit records legitimately carry (hashes, uuids, base64 ids), redacting the very fingerprints we want to keep. So: share the deterministic detectors, keep entropy egress-only.
didericis marked this conversation as resolved
didericis-claude reviewed 2026-07-26 03:35:52 -04:00
@@ -0,0 +126,4 @@
// --- chain / ordering (structural, host-owned) ---
"epoch": 7, // writer-boot counter, bumped once per host-controller (writer) start
"seq": 1287, // monotonic sequence within this epoch (gap-detectable)
Collaborator

Handled explicitly now. Ordering is (epoch, seq), not the clock: on restart the writer reads the last journal line, adopts its hash as the next prev (chain stays continuous), bumps epoch, and resets seq. ts_mono orders only within an epoch. See the new Single writer; ordering across restarts section.

Handled explicitly now. Ordering is `(epoch, seq)`, not the clock: on restart the writer reads the last journal line, adopts its `hash` as the next `prev` (chain stays continuous), bumps `epoch`, and resets `seq`. `ts_mono` orders only *within* an epoch. See the new **Single writer; ordering across restarts** section.
didericis marked this conversation as resolved
didericis-claude reviewed 2026-07-26 03:35:53 -04:00
@@ -0,0 +130,4 @@
"prev": "<hex>", // hash of the previous record in the chain ("" for genesis)
"hash": "<hex>", // sha256(prev + canonical(this event with hash=""))
// --- trusted: everything the *host* established; authoritative ---
Collaborator

Renamed to bottled_agent — both the trusted.bottled_agent field and the lifecycle.bottled_agent_* leaves, so one term for the subject everywhere.

Renamed to `bottled_agent` — both the `trusted.bottled_agent` field and the `lifecycle.bottled_agent_*` leaves, so one term for the subject everywhere.
didericis-claude reviewed 2026-07-26 03:35:54 -04:00
@@ -0,0 +125,4 @@
"type": "egress.decision", // dotted event type from the registry
// --- chain / ordering (structural, host-owned) ---
"epoch": 7, // writer-boot counter, bumped once per host-controller (writer) start
Collaborator

Agreed — it's host-supplied, so it doesn't belong outside trusted. Moved ts_wall/ts_mono and producer into the trusted block; only chain metadata (v/id/type/epoch/seq/prev/hash) stays at the top level.

Agreed — it's host-supplied, so it doesn't belong outside `trusted`. Moved `ts_wall`/`ts_mono` **and** `producer` into the `trusted` block; only chain metadata (`v/id/type/epoch/seq/prev/hash`) stays at the top level.
Author
Owner

Actually let's change this a bit: everything outside of "untrusted" is trusted, (including epoc, seq) so we should just remove the "trusted" subsection.

Actually let's change this a bit: everything outside of "untrusted" is trusted, (including epoc, seq) so we should just remove the "trusted" subsection.
Collaborator

Thanks — all seven addressed and pushed (head now 60039f2). Summary:

Review note Change
Host-controller restarts New epoch (writer-boot) counter + chain-head carry; order is (epoch, seq), not the clock. ts_mono is intra-epoch only.
Label the subject "bottled-agent" Renamed to bottled_agent everywhere (field + prose).
Why is ts_wall not in trusted? Moved ts_wall/ts_mono and producer into trusted; only chain metadata stays top-level.
Consistent lifecycle names lifecycle.bottled_agent_start / _stop / _crash.
Single writer Locked in as decided (host controller owns it).
Retention head-carry Adopted — rotated-out head becomes the next segment's genesis prev.
Reuse egress credential detectors Reuse dlp_detectors scan_token_patterns/redact_tokens/scan_known_secrets; exclude scan_entropy (brittle on short structured audit values).

Resolved points are folded into the design under a new Resolved in review (#495) block; two smaller open questions remain (inline-vs-index-time value scan, and where epoch is persisted). Still PRD-only / Draft.

Thanks — all seven addressed and pushed (head now `60039f2`). Summary: | Review note | Change | |---|---| | Host-controller restarts | New **epoch (writer-boot) counter** + chain-head carry; order is `(epoch, seq)`, not the clock. `ts_mono` is intra-epoch only. | | Label the subject "bottled-agent" | Renamed to `bottled_agent` everywhere (field + prose). | | Why is `ts_wall` not in `trusted`? | Moved `ts_wall`/`ts_mono` **and** `producer` into `trusted`; only chain metadata stays top-level. | | Consistent lifecycle names | `lifecycle.bottled_agent_start` / `_stop` / `_crash`. | | Single writer | Locked in as decided (host controller owns it). | | Retention head-carry | Adopted — rotated-out head becomes the next segment's genesis `prev`. | | Reuse egress credential detectors | Reuse `dlp_detectors` `scan_token_patterns`/`redact_tokens`/`scan_known_secrets`; **exclude `scan_entropy`** (brittle on short structured audit values). | Resolved points are folded into the design under a new **Resolved in review (#495)** block; two smaller open questions remain (inline-vs-index-time value scan, and where `epoch` is persisted). Still PRD-only / Draft.
didericis added 1 commit 2026-07-26 03:46:02 -04:00
docs(prd): flatten to one untrusted region + address CloudEvents/OTel export
prd-number-check / require-numbered-prds (pull_request) Failing after 7s
tracker-policy-pr / check-pr (pull_request) Successful in 19s
5a9428cc86
- Remove the trusted sub-block: everything outside untrusted (chain
  metadata, producer/host, bottled_agent, ts_*) is trusted by construction.
  A field is trusted unless deliberately placed under untrusted (#495).
- Add Export/interoperability section: the flattened envelope projects
  cleanly onto CloudEvents JSON (top-level scalars -> context/extension
  attributes, untrusted -> data) and the OpenTelemetry Logs data model
  (ts_wall -> Timestamp, trusted -> botbottle.* attributes, untrusted ->
  botbottle.untrusted.*). Attribution preserved structurally; integrity
  fields carried as data with verification always on the native journal.
  Satisfies #487's export/interop requirement. Export adapters = chunk 6.

Refs #487

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
didericis force-pushed didericis/prd-audit-event-schema from 60039f2eb3 to 5a9428cc86 2026-07-26 03:46:02 -04:00 Compare
didericis-claude reviewed 2026-07-26 03:46:32 -04:00
@@ -0,0 +125,4 @@
stable top level:
```
{
Collaborator

Done — removed the trusted sub-block. The envelope is now flat: v/id/type/epoch/seq/prev/hash/producer/host/bottled_agent/ts_wall/ts_mono all sit at the top level and are trusted by construction; untrusted is the one region for agent/remote-claimed data. The rule is now "a field is trusted unless it's deliberately placed under untrusted," and the construction API takes trusted fields positionally + a single untrusted mapping, so a producer can't accidentally emit a top-level field that looks authoritative but isn't. (Head 5a9428c.)

Done — removed the `trusted` sub-block. The envelope is now flat: `v/id/type/epoch/seq/prev/hash/producer/host/bottled_agent/ts_wall/ts_mono` all sit at the top level and are trusted **by construction**; `untrusted` is the one region for agent/remote-claimed data. The rule is now "a field is trusted unless it's deliberately placed under `untrusted`," and the construction API takes trusted fields positionally + a single `untrusted` mapping, so a producer can't accidentally emit a top-level field that looks authoritative but isn't. (Head `5a9428c`.)
Collaborator

RE export/interop (CloudEvents & OpenTelemetry Logs) — does the flatten conflict? No, it helps.

Good prompt — #487's Export/interoperability line ("map the envelope to the OTel Logs data model and/or a CloudEvents JSON envelope without losing integrity or attribution semantics") was under-addressed, so I added a dedicated Export / interoperability section. Analysis:

  • CloudEvents. Context attributes must be scalar simple types — a map can't be a context attribute — so a nested trusted block would have had to be flattened for CloudEvents anyway. The now-flat top level maps directly: idid, typetype, producer+hostsource, bottled_agentsubject, ts_walltime; the chain/integrity fields (epoch/seq/prev/hash/v) ride as extension attributes (scalars — legal); untrusteddata. Only mechanical transform: extension names must be lowercase-alnum, so bottled_agent/ts_mono/etc. get a botbottle-prefixed rename at export. Naming rule, not a conflict.
  • OpenTelemetry Logs. ts_wallTimestamp, typeevent.name, flat trusted fields → Attributes under botbottle.*, untrusted.*botbottle.untrusted.* (or Body). OTel attributes are a dotted map that carries the subtree fine.
  • Attribution preserved because the boundary is now structural: top-level → trusted context/attributes, untrusted stays one clearly-named region, so a downstream consumer still sees exactly which fields the agent claimed. Nothing agent-claimed is promoted to a trusted-looking slot.
  • Integrity — one deliberate caveat. hash/prev are computed over our canonical JSON, not the CloudEvents/OTel form. So the chain fields travel as data for reference, but tamper-evidence is always verified against the native journal — never re-derived from an exported record (whose key order / number formatting the exporter may change). Export is a lossless-for-attribution projection; verification stays on the source of truth.

Net: the flatten makes the mapping a field re-map rather than a reformat. Export adapters are broken out as impl chunk 6 (feeding #324's webhook delivery); this PRD just fixes the schema so they stay a re-map. All in 5a9428c.

**RE export/interop (CloudEvents & OpenTelemetry Logs) — does the flatten conflict? No, it helps.** Good prompt — #487's *Export/interoperability* line ("map the envelope to the OTel Logs data model and/or a CloudEvents JSON envelope without losing integrity or attribution semantics") was under-addressed, so I added a dedicated **Export / interoperability** section. Analysis: - **CloudEvents.** Context attributes must be *scalar* simple types — a map can't be a context attribute — so a nested `trusted` block would have had to be flattened for CloudEvents *anyway*. The now-flat top level maps directly: `id`→`id`, `type`→`type`, `producer`+`host`→`source`, `bottled_agent`→`subject`, `ts_wall`→`time`; the chain/integrity fields (`epoch/seq/prev/hash/v`) ride as **extension attributes** (scalars — legal); `untrusted` → `data`. Only mechanical transform: extension names must be lowercase-alnum, so `bottled_agent`/`ts_mono`/etc. get a `botbottle`-prefixed rename at export. Naming rule, not a conflict. - **OpenTelemetry Logs.** `ts_wall`→`Timestamp`, `type`→`event.name`, flat trusted fields → `Attributes` under `botbottle.*`, `untrusted.*` → `botbottle.untrusted.*` (or `Body`). OTel attributes are a dotted map that carries the subtree fine. - **Attribution preserved** *because* the boundary is now structural: top-level → trusted context/attributes, `untrusted` stays one clearly-named region, so a downstream consumer still sees exactly which fields the agent claimed. Nothing agent-claimed is promoted to a trusted-looking slot. - **Integrity — one deliberate caveat.** `hash`/`prev` are computed over *our* canonical JSON, not the CloudEvents/OTel form. So the chain fields travel *as data* for reference, but tamper-evidence is always verified against the **native journal** — never re-derived from an exported record (whose key order / number formatting the exporter may change). Export is a lossless-for-attribution *projection*; verification stays on the source of truth. Net: the flatten makes the mapping a field re-map rather than a reformat. Export adapters are broken out as impl chunk 6 (feeding #324's webhook delivery); this PRD just fixes the schema so they stay a re-map. All in `5a9428c`.
didericis added 1 commit 2026-07-26 04:08:27 -04:00
docs(prd): complete audit-event contract to #487 acceptance checklist
prd-number-check / require-numbered-prds (pull_request) Failing after 10s
tracker-policy-pr / check-pr (pull_request) Successful in 11s
88b82a169e
Expand the PRD from a schema sketch to the full contract the issue mandates
(issue is spec-only: 'defines the contract; implementation may be split
into follow-up PRs'):

- Envelope: add observed vs event timestamps, bottle/activation ids,
  manifest_digest + policy_version, actor/action/resource/outcome,
  correlation_id/causation_id, sensitivity class, typed payload, segment id.
- Add a per-field trust-provenance table (trusted vs claimed for every
  common field); per-type trusted/claimed in the registry.
- Canonicalization: normative, reproducible hash-chain test vectors;
  idempotency (id key, UPSERT), ordering guarantees, and behavior across
  rotation/restart/import/truncation (truncated-tail vs gap).
- Storage: indexable fields + local audit query/verify/rebuild/import CLI.
- Registry: cover all mandated groups incl hostctl.*, egress
  request/decision/cutoff/anomaly, commit.signed (#480), auth/authz, and
  audit.* self-events; schema-evolution + backward-compatible reader rules.
- Export: #324 delivery contract (payload, (epoch,seq) cursor, dedup,
  backpressure, retention ordering); #480 mapping preserving its
  byte-to-activation-key guarantee.
- No raw prompt/response/body capture by default.
- Add an acceptance-criteria coverage table mapping each #487 checkbox to a
  section.

Refs #487

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
didericis force-pushed didericis/prd-audit-event-schema from 5a9428cc86 to 88b82a169e 2026-07-26 04:08:27 -04:00 Compare
Collaborator

Filled the gaps against #487's acceptance checklist (head 88b82a1). The issue is spec-only ("defines the contract; implementation may be split into follow-up PRs"), so this PR is now the complete decision record — no wiring needed here; the wiring is #468/#324/#480.

What I added:

  • Envelope — observed-vs-event timestamps, bottle/activation ids (alongside bottled_agent), manifest_digest + policy_version, host-observed actor/action/resource/outcome, correlation_id/causation_id, a sensitivity class, typed payload, and segment id. Plus a per-field trust-provenance table.
  • Canonicalizationnormative, reproducible hash-chain test vectors (real sha256s, incl. a tamper case), idempotency (id key + UPSERT), ordering guarantees, and behavior across rotation/restart/import/truncation (truncated-tail vs gap).
  • Storage/query — indexable fields + a local audit query/verify/rebuild/import CLI surface.
  • Registry — all mandated groups: hostctl.*, egress request/decision/cutoff/anomaly, commit.signed (#480), auth/authz, and audit.* self-events (verify/truncation/export failure); each with trusted-vs-claimed fields, default sensitivity, redaction, correlation. Plus schema-evolution + backward-compatible reader rules.
  • Export — the #324 delivery contract (native payload, (epoch,seq) cursor, dedup on id, backpressure via lagging cursor, retention-must-not-prune-ahead-of-cursor), and the #480 mapping that references (doesn't replace) its byte-to-activation-key signature.
  • No raw prompt/response/body capture by default.
  • An acceptance-criteria coverage table mapping each #487 checkbox to a section, flagging the two items that are specified here, implemented later (test-vector fixtures + audit CLI in chunks 2-3, #324 outbox in chunk 6) — which the issue explicitly permits.

I believe this now satisfies the full checklist. Since it's spec-only, my suggestion: merge with Closes #487 when you're happy (it stays Status: Draft — PRD-only — and the implementation lands as the follow-up chunks). Happy to trim scope to a leaner v1 with named follow-up decision records instead, if you'd prefer.

**Filled the gaps against #487's acceptance checklist** (head `88b82a1`). The issue is spec-only ("defines the contract; implementation may be split into follow-up PRs"), so this PR is now the complete decision record — no wiring needed here; the wiring is #468/#324/#480. What I added: - **Envelope** — observed-vs-event timestamps, `bottle`/`activation` ids (alongside `bottled_agent`), `manifest_digest` + `policy_version`, host-observed `actor`/`action`/`resource`/`outcome`, `correlation_id`/`causation_id`, a `sensitivity` class, typed `payload`, and `segment` id. Plus a **per-field trust-provenance table**. - **Canonicalization** — **normative, reproducible hash-chain test vectors** (real sha256s, incl. a tamper case), idempotency (`id` key + `UPSERT`), ordering guarantees, and behavior across **rotation/restart/import/truncation** (`truncated-tail` vs `gap`). - **Storage/query** — indexable fields + a local `audit query`/`verify`/`rebuild`/`import` CLI surface. - **Registry** — all mandated groups: `hostctl.*`, egress `request`/`decision`/`cutoff`/`anomaly`, `commit.signed` (#480), auth/authz, and `audit.*` self-events (verify/truncation/export failure); each with trusted-vs-claimed fields, default sensitivity, redaction, correlation. Plus **schema-evolution + backward-compatible reader** rules. - **Export** — the **#324 delivery contract** (native payload, `(epoch,seq)` cursor, dedup on `id`, backpressure via lagging cursor, retention-must-not-prune-ahead-of-cursor), and the **#480 mapping** that references (doesn't replace) its byte-to-activation-key signature. - **No raw prompt/response/body capture by default.** - An **acceptance-criteria coverage table** mapping each #487 checkbox to a section, flagging the two items that are *specified here, implemented later* (test-vector fixtures + `audit` CLI in chunks 2-3, #324 outbox in chunk 6) — which the issue explicitly permits. I believe this now satisfies the full checklist. Since it's spec-only, my suggestion: merge with `Closes #487` when you're happy (it stays `Status: Draft` — PRD-only — and the implementation lands as the follow-up chunks). Happy to trim scope to a leaner v1 with named follow-up decision records instead, if you'd prefer.
didericis added 1 commit 2026-07-26 04:28:23 -04:00
docs(prd): drop policy_version (manifest is the policy); add engine field
prd-number-check / require-numbered-prds (pull_request) Failing after 6s
tracker-policy-pr / check-pr (pull_request) Successful in 5s
f3664dea9f
Per review: bot-bottle has no separate policy artifact — a bottled agent's
egress routes etc. are declared in its manifest (manifest/egress.py), so
manifest_digest already pins the policy in force. Remove the redundant
policy_version. Given a fixed manifest, the only other axis that changes an
outcome is the enforcing code, so add 'engine' (bot-bottle version + git
SHA) as a trusted field. Runtime operator overrides (supervise egress-allow)
are themselves audit events, so effective policy = manifest_digest + logged
deltas, reconstructable from the chain. Note the build must stamp the git
SHA (only version=0.1.0 exists today).

Refs #487

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
didericis force-pushed didericis/prd-audit-event-schema from 88b82a169e to f3664dea9f 2026-07-26 04:28:23 -04:00 Compare
Collaborator

Dropped policy_version, added engine (head f3664de).

You're right — bot-bottle has no separate policy artifact; the egress routes and other constraints are declared in the manifest (manifest/egress.py), so manifest_digest already pins the policy in force. A separate policy_version was redundant, so it's removed.

Given a fixed manifest, the only other axis that can change a decision's outcome is the enforcing code — so I added engine (bot-bottle version + git SHA) as a trusted field. Now two same-resource decisions with different outcome are explained by exactly one of: different manifest_digest (rules changed) or different engine (enforcer changed). Runtime operator overrides (a supervise egress-allow) are themselves audit events, so effective policy at any instant = manifest_digest + logged approved deltas — reconstructable from the chain, no version stamp needed.

Caveat: engine needs the build to stamp the git SHA — only version = "0.1.0" exists in pyproject.toml today — noted in impl chunk 2.

**Dropped `policy_version`, added `engine`** (head `f3664de`). You're right — bot-bottle has no separate policy artifact; the egress routes and other constraints are declared in the **manifest** (`manifest/egress.py`), so `manifest_digest` already pins the policy in force. A separate `policy_version` was redundant, so it's removed. Given a fixed manifest, the only other axis that can change a decision's outcome is the enforcing **code** — so I added `engine` (bot-bottle version + git SHA) as a trusted field. Now two same-`resource` decisions with different `outcome` are explained by exactly one of: different `manifest_digest` (rules changed) or different `engine` (enforcer changed). Runtime operator overrides (a supervise `egress-allow`) are themselves audit events, so effective policy at any instant = `manifest_digest` + logged approved deltas — reconstructable from the chain, no version stamp needed. Caveat: `engine` needs the build to stamp the git SHA — only `version = "0.1.0"` exists in `pyproject.toml` today — noted in impl chunk 2.
Collaborator

Fixed in #503 (commit 4cb7bbb). This PR was created from a synthetic refs/pull/495/head with no writable source branch, so Gitea rejects direct updates to it. #503 preserves this PR verbatim and adds the durable deduplication, imported-chain ordering/cursor, and chain-genesis corrections from review.

Fixed in #503 (commit `4cb7bbb`). This PR was created from a synthetic `refs/pull/495/head` with no writable source branch, so Gitea rejects direct updates to it. #503 preserves this PR verbatim and adds the durable deduplication, imported-chain ordering/cursor, and chain-genesis corrections from review.
didericis-codex closed this pull request 2026-07-26 13:40:16 -04:00
didericis reopened this pull request 2026-07-26 13:44:02 -04:00
Collaborator

Replaced by #504, which is backed by the normal branch didericis/prd-audit-event-schema and includes the review fixes.

Replaced by #504, which is backed by the normal branch `didericis/prd-audit-event-schema` and includes the review fixes.
didericis-codex closed this pull request 2026-07-26 13:50:18 -04:00
Some optional checks failed
prd-number-check / require-numbered-prds (pull_request) Failing after 6s
tracker-policy-pr / check-pr (pull_request) Successful in 5s
Required
Details

Pull request closed

Sign in to join this conversation.