docs(prd): drop policy_version (manifest is the policy); add engine field
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>
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user