Persist supervisor grants as active manifest versions #433

Open
opened 2026-07-20 15:05:29 -04:00 by didericis · 0 comments
Owner

Problem

Supervisor grants currently leave no trace in the manifest. bot_bottle/supervise.py says so outright:

For 0013 the supervisor's approval handlers are deliberately no-ops: on approval the audit log is written and the response file is delivered to the agent, but no host-side config change happens.

So today an approved egress-allow writes an AuditEntry (with a rendered diff) to supervise_audit_entries and hands the agent back {status, notes} — and that's it. The grant is real at runtime but invisible to the manifest. The declarative bottle definition under bottles/*.md still describes a bottle that never received the grant.

That gives us two problems:

  1. No durable record of the effective policy. The audit table holds a sequence of diffs, not a materialized state. Answering "what is this bottle actually allowed to reach right now?" means replaying diffs by hand.
  2. Grants don't survive. Recreate the bottle from the manifest and every operator approval is gone, with nothing to tell you what was lost.

Proposal

Every grant that goes through the supervisor should translate into a modification of the active manifest, appended as a new version — an active_manifest_versions table in the existing SQLite store.

Sketch, following the AuditStore / TableMigrations conventions in bot_bottle/audit_store.py:

CREATE TABLE IF NOT EXISTS active_manifest_versions (
    id              INTEGER PRIMARY KEY AUTOINCREMENT,
    bottle_slug     TEXT NOT NULL,
    version         INTEGER NOT NULL,
    created_at      TEXT NOT NULL,
    component       TEXT NOT NULL,   -- egress, git-gate, ...
    proposal_id     TEXT,            -- NULL for the baseline version
    content         TEXT NOT NULL,   -- fully resolved manifest at this version
    content_sha256  TEXT NOT NULL,
    parent_sha256   TEXT             -- NULL for the baseline
);

Append-only, one row per grant, version monotonic per bottle_slug. Version 1 is the baseline captured at bottle start — the resolved manifest after extends: chains (manifest_loader.load_bottle_chain_from_dir). Each approved proposal appends the next. parent_sha256 chains versions so a tampered or skipped row is detectable, and sha256_hex already exists in supervise.py.

Rough scope:

  • ManifestVersionStore alongside AuditStore, same DbStore + TableMigrations pattern
  • Capture the baseline version at bottle start
  • Append a version on each approved/modified proposal, linked to proposal_id
  • Read path: "current active manifest for slug"
  • CLI surface to show current version and diff any two versions

Open design question

Which artifact is the source of truth?

The manifest is human-authored per-file Markdown with extends: chains resolved at load. If grants accumulate in SQLite, the resolved runtime manifest and the checked-in .md files can disagree, and it isn't obvious which one wins on next start. Two directions:

  • (a) DB authoritative at runtime. .md files are the declarative baseline; the DB holds accumulated deltas layered on top at start. Grants persist across recreates for free, but the checked-in manifest quietly stops describing the running system — which is the thing we were trying to fix.
  • (b) DB as an append-only log, .md stays authoritative. Versions are a record and a staging area; a bot-bottle manifest promote-style command writes accepted grants back into the Markdown so they land in review. Grants don't silently persist, but the manifest stays the real declaration and drift is visible in git.

I lean (b) — it keeps the manifest the single declarative source and puts grant persistence behind an explicit human step, which fits how the supervisor already works. But (a) is a better story for long-lived bottles where re-approving after every recreate is real friction. Worth settling before implementation, since it decides whether this table is authoritative state or a log.

Related

  • #323 — moving done-signal relay and op-log from filesystem to SQLite (same direction)
  • #341 — show complete manifest on preflight (wants the same "fully resolved manifest" primitive)
  • #324 — provenance stream
## Problem Supervisor grants currently leave no trace in the manifest. `bot_bottle/supervise.py` says so outright: > For 0013 the supervisor's approval handlers are deliberately no-ops: on approval the audit log is written and the response file is delivered to the agent, but no host-side config change happens. So today an approved `egress-allow` writes an `AuditEntry` (with a rendered diff) to `supervise_audit_entries` and hands the agent back `{status, notes}` — and that's it. The grant is real at runtime but invisible to the manifest. The declarative bottle definition under `bottles/*.md` still describes a bottle that never received the grant. That gives us two problems: 1. **No durable record of the effective policy.** The audit table holds a sequence of diffs, not a materialized state. Answering "what is this bottle actually allowed to reach right now?" means replaying diffs by hand. 2. **Grants don't survive.** Recreate the bottle from the manifest and every operator approval is gone, with nothing to tell you what was lost. ## Proposal Every grant that goes through the supervisor should translate into a modification of the active manifest, appended as a new version — an `active_manifest_versions` table in the existing SQLite store. Sketch, following the `AuditStore` / `TableMigrations` conventions in `bot_bottle/audit_store.py`: ```sql CREATE TABLE IF NOT EXISTS active_manifest_versions ( id INTEGER PRIMARY KEY AUTOINCREMENT, bottle_slug TEXT NOT NULL, version INTEGER NOT NULL, created_at TEXT NOT NULL, component TEXT NOT NULL, -- egress, git-gate, ... proposal_id TEXT, -- NULL for the baseline version content TEXT NOT NULL, -- fully resolved manifest at this version content_sha256 TEXT NOT NULL, parent_sha256 TEXT -- NULL for the baseline ); ``` Append-only, one row per grant, `version` monotonic per `bottle_slug`. Version 1 is the baseline captured at bottle start — the resolved manifest after `extends:` chains (`manifest_loader.load_bottle_chain_from_dir`). Each approved proposal appends the next. `parent_sha256` chains versions so a tampered or skipped row is detectable, and `sha256_hex` already exists in `supervise.py`. Rough scope: - [ ] `ManifestVersionStore` alongside `AuditStore`, same `DbStore` + `TableMigrations` pattern - [ ] Capture the baseline version at bottle start - [ ] Append a version on each approved/modified proposal, linked to `proposal_id` - [ ] Read path: "current active manifest for slug" - [ ] CLI surface to show current version and diff any two versions ## Open design question **Which artifact is the source of truth?** The manifest is human-authored per-file Markdown with `extends:` chains resolved at load. If grants accumulate in SQLite, the resolved runtime manifest and the checked-in `.md` files can disagree, and it isn't obvious which one wins on next start. Two directions: - **(a) DB authoritative at runtime.** `.md` files are the declarative baseline; the DB holds accumulated deltas layered on top at start. Grants persist across recreates for free, but the checked-in manifest quietly stops describing the running system — which is the thing we were trying to fix. - **(b) DB as an append-only log, `.md` stays authoritative.** Versions are a record and a staging area; a `bot-bottle manifest promote`-style command writes accepted grants back into the Markdown so they land in review. Grants don't silently persist, but the manifest stays the real declaration and drift is visible in git. I lean **(b)** — it keeps the manifest the single declarative source and puts grant persistence behind an explicit human step, which fits how the supervisor already works. But (a) is a better story for long-lived bottles where re-approving after every recreate is real friction. Worth settling before implementation, since it decides whether this table is authoritative state or a log. ## Related - #323 — moving done-signal relay and op-log from filesystem to SQLite (same direction) - #341 — show complete manifest on preflight (wants the same "fully resolved manifest" primitive) - #324 — provenance stream
gitea-actions bot added the Status/Needs Triage label 2026-07-20 15:05:34 -04:00
didericis added the Kind/Feature label 2026-07-20 15:05:46 -04:00
didericis added
Priority
High
2
and removed Status/Needs Triage labels 2026-07-20 23:44:27 -04:00
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: didericis/bot-bottle#433