RoleSnapshot frozen at session issue
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.