Pillar 6 · Access control (the valves)

Updated · View the entry on sijie.xyz ↗

Who may see and do what — the most heavily tested pillar (the acl-* e2e matrices). Entry is by access code (= invitation, QR); a code issues against a role whose RoleSnapshot freezes at issue (in-session rules never drift). Authorization composes as pure AND narrowing across three layers — global (live) ∧ role (frozen) ∧ ¬code-deny (frozen) — a code can only subtract, never re-allow (sparse deny tables, no tri-state). Identity rides the neutral _meta sidechannel; owner identity is an Ed25519 keypair (private key never enters domain); BYOAI modes let a visitor bring their own model under an envelope.

Status: landed (global layer live; role/code deny tables per the finalized 2026-06-23 design).

flowchart TB
  ALL["all capabilities + skills"] --> G{"global enabled?
(live — flips kill sessions)"}
  G --> R{"role grants?
(frozen at issue → RoleSnapshot)"}
  R --> C{"code denies?
(sparse deny tables — subtract only)"}
  C --> DQ{"connector deps met ∧ quota ok"}
  DQ --> EXP["exposed set for this session"]
  G -. "deny" .-> X1["hidden"]
  R -. "not granted" .-> X1
  C -. "denied" .-> X1

Data model (ER)

erDiagram
  OWNERS ||--o{ ROLES : defines
  ROLES }o--o| PROMPTS : "prompt_id (persona)"
  ROLES ||--o{ ROLE_SKILLS : grants
  ROLES ||--o{ ROLE_MCP_SERVERS : grants
  ROLES ||--o{ ROLE_CORPUS_URIS : scopes
  ACCESS_CODES }o--|| ROLES : "assumed_role_id"
  ACCESS_CODES ||--o{ CODE_CAPABILITY_DENIALS : "deny (sparse)"
  ACCESS_CODES ||--o{ CODE_SKILL_DENIALS : "deny (sparse)"
  ACCESS_CODES ||--o{ CODE_CORPUS_DENIALS : "deny (sparse, uri_pattern)"
  ACCESS_CODES ||--o{ CODE_MEMBERS : admits
  ACCESS_CODES ||--o| EMBEDS : "exposed by (code_id UNIQUE)"
  OWNERS ||--o{ CAPABILITY_SETTINGS : "global layer (live)"
  OWNERS ||--o{ OWNER_KEYPAIRS : "Ed25519"

Read it with the three layers in mind: capability_settings is the live global layer; everything reachable from roles freezes into the RoleSnapshot at issue; the code_*_denials tables (capability / skill / corpus — the corpus one landed 2026-07-16, 6395374b0, backend/db/schema.sql code_corpus_denials) are the frozen, subtract-only third layer.

The timing story — when each control acts

sequenceDiagram
  participant O as Owner
  participant B as Backend
  participant V as Visitor
  O->>B: create role + issue access code
  Note over B: RoleSnapshot FROZEN at issue<br/>(role grants + code denials)
  V->>B: open /handle?code=...
  B->>B: load frozen snapshot
  B->>B: assemble capabilities through gates<br/>(undiscovered = ErrHidden)
  Note over B,V: ACL acts at SESSION ESTABLISHMENT —<br/>denied things are never discovered
  V->>B: chat turns...
  Note over B: call-time inline = monitor + secret-scan ONLY
  O->>B: flip capability_settings (global)
  Note over B: LIVE layer — running sessions killed instantly

Children

  • access-control-diagram-coverage — diagram-coverage ledger.
  • acl-and-quota-granularity — the three-layer pure-AND model; sparse deny tables.
  • role-snapshot-frozen — freeze-at-issue vs the live global layer.
  • trusted-identity-via-meta — identity via the neutral _meta sidechannel.
  • byoai-envelope · byoai-browser-vault — visitor-supplied models, contained.
  • owner-keypair-auth — Ed25519 Sigv1 on /mcp/*: private PEM returned once, ±5min window + one-time nonce; ⚠ signature binds caller, not the request.
  • embed-credential-never-carries-the-code — the embed analog of the above: a per-embed Ed25519 EdDSA JWT authenticates a widget session so the code stays server-side; folds in bound origin + exp + one-time jti. Shipped 2026-09-01 (2098975db, backend/internal/access/usecase/embed_token.go); the Turnstile cnf binding is the one layer still deferred.
  • owner-sessions-and-abuse-controls — the small valves: Redis owner sessions that sign-out really revokes + the active-sessions panel, keypair last-used device/ip, per-IP code-fail lockout (codefail:ip:, 10 in 15 min) and banned_ips, gas metering for the no-code tier, the 30/hour per-recipient mail throttle, APP_BIND_HOST + Referrer-Policy. Shipped 2026-08-31 → 2026-09-06.
  • coded-landing-and-code-rotation — a code's three parts: row id (what links key on), snowflake slug → /c/<slug> (a locator, never a credential; ?code= is absorbed out of the URL on first paint), and the 64-bit code string — the only part codes.rotate replaces, killing every printed copy and live session while embeds/applications survive. Shipped 2026-09-06 → 07 (c6c54ce88, 505b3fc4f, backend/internal/access/usecase/codes.go).

Related notes