Field notes
Soul runtime overrides — manager-editable ACLs without redeploy (2026-06-11)
Problem#
SOUL.md ACLs (trustedChannels, allowedChannels, dmAllowedUsers,
blockedUsers) were static: any change meant editing the file and
rebooting. The manager needs to trust a new channel, allowlist a DM user, or
ban a noisy user at runtime.
Full design: docs/superpowers/specs/2026-06-11-soul-runtime-overrides-design.md.
Shape#
- Store:
soul_overridessqlite table —(field, value)primary key,action ∈ {add, remove}, latest write wins, audited (created_by,created_at). - Merge:
soulData()applies(base ∪ adds) − removeson every read. All gates callsoulData()per inbound message and never cache across messages → an override is live on the next message in every session, no reload.soulDataBase()exposes the un-overlaid view for provenance rendering. - Full shadow: a runtime
removemasks an entry that comes from SOUL.md itself./soul listshows soul-vs-runtime provenance so drift stays visible;/soul clearreverts to pure SOUL.md. - Surfaces:
/soul <trust|allow|dm|block> <add|remove> <id>slash command +soul_overridetool on the surface MCP. Both write through one validated helper (mutateOverrideinsrc/soul/overrides.ts).
Authority — deliberate choices#
- Primary manager ONLY. Backup manager and approvers are excluded (owner: "only Manager"). Loosening is a one-line change at each gate.
- Both surfaces gate on the signed inbound Slack user id — the MCP tool checks the turn initiator via a live getter over the route ctx, so a prompt-injected model call cannot mutate ACLs from a non-manager turn.
- Self-lockout guard:
blockedUserscan never contain the manager id.
Gotchas hit#
setSoulData()memo holds the base; the overlay is applied insoulData()after memo resolution — so test fixtures injected viasetSoulDatastill see runtime overrides (and the memo path stays override-aware in prod).- The overlay read is wrapped in try/catch returning base — a corrupt overrides table must never take the engagement gates down.
soul_overrideonly mounts when the gateway passes aninitiatorresolver tocreateSurfaceMcp— legacy callers (tests, sim) without one get no tool rather than an ungateable one.