RoleSnapshot frozen at session issue

Updated · View the entry on sijie.xyz ↗

When a session is issued, the entire role state (corpus URIs, prompt body, skill prompts, allowed tools, denied capabilities, denied corpus globs, waypoints, per-capability config, role id/name) is snapshotted into session_data in Redis; the session never reads the role row again (backend/internal/access/entity/role_snapshot.go).

Consequence: an owner editing a role / prompt / skill does not affect sessions already running — no "in-flight session suddenly loses access." The only mid-session remedy is revoking the access code — except the global capability layer, which is live and kills sessions instantly (confusables). deniedCapabilities is explicit (not subtraction), so it can gate even ACL=always capabilities (retrieval, ask_visitor).

Class view

classDiagram
  class RoleSnapshot {
    <<frozen at issue - Redis session_data>>
    -capConfig map~string,json.RawMessage~ - per-capability config
    -frozenAt time.Time
    -roleID, roleName string
    -promptBody string
    -codePromptBody string
    -providerID string
    -corpusURIs []string - granted globs
    -skillPrompts []string
    -allowedTools []string
    -deniedCapabilities []string - explicit
    -deniedCorpusURIs []string - code corpus narrowing
    -skillIDs, mcpServerIDs []string
    -dockButtons []DockButtonConfig
    -waypoints []Waypoint
    -requireGhostEvidence bool
    -gasMetered bool
    +AllowsCorpus(uri, published) bool
    +CorpusScope() CorpusScope
    +AllowsCapability(capID, aclAlways) bool
    +IsZero() bool
    +MarshalJSON() / UnmarshalJSON()
  }
  class RoleSnapshotInit {
    exported mirror of every field
    the ONLY way in - NewRoleSnapshot(init)
  }
  class roleRow {
    <<postgres - live owner-editable>>
    roles + role_skills + role_mcp_servers + role_corpus_uris
  }
  class capabilitySettings {
    <<postgres - the ONE live layer>>
    owner_id, capability_id PK
    enabled bool
    updated_at
  }
  RoleSnapshotInit --> RoleSnapshot : NewRoleSnapshot
  roleRow --> RoleSnapshotInit : copied ONCE at issue
  capabilitySettings ..> RoleSnapshot : bypasses the freeze (live AND-term)

All 18 fields are unexported — the snapshot is immutable after construction (RoleSnapshotInit is the only door, and JSON round-trips it through Redis). Note it also freezes codePromptBody (the per-code prompt), the role's dockButtons, the code's corpus denials (deniedCorpusURIs, from code_corpus_denials) and each capability's per-role config (capConfig, opaque to this domain).

AllowsCorpus(uri, published) hard-denies raw://**, then requires a match in the positive glob list AND no match in the code's deny globs — the same frozen scope (CorpusScope()) is forwarded whole, as one opaque blob, to the externalized retrieval plugin via _meta.

Related notes