feat(notifications): Slice 3 — governance action queue (RBAC-scoped)#686
Merged
Merged
Conversation
Turn the bell into an action queue: route governance + remediation events to
the users who can act on them, not the whole fleet.
New primitives:
- auth.RolesWithPermission(p) — resolve a permission to the built-in roles that
grant it (pure helper over BuiltInRoles).
- notifyfeed.Store.RecordForRoles — fan one row per active user holding any of
the given roles (EXISTS, so a multi-role user gets one row), same
upsert/collapse as RecordFanout.
Producer: notifyfeed.GovernanceProjector
- ExceptionRequested -> users who can approve (roles granting exception:approve:
auditor, security_admin, admin), kind exception_pending, high, deep-link
/settings/policies, grouped per exception.
- ExceptionDecided -> the requester only, exception_approved/_rejected, medium.
- RemediationFailed -> users who can act (roles granting remediation:execute:
ops_lead, security_admin, admin), kind remediation_failed, high, deep-link
/hosts/{id}, grouped per host+rule. Fires on a TERMINAL failure (execute that
failed, or a rollback that did not restore) — NOT a successful user-initiated
rollback (that is intended, not an alarm).
Producer hooks (best-effort, never fail the transition/job; nil-safe):
- exception.Service.WithNotifier + calls in Request/Approve/Reject.
- worker.RemediationWorker GovernanceNotifier + calls on the two failure sites.
- wired in cmd/openwatch/main.go (serve) and worker.go (dedicated worker).
Both producers hold interfaces in their own packages, so neither imports
notifyfeed. Spec system-notifications v1.5.0: C-08 + AC-15/16/17. Tests:
auth.RolesWithPermission unit test; DB-backed governance fan-out tests
(approver-scoped, requester-only, operator-scoped). No frontend change — rows
render in the existing bell drawer.
This was referenced Jun 25, 2026
remyluslosius
added a commit
that referenced
this pull request
Jun 25, 2026
Bump version.env to 0.2.0-rc.16 and roll the CHANGELOG [Unreleased] accumulator into a dated section. Bundles the notifications action-queue work since rc.15: rule-regression projector (#685), RBAC-scoped governance queue (#686), and exception-expiry lifecycle (#687). Local: changelog + version-consistency + package-build + fips tests pass.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Notifications Slice 3 (per the change-driven design §3/§7/§9): turn the bell into an action queue. Where Slice 1 (alerts) and Slice 2 (rule regressions) fan to every active user, governance items route only to the users who can act on them.
New primitives (the RBAC-scoped fan-out Slice 3 needed)
auth.RolesWithPermission(p)— resolves a permission to the built-in roles that grant it (pure function overBuiltInRoles).notifyfeed.Store.RecordForRoles— fans one row per active user holding any of the given roles (EXISTS, so a user with two matching roles still gets one row; avoids anON CONFLICTdouble-hit), with the same upsert/collapse/re-surface semantics asRecordFanout.Producer:
notifyfeed.GovernanceProjectorexception:approve(auditor, security_admin, admin)exception_pending/ high/settings/policiesexception_approved/_rejected, medium/settings/policiesremediation:execute(ops_lead, security_admin, admin)remediation_failed/ high/hosts/{id}Grouping: exceptions per exception id, remediation per
(host, rule). A successful user-initiated rollback is not notified — it's the intended outcome, and alarming on it would violate the design's "noise is a bug" principle (§7).Producer hooks (best-effort, nil-safe, never fail the transition/job)
exception.Service.WithNotifier(...)+ calls inRequest/Approve/Reject.worker.RemediationWorkerGovernanceNotifier+ calls on the two terminal-failure sites (execute→failed, rollback-not-clean).cmd/openwatch/main.go(serve) andworker.go(dedicated worker).Both producers hold the interface in their own package, so neither imports
notifyfeed(the projector satisfies them structurally).Spec & tests
system-notificationsv1.5.0: C-08 + AC-15/16/17.specter checkgreen (114 specs); annotation coverage = system-notifications 17/17 ACs, 100%.auth.RolesWithPermissionunit test (which caught my own wrong assumption —exception:approveincludes auditor, confirming the projector notifies auditors); DB-backed governance fan-out tests proving approver-scoped / requester-only / operator-scoped routing and that out-of-scope roles receive nothing.Scope notes
🤖 Generated with Claude Code