2026-09-23·by Sijie Wang#standmeet#architecture#design

acl-and-quota-granularity

ACL & quota granularity

Parent: access-control

Core model

Notation:

  • X = a capability or tool being checked for access

Exposure is pure AND narrowing across three layers:

exposed(X) = global_enabled(X) ∧ role_grants(X) ∧ ¬code_denies(X) ∧ connector_deps_met ∧ quota_ok

No layer can open what an upper layer didn't grant. Each must pass for access to succeed.

Freeze/live asymmetry

Access control enforcement depends on when each layer is evaluated (confusables):

  • Global layer: live. Stored in capability_settings, evaluated at request time. Flipping a global capability instantly kills all running sessions using it.
  • Role layer: frozen at issue. Snapshot into role-snapshot-frozen when the code is created. A role change doesn't retroactively affect existing codes.
  • Code layer: frozen at issue. Deny tables code_capability_denials / code_skill_denials / code_corpus_denials are locked at code creation. Past codes never see future code-side denials.

The tri-state elimination

The earlier tri-state code_capability_overrides (allow/deny/inherit) has been killed. It is now replaced by sparse deny tables:

  • code_capability_denials: presence of an entry means the capability is denied; no state column.
  • code_skill_denials: presence of an entry means the skill is denied; no state column.
  • code_corpus_denials: (code_id, uri_pattern) — a glob this code takes back from the role's positive corpus list (the third kind, landed 2026-07-16, 6395374b0; backend/db/schema.sql). Frozen into the snapshot as deniedCorpusURIs and checked at match time, not subtracted from the grant list (a glob cannot subtract from a list entry).

A code can only subtract, never re-allow. This eliminates the "code resurrects a capability" privilege escalation corner cases by construction.

V1 scope

The management-plane phase landed the global layer. V1 coverage:

  • Capability/tool ACL: fully implemented.
  • Skill ACL: fully implemented.
  • Corpus-glob ACL: LANDED — allowsCorpusEntry(scope, genre, path, published)access.AllowsCorpusEntry(scope, CorpusEntryRef) enforced on every corpus read (backend/internal/corpus/usecase/corpus_lister_pg.go:53, backend/internal/access/entity/path_acl.go:143); the frozen scope (grants ∧ ¬code corpus denials ∧ published-only for the public identity) rides the RoleSnapshot (AllowsCorpus(uri, published) / CorpusScope()), raw://** hard-denied; forwarded to the externalized retrieval plugin as one opaque corpus_scope blob via _meta.
  • Owner-MCP-server ACL: LANDED — ext-mcp servers attach per role via role_mcp_servers (SetMCPServers + ownership validation); frozen into the snapshot as mcpServerIDs.

Implementation detail: ACL=always capabilities

Some capabilities are marked ACL=always (retrieval, ask_visitor, summarize) and do not appear in allowedTools. Their denial is gated at the exposure door via RoleSnapshot.deniedCapabilities, not table lookup.

Verification

The heaviest e2e coverage in the repo:

  • acl-capability-matrix: tests all combinations of global/role/code layers.
  • acl-skill-matrix: tests skill ACL isolation.
  • acl-freeze-isolation: verifies freeze/live asymmetry under concurrent changes.
about this entry

One of sijie's wiki entries. The AI on this site is grounded in the same corpus and answers in sijie's voice, with citations back to entries like this one — answering costs sijie money, so it waits behind a code: enter an access code →