You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Guardian state store (bun:sqlite): ingress/client registry + optional restart persistence
As the guardian grows from "HMAC channels only" to multiple ingress surfaces (HMAC channels + OpenCode-proxy #429 + MCP + A2A #432), its per-ingress configuration and runtime state outgrow env vars + in-memory Maps. Add a small guardian-local SQLite DB (bun:sqlite — built into Bun, no new dependency) for the things that genuinely benefit from a table or from surviving a restart. Keep ephemeral hot-path state in memory.
Restart resilience. Ownership maps and the A2A task store are in-memory; a guardian restart orphans sessions (today mitigated by broadcasting a synthetic session.error). A durable store removes the orphan window.
Durable tasks/get across restart; v1 can stay in-memory
Audit log (audit.ts)
JSONL file
⚖️ optional
Queryable/rotatable; file is fine for v1
Nonce/replay store (replay.ts)
in-mem Map, 50k cap, 5-min window
❌ no
Ephemeral by design; a 5-min window needs no persistence; per-request disk I/O for zero security gain
Rate-limit buckets (rate-limit.ts)
in-mem Map, 10k cap
❌ no
Ephemeral counters; same reasoning
Principle: persist what benefits from durability or scales as a table (registry, ownership, tasks, audit). Keep short-window ephemeral state (nonce, rate) in memory — moving it to disk adds hot-path I/O without benefit.
Design
bun:sqlite (import { Database } from "bun:sqlite"), WAL mode, one DB file under the guardian's data dir (data/guardian/state.db), 0600. No new dependency (Bun built-in — satisfies the no-dep-duplicates-a-Bun-built-in rule).
A thin core/guardian/src/store.ts with typed accessors (no ORM, no abstraction layer — per the zero-abstraction policy). Schema migrations are a simple PRAGMA user_version step on boot.
Phase 1 — ingress/client registry only. One ingress table (id, kind, token_hash, enabled, protocols, persona, rate_policy, created_at). The admin UI manages rows; the guardian reads them at request time (cached, short TTL). This alone delivers the "growing ingress list" value and trims env vars.
Phase 2 (optional) — ownership + A2A task persistence. Back ownership.ts and the A2A task store with SQLite tables, keeping the same TTL/eviction discipline (now durable). Gated on whether the orphan-on-restart window actually bites in practice.
Never SQLite-back the nonce or rate-limit stores.
Security invariants
DB is guardian-private (data/guardian/, 0600), never mounted elsewhere; tokens stored hashed (never plaintext), consistent with the secret-file model.
No change to ingress posture: registry rows only describe ingress; they don't grant network exposure (still loopback-default + OP_BIND_ADDRESS opt-in).
Reads on the request hot path must be cached (short TTL) so the registry lookup isn't a per-request disk hit.
Acceptance criteria
bun:sqlite-backed core/guardian/src/store.ts; no new npm dependency added to the guardian.
Phase 1: an ingress registry table + admin CRUD; per-ingress token (hashed), enabled flag, protocols, persona, rate policy read from the DB (cached) instead of new env vars.
Nonce + rate-limit stores remain in-memory (explicitly NOT migrated).
DB file is data/guardian/state.db, 0600, guardian-private; tokens hashed at rest.
cd core/guardian && bun test passes (store accessors + cache + hot-path-no-disk-on-nonce covered).
Forward-looking (Guardian: mTLS adapter auth + keep authn/authz seams generic #435): model the row as a generic Principal (machine or human), not "channel" specifically, so the future true auth/authz layer (OAuth/OIDC + multi-user) can add a users/identities + roles/policy table here (or federate to an external IdP) without reshaping the schema.
🤝 Spun out of the gateway/ingress evaluation (2026-06-05): bun:sqlite is zero-dep; persist the registry (+ optionally ownership/tasks), keep ephemeral nonce/rate in memory. Effort: S (Phase 1) / M (with Phase 2 persistence).
Guardian state store (
bun:sqlite): ingress/client registry + optional restart persistenceAs the guardian grows from "HMAC channels only" to multiple ingress surfaces (HMAC channels + OpenCode-proxy #429 + MCP + A2A #432), its per-ingress configuration and runtime state outgrow env vars + in-memory Maps. Add a small guardian-local SQLite DB (
bun:sqlite— built into Bun, no new dependency) for the things that genuinely benefit from a table or from surviving a restart. Keep ephemeral hot-path state in memory.Motivation
CHANNEL_*_SECRET_FILE,OP_GUARDIAN_*_TOKEN, …). That surface balloons as ingress options multiply — the opposite of Collapse port + bind-address pairs (introduce OP_BIND_ADDRESS global) #395's env-reduction goal. A small registry table scales better and is queryable by the admin UI.session.error). A durable store removes the orphan window.What goes in SQLite (and what does NOT)
ownership.ts)InMemoryTaskStoretasks/getacross restart; v1 can stay in-memoryaudit.ts)replay.ts)rate-limit.ts)Principle: persist what benefits from durability or scales as a table (registry, ownership, tasks, audit). Keep short-window ephemeral state (nonce, rate) in memory — moving it to disk adds hot-path I/O without benefit.
Design
bun:sqlite(import { Database } from "bun:sqlite"), WAL mode, one DB file under the guardian's data dir (data/guardian/state.db),0600. No new dependency (Bun built-in — satisfies the no-dep-duplicates-a-Bun-built-in rule).core/guardian/src/store.tswith typed accessors (no ORM, no abstraction layer — per the zero-abstraction policy). Schema migrations are a simplePRAGMA user_versionstep on boot.ingresstable (id, kind, token_hash, enabled, protocols, persona, rate_policy, created_at). The admin UI manages rows; the guardian reads them at request time (cached, short TTL). This alone delivers the "growing ingress list" value and trims env vars.channelId:channelSecret(the Basic-auth credential a channel/client presents). The guardian parses the inboundAuthorizationheader itself (it's our Bun server, not bound to OpenCode's single-password model), validates againsttoken_hash, and resolves the row → aPrincipalwith its rules (protocols, persona, rate). The Basic creds are the identity — per-channel principal over standard Basic/Bearer transport. ReplacesCHANNEL_*_SECRET_FILE. (End-user-within-a-channel identity is a separate, optional metadata concern — see Guardian: opt-in moderating front door for direct OpenCode clients #429/Channel adapters become official-OpenCode-SDK clients (X-to-OpenCode packages); retire channels-sdk transport #434 — not a registry credential.)ownership.tsand the A2A task store with SQLite tables, keeping the same TTL/eviction discipline (now durable). Gated on whether the orphan-on-restart window actually bites in practice.Security invariants
data/guardian/,0600), never mounted elsewhere; tokens stored hashed (never plaintext), consistent with the secret-file model.OP_BIND_ADDRESSopt-in).Acceptance criteria
bun:sqlite-backedcore/guardian/src/store.ts; no new npm dependency added to the guardian.ingressregistry table + admin CRUD; per-ingress token (hashed), enabled flag, protocols, persona, rate policy read from the DB (cached) instead of new env vars.data/guardian/state.db,0600, guardian-private; tokens hashed at rest.cd core/guardian && bun testpasses (store accessors + cache + hot-path-no-disk-on-nonce covered).Relationships
mcp/a2a/direct) and the A2A task store can later be SQLite-backed.Principal(machine or human), not "channel" specifically, so the future true auth/authz layer (OAuth/OIDC + multi-user) can add a users/identities + roles/policy table here (or federate to an external IdP) without reshaping the schema.🤝 Spun out of the gateway/ingress evaluation (2026-06-05):
bun:sqliteis zero-dep; persist the registry (+ optionally ownership/tasks), keep ephemeral nonce/rate in memory. Effort: S (Phase 1) / M (with Phase 2 persistence).