From f3664dea9fab99f4186c5383c8b59c6545256a5d Mon Sep 17 00:00:00 2001 From: claude Date: Sun, 26 Jul 2026 08:28:10 +0000 Subject: [PATCH] docs(prd): drop policy_version (manifest is the policy); add engine field MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- docs/prds/prd-new-audit-event-schema.md | 41 +++++++++++++++++-------- 1 file changed, 29 insertions(+), 12 deletions(-) diff --git a/docs/prds/prd-new-audit-event-schema.md b/docs/prds/prd-new-audit-event-schema.md index f8b4c7b0..9b781ea2 100644 --- a/docs/prds/prd-new-audit-event-schema.md +++ b/docs/prds/prd-new-audit-event-schema.md @@ -21,7 +21,8 @@ emits into: 1. A **versioned envelope** — schema version, event id, event/observed timestamps, host-attributed identity (`bottle`/`bottled_agent`/ - `activation`), provenance (`manifest_digest`, `policy_version`), + `activation`), provenance (`manifest_digest` — the manifest is the + policy — and `engine` = bot-bottle version/SHA), host-observed `actor`/`action`/`resource`/`outcome`, correlation/ causation ids, a sensitivity class, a typed payload, and an explicit **trust boundary** between host-supplied and agent-claimed fields. @@ -152,11 +153,11 @@ stable top level: // ---- attribution + provenance (host-established) ---- "producer": "egress", // host component that emitted the event "host": "mac-studio-1", + "engine": "bot-bottle/0.1.0+abc1234", // bot-bottle version + git SHA of the enforcing host code "bottle": "amber-fox", // bottle (container/VM) identity "bottled_agent": "amber-fox-12", // bottled-agent slug from source-IP attribution (null for host-level events) "activation": "01J8Z...", // activation id: one run/session of the bottled agent (null if n/a) - "manifest_digest": "sha256:9f2…", // digest of the manifest the bottled agent is running (null if n/a) - "policy_version": "egress-2026-07-20", // policy/config version in force at decision time (null if n/a) + "manifest_digest": "sha256:9f2…", // digest of the manifest — which IS the policy (egress routes etc.); null if n/a // ---- semantics: host-observed facts of what happened ---- "actor": "bottled-agent:amber-fox-12", // who acted, as a host-attributed identity @@ -186,7 +187,7 @@ The **trust boundary is a single region, not a split.** Everything outside `untrusted` is trusted by construction — the host established it: schema and chain metadata, both timestamps, the attribution/provenance fields (source-IP → `bottled_agent`/`bottle`/`activation`, `manifest_digest`, -`policy_version`), the host-observed semantics +`engine`), the host-observed semantics (`actor`/`action`/`resource`/`outcome`), the host-assigned correlation ids, and the typed `payload`. `untrusted` is the **one** place anything an agent or a remote claimed may go (e.g. the agent's free-text `reason`). A reader @@ -209,12 +210,26 @@ the single writer durably appended it. Ordering and the chain use `(epoch, seq)`, never either wall clock. Both are host-set — a bottled agent never supplies a timestamp. +**The manifest *is* the policy.** bot-bottle has no separate policy +artifact — a bottled agent's egress routes and other constraints are +declared in its manifest (`bot_bottle/manifest/egress.py`), so +`manifest_digest` already pins the ruleset in force; there is no distinct +`policy_version`. Given a fixed manifest, the only other thing that can +change a decision's outcome is the enforcing code — captured by `engine` +(bot-bottle version + git SHA). So two `egress.decision` records with the +same `resource` but different `outcome` are explained by exactly one of: +different `manifest_digest` (the rules changed) or different `engine` (the +enforcer changed). Runtime operator overrides (a supervise `egress-allow`) +are themselves audit events, so the effective ruleset at any instant is +`manifest_digest` plus the logged, approved deltas — reconstructable from +the chain, not from a version stamp. + **Optionality.** `bottle`/`bottled_agent`/`activation`, `manifest_digest`, -`policy_version`, and `payload`/`untrusted` are `null`/absent for events -that have no such subject (a host-level `hostctl.*` or `audit.*` event has -no bottled agent). Absent ≠ empty: a reader distinguishes "no subject" from -"unknown". `id`, `type`, the chain fields, both timestamps, `producer`, -`host`, `actor`, `action`, `outcome`, and `sensitivity` are always present. +and `payload`/`untrusted` are `null`/absent for events that have no such +subject (a host-level `hostctl.*` or `audit.*` event has no bottled agent). +Absent ≠ empty: a reader distinguishes "no subject" from "unknown". `id`, +`type`, the chain fields, both timestamps, `producer`, `host`, `engine`, +`actor`, `action`, `outcome`, and `sensitivity` are always present. #### Trust provenance of every common field @@ -223,9 +238,9 @@ no bottled agent). Absent ≠ empty: a reader distinguishes "no subject" from | `v` `id` `type` `epoch` `seq` `segment` `prev` `hash` | trusted | the single writer | | `ts_event` | trusted | emitting host component (boundary) | | `ts_recorded` `ts_mono` | trusted | the single writer | -| `producer` `host` | trusted | the single writer | +| `producer` `host` `engine` | trusted | the single writer | | `bottle` `bottled_agent` `activation` | trusted | gateway source-IP → slug attribution | -| `manifest_digest` `policy_version` | trusted | control plane (what was in force) | +| `manifest_digest` | trusted | control plane (the manifest = the policy in force) | | `actor` `action` `resource` `outcome` | trusted | host component that observed/decided it | | `sensitivity` | trusted | registry default for `type`, overridable up (never down) by the producer | | `correlation_id` `causation_id` | trusted | the single writer (assigned as it threads the flow) | @@ -601,7 +616,9 @@ the committing agent. `canonical()`, chain hashing, and the single-writer journal appender in `bot_bottle/store/` (reusing `sha256_hex`); redaction wired to the existing `gateway/egress/dlp_detectors` (`scan_token_patterns` / - `redact_tokens`); unit tests for determinism, chain-break detection, + `redact_tokens`); embed the git SHA at build so `engine` is populated + (only `version = "0.1.0"` exists in `pyproject.toml` today — the build + must stamp the SHA); unit tests for determinism, chain-break detection, `epoch`/`seq` continuity across a simulated restart, and redaction of both a deny-listed key and a token-shaped value. 3. **SQLite index + `audit` CLI.** New `audit_events` migration (indexable