T 022
uid: T-022 title: Enable RBAC enforcement β strict Security Rules (all DBs) + badge-based permissions model + flip RBAC_ENABLED status: done area: auth-rbac created: 2026-06-13 updated: 2026-06-23 precedes: T-025, T-027
RBAC initiative β sequence map (marked 2026-06-23, EOP Local Assistance). The RBAC/auth tasks are deliberately split (not merged), so this records the ORDER instead. Three parallel tracks: - Enforcement (this track): T-022 (hub β do FIRST) β T-025 (client-SDK migration; needs the strict Rules live) β T-027 (per-DB SA scoping; optional defense-in-depth, lowest priority). T-026 (Rules CI auto-deploy) is supporting tooling β no hard precede/succeed; can land independently, and is currently
blockedon cross-session coordination. - People / role model: T-094 (roster + user-management β FIRST) β T-095 (it_supportrole; needs T-094's role-assignment UI). - Auth specifics: T-083 + T-088 are independent (no ordering β parallelisable); T-057 β T-058 were the historical pair (both done).Each task carries
succeeds:/precedes:frontmatter mirroring this. (Convention introduced here, owner- requested 2026-06-23.)
Goal¶
Make user access to Firestore enforced by the DATABASE, not just remembered in code: user-facing reads/writes go through the Firebase CLIENT SDK gated by Security Rules + custom claims; the Admin SDK is reserved for privileged/derived/ integration ops (journals, reports, bank/Drive/GCP/Telegram, claim mints, cross-entity admin).
Key finding (2026-06-13 scoping)¶
This is ~80% ALREADY BUILT, behind the RBAC_ENABLED flag:
- lib/rbac/ β claims.ts, permissions.ts, subsidiaryAccess.ts, types.ts, config.ts.
Custom claims (role/status/subsidiaries/primarySubsidiary) synced on profile
create/update + sign-in, even while RBAC_ENABLED=false (SYNC_CLAIMS_WHEN_DISABLED).
- Security Rules files for ALL 4 DBs (firestore.{tebs-erl,tebs-mel,tebs-epl,aote-system}.rules)
wired in firebase.json β currently PERMISSIVE; STRICT role+subsidiary versions
written but commented out.
- Client SDK already used in some components (StudentDialog); user profiles in
aote-system/users with role + subsidiaryAccess; roles: super_admin / admin /
auditing_accountant / bookkeeper / project_admin / pending.
So the work = FINISH + ENABLE, not design from scratch: tighten/verify the strict
rules (incl. for the migrated documents per T-021), decide which Admin-SDK
reads move client-side, scope per-DB, add a Rules test plan, flip the flag.
This task vs siblings (RBAC initiative)¶
T-022 is now scoped to the ENABLE step only. The other surfaces are their own UIDs:
- T-025 β migrate user-facing Firestore reads/writes to the client SDK.
- T-026 β re-introduce the firestore.rules CI auto-deploy workflow.
- T-027 β narrow the Firestore Admin SDK credential (per-database SA scoping).
So T-022 = tighten/verify the strict (commented) Security Rules for all 4 DBs incl.
the migrated documents (T-021), complete position-level claims, add a Rules
test plan, and flip RBAC_ENABLED.
SA coordination β answered 2026-06-13 (UNBLOCKED, zero conflict)¶
The SA-refactoring agent's refactor is pure GCP IAM and orthogonal to all auth work:
- App's Firestore cred = firebase-adminsdk-fbsvc@aote-pms (FIREBASE_ADMIN_PROJECT_ID/
_CLIENT_EMAIL/_PRIVATE_KEY in Vercel) β UNTOUCHED by their refactor; still
datastore.owner + firebase.sdkAdminServiceAgent at project scope. Narrowing
it is mine β T-027.
- They are NOT touching Firebase Auth, custom claims, providers, or NextAuth.
Claims (subsidiary + position/role) are entirely mine β ship anytime.
- Client SDK + Security Rules are mine β ship anytime; NO timeline constraints.
- CI: firebase-rules-deployer@ (role firebaserules.admin) + GH secrets
FIREBASE_DEPLOY_KEY + FIREBASE_ADMIN_PROJECT_ID are ALREADY provisioned to
auto-deploy firestore.*.rules; the workflow file was reverted at 93e64a2
(branch claude/audit-user-access-control-cch7t) and must be re-introduced β
T-026 (coordinate with the user-access-control session, not the SA agent).
- Their ask back: if I mint any new SA, note it in memory project_sa_audit_aote_pms.
- Caveat: MEL/EPL not yet incorporated β tebs-mel/tebs-epl are conceptual; tebs-erl
is the only real subsidiary DB; aote-system is system-config.
Log¶
- 2026-06-13 created (was blocked on SA answers).
- 2026-06-13 UNBLOCKED + re-scoped to the enable step; SA answers recorded; split client-SDK migration / CI workflow / SA-scoping into T-025/026/027.
- 2026-06-23 expanded scope to own the role-model split β "complete position-level claims"
promoted from a footnote into the primary deliverable. See decision-log entry below for
the badge-based model the owner approved. The
it-supportcapability that T-095 was going to ship as a standalone role is folded inside T-022 instead: it's one of the starter badges in the registry T-022 introduces. T-095 was dropped by Accounting (Infrastructure) on 2026-06-23 with an explicit hand-off to me; its originating problem (I-007 infra-alert leak) is already shipped/fixed via PR #785.
Decision log¶
2026-06-23 β expanded scope: badge-based, IAM-style permissions model¶
- β
Attestation (User Management): read
AGENTS.md; per the cross-agent-edit rule I'm appending this entry rather than rewriting the prior author's prose, and signing it with theSource:line below. The earlier## Goal,## Key finding,## This task vs siblings, and## SA coordinationsections (EOP Local Assistance, 2026-06-13) stay as written β they remain accurate at the enforcement layer. This entry adds the permissions-model layer the enforcement work has to encode. - Source: User Management Β· https://claude.ai/code/session_01GGT5n9vCxKWoUQSRAfMSiW
- Proposed by: User Management (the conceptual model). Approved by: the owner (the owner re-framed it into a badge / IAM-style model and approved baking subsidiary into each badge).
Why the rescope β the gap the prior wording left¶
lib/rbac/types.ts today carries one UserRole union that mixes two independent dimensions:
type UserRole = 'super_admin' | 'admin' // β clearance / authority
| 'auditing_accountant' | 'bookkeeper' | 'project_admin' // β position / job function
| 'pending'
You can't say "Alison is a Bookkeeper with Admin clearance" β you'd have to pick one. The prior wording promised "complete position-level claims" but didn't define what a position-level claim should look like alongside everything else. This entry defines it.
The owner's preferred mental model (verbatim quotes under "Evidence" below) is named permission
bundles (badges) attached to users, where:
- Each badge declares a set of (surface, resource, action, subsidiary) grants in IAM-style
permission strings.
- Subsidiary is baked into the badge β e.g. mel-administrator is intrinsically the MEL-scoped
one; there is no separate "soft" subsidiary field overriding the badge's grants.
- A user holds one or more badges; effective rights are the union of those badges' grants.
- "Clearance" (Super Admin / Admin) is just a class of badges that grants meta-actions β rbac:badges:edit@all,
rbac:assign@all, audit-log:read@all. There is no separate clearance axis; it's badges all
the way down.
The permission-string shape (working defaults β refine on contact with reality)¶
<surface>:<resource>:<action>@<subsidiary>
| Axis | Working values |
|---|---|
surface |
coaching, projects, accounting, bank, contacts, records, dashboard, notifications, profile, file-archive, gadgets, tools, bank-accounts, dmarc, hk-banks, audit-log, rbac, infra |
resource |
per logical resource within each surface, e.g. transactions, adjustments, period, journals under accounting; sessions, students, invoice under coaching. Codified incrementally as each surface is migrated to badge-checked actions. |
action |
read, write (= create + update), delete, approve, configure. Wildcards: *. |
subsidiary |
erl, mel, epl, jft, all. Single value, no list β a badge that needs MEL+EPL is two badges. |
Wildcards allowed on each axis: accounting:*:read@erl (read everything in accounting under ERL),
*:*:read@mel (read-only on every MEL surface).
The starter badge set the owner described¶
Each badge is a curated bundle of the primitives above. Subsidiary-baked: there is no separate subsidiary field on the user record once badges are live.
| Badge id (working) | What it grants β at a glance |
|---|---|
mel-administrator |
coaching:*:*@mel, contacts:students:*@mel, notifications:*:read@all |
epl-administrator |
projects:*:*@epl, contacts:clients:*@epl, notifications:*:read@all |
bookkeeper-mel |
accounting:transactions:read+write@mel, coaching:invoice:read@mel, bank:accounts:read@erl |
bookkeeper-epl |
accounting:transactions:read+write@epl, projects:invoice:read@epl, bank:accounts:read@erl |
auditing-accountant-erl |
accounting:*:read@erl + :write+approve+configure on adjustments/period/journals; bank:*:read@erl |
director-erl |
union of MEL+EPL admin + accounting+bank+records under ERL; high-value approvals; audit-log:read@all |
super-admin (meta-badge) |
rbac:badges:edit@all, rbac:assign@all, audit-log:read@all, plus access to the Settings β Badges editor |
it-support |
infra:dmarc:read@all, notifications:infra:read@all β no accounting/projects/users surface |
Stacking examples: the owner is director-erl + super-admin. An external auditor at year-end
gets auditing-accountant-erl only, with accessExpiresAt. A coaching teacher who also does
light EPL admin during summer gets mel-administrator + epl-administrator.
Migration path β what changes where¶
This is the work T-022 now owns end-to-end. Sub-steps land as separate PRs under this UID:
- Data model + claims. Add
badges: string[]toUserProfile(alongside the existingrolefor read-back compatibility during the migration). AddBADGESregistry tolib/rbac/types.tswith the primitives table + the starter badge bundles. Syncbadgesinto Firebase custom claims viaclaims.tsso server-side guards can read them without a Firestore round-trip. - Page rules. Refactor
lib/rbac/pageRules.ts'sPageRuleto express "user must hold at least one badge whose grants intersect this set of required primitives" instead ofallowedRoles. The existing per-page rule format (PR #424) already supports incremental migration via the Firestore overlay. - Action permissions. Replace
ROLE_PERMISSIONSwithBADGE_PERMISSIONSkeyed by badge id. ReplacecanPerform(role, status, permission)withcanPerformAction(user.badges, permission)that scans across all of the user's badges. - Strict Security Rules. The strict Security Rules (today commented out) read custom claims
directly; rewrite them to read the
badgesclaim and grant per-collection access by matching badge primitives. - Existing-user migration. Map every existing
(role, subsidiaryAccess)pair to the closest badge bundle; write the migration asscripts/migrate-users-to-badges.ts(idempotent, dry-run by default) and keep a one-PR-window of dual-read fallback inrequireRbacso a rollback doesn't lock users out. - Admin UI. Settings β User Management β Users tab gets a "Badges" column + a badge-picker
in the Edit User modal (checkbox grid of badge ids). Settings gains a Badges editor tab so
the owner can add new badges / edit primitives without a code deploy (admin-only, server-side
validation, Admin-SDK writes β mirrors the
/rbac/pageRulescollection pattern from PR #424). - Flip
RBAC_ENABLED. With strict Rules deployed and migration complete, flip the flag.
Each step is a PR-sized increment; T-022 stays doing until all seven land. T-095 succeeds
this (its it_support becomes a badge id under step 6, not a UserRole union member).
Progress¶
- Step 1 β data model + registry + claims β DONE (branch
claude/t022-badge-model).lib/rbac/badges.tsholds the permission grammar (surface:resource:action@subsidiary), the pure parser/matcher (parsePermission/grantSatisfies/expandBadges/badgesSatisfy), and theBADGESregistry with all 8 starter badges.UserProfile.badges? CustomClaims.badges?added (ids only; omitted-when-empty to stay under Firebase's ~1000-byte claim limit);buildClaimsFromProfilesyncs them;ADMIN_PROFILE_UPDATE_KEYSaccepts the field. 27 unit tests pin the grammar + union semantics + registry invariants. Dormant β no live gate consumes badges yet;roleis still enforced. tsc clean; suite 424/424. Source: User Management Β· https://claude.ai/code/session_01GGT5n9vCxKWoUQSRAfMSiW- Step 2 β page rules + requireRbac understand badges β DONE (same branch).
PageRule.requireAnyBadgeGrant?: string[]added;evaluatePageRuleandrequireRbac'sRbacCheckOptionsunderstand it with three-case semantics: (1) user satisfies badge gate β pass, skip legacy checks; (2) user fails badge gate but the rule has legacy gates β fall through to legacy (migration safety β role-only users keep working); (3) user fails badge gate AND the rule has no legacy gates β deny (a badge-gated-only rule isn't a no-op). Status check still applies on the badge path so suspended users can't slip in.DEFAULT_PAGE_RULESadds badge-grant requirements alongside every existing legacy entry (no rule had its legacy fields REMOVED). NextAuth session/JWT propagate the newbadgesfield throughapplyProfileToTokenUser+ both callback emit points;types/next-auth.d.tsaugments Session.user / User / JWT. 14 new unit tests pin the badge-shortcut semantics + DEFAULT_PAGE_RULES against the 8 starter badges. Dormant in practice β no user has badges yet, so the legacy gates still do all the work; but the moment an admin assigns a badge in step 6, the badge path activates for that user. tsc clean; suite 438/438. Source: User Management Β· https://claude.ai/code/session_01GGT5n9vCxKWoUQSRAfMSiW - Step 3 β action permissions (canPerformAction + bridge) β DONE (same branch).
Two functions added to
lib/rbac/badges.ts:canPerformAction(user, permission)is the pure badge check (badges + status='active');canPerformActionOrLegacy(user, { badge, legacy? })is the migration bridge that takes the badge path first and falls back to the existingcanPerform(role, status, legacy)when the user has no matching badge. NoROLE_PERMISSIONSrewrite β the legacy table stays in place during the migration so role-only users keep working until step 7's flag flip. The bridge's signature lets call sites name BOTH grants explicitly ({ badge: 'accounting:transactions:write@mel', legacy: 'transactions:write' }) so the badge path can carry the subsidiary scope a pure legacy mapping couldn't (the legacyPermissionstrings are subsidiary-agnostic by design βtransactions:writedoesn't say "for which subsidiary"). Step 5+ will migrate API-route call sites incrementally tocanPerformActionOrLegacy; onceRBAC_ENABLEDflips in step 7, thelegacyfield drops and bridge collapses tocanPerformAction. 15 new unit tests pin the pure-badge semantics + the badge-or-legacy bridge (RBAC_ENABLED mocked true for the legacy-path tests). Dormant in practice β no live gate calls either function yet; both are available for the step-5+ caller migration. tsc clean; suite 453/453. Source: User Management Β· https://claude.ai/code/session_01GGT5n9vCxKWoUQSRAfMSiW - Step 4 β strict Security Rules read badges from claims β DONE (same branch).
All four rules files (
firestore.aote-system.rules,tebs-erl,tebs-mel,tebs-epl) now carry badge-aware helpers (hasBadge/hasAnyBadgereadingrequest.auth.token.badges) and capability functions that gate per-surface access on EITHER a matching badge OR a matching legacy role (migration-window dual-gate). The commented strict alternatives are rewritten in terms ofcanReadProjects(),canWriteAccountingErl(),canReadCoaching(),canReadClients(),canWriteBankAccounts()etc β each capability enumerates the badge ids it accepts in one place per file, so a registry rename is a single rules edit not N. Live behaviour change inaote-system.rulesonly: the/users/{uid}strict write rule now acceptssuper-adminbadge holders in addition torole: admin | super_admin(isAdminFromClaims()βisAdminOrSuperBadge()); additive β no user holds badges today so behaviour is byte-identical.__tests__/lib/rbac/strictRulesBadgeDrift.test.tspins the badge-id literals across rules files against the BADGES registry β a rename in TS that didn't update the rules fails the suite loudly. Rules still commented in strict mode β step 7 uncomments them; until then the live behaviour is the permissive baseline plus the one /users additive change. tsc clean; suite 457/457. Source: User Management Β· https://claude.ai/code/session_01GGT5n9vCxKWoUQSRAfMSiW - Step 5 β existing-user migration script β DONE (same branch).
lib/rbac/badgeMigration.tsis the pure mapping:mapUserToBadges({role, subsidiaryAccess})returns the closest badge bundle from the BADGES registry. Choices encode the migration semantic so post-step-7 access never NARROWS during the window:super_admin/adminβ['super-admin'](clearance);auditing_accountantβauditing-accountant-erl+bookkeeper-{mel,epl}per granted subsidiary;bookkeeperβbookkeeper-{mel,epl};project_adminβmel-administrator+epl-administrator;pendingβ[].diffBadges(current, next)computes the set-equal / added / removed view used by the script's dry-run printer.scripts/migrate-users-to-badges.tslists users, prints a per-user diff, and on--applywritesbadges+ re-syncs Firebase custom claims so the next request carries them. Flags:--apply(write),--uid=<u>(one user),--status=<s>(filter),--no-claims(skip claim sync β user re-signs to pick up). Idempotent: re-running on already-migrated users is a no-op. Dormant in practice β the badge ride is additive, the migration bridge (steps 2-4) keeps role-only users working, and strict rules stay commented until step 7. 27 new unit tests pin the mapping - diff. tsc clean; suite 484/484. Source: User Management Β· https://claude.ai/code/session_01GGT5n9vCxKWoUQSRAfMSiW
- Step 6 β admin badge-picker UI β DONE (same branch).
Added a "Badges (T-022)" section to
components/settings/UserEditModal.tsxbetween Subsidiary Access and Line Manager. Two-column checkbox grid grouped by badge kind (operational / clearance), each row showing label + one-line description from theBADGESregistry. A "Suggested for current role" hint runs the samemapUserToBadgesthe migration script uses, displays chips, and offers a one-click "Use these" button (disabled when the selection already matches). StateselectedBadges: string[], initialised fromuser.badgeson user-switch; sent in the PATCH body only when the set differs (order-independent equality so the audit trail and claims sync don't churn). API plumbing:UpdateBody.badgesvalidated asArray<isBadgeId>;updateUserProfile's signature gainsbadges?; auditbefore/aftersnapshots capture the change. Picker is dormant for existing users β opens with whatever badges they already hold ([]for users not migrated yet); checking a box and Save runs through the same API path the script uses. tsc clean; suite 484/484. Source: User Management Β· https://claude.ai/code/session_01GGT5n9vCxKWoUQSRAfMSiW - Step 7 β strict Security Rules flip β STAGED IN PR (branch
claude/t022-step-7-strict-rules, opened SEPARATELY from PR #797 because it has real production blast radius and must merge AFTER the migration script runs on prod). All four rules files (firestore.{aote-system,tebs-erl,tebs-mel,tebs-epl}.rules) flipped from permissive to strict: helpers uncommented + permissiveallow β¦ if isAuthenticated()lines replaced with the capability-function gates (badge OR legacy role) from step 4. Deploy runbook lives atdocs/runbooks/t022-step7-strict-rules-flip.mdβ pre-merge checklist (migration applied + super-admin badges verified for bootstrap directors), smoke steps, rollback path. TheRBAC_ENABLEDenv-var flip stays a separate operator action AFTER the rules deploy bakes for 24h. The legacy role half of the dual gate stays in the rules for a follow-up cleanup PR once we're confident no role-only path is in flight (typically 1-2 weeks after the flip). Drift guard still green; tsc clean; suite 484/484. Source: User Management Β· https://claude.ai/code/session_01GGT5n9vCxKWoUQSRAfMSiW
T-022 work is now feature-complete on claude/t022-step-7-strict-rules; the
remaining motions are operator merges + the prod migration window. After
step 7 lands and bakes, open a follow-up T-NNN to drop the legacy role
half from every capability helper and remove ROLE_PERMISSIONS.
Why subsidiary-in-badge and not subsidiary-orthogonal¶
The owner asked the question directly ("Would you think that it's stupid if I would like the
subsidiary to bake into each badge?"). The answer is no β it's the AWS IAM pattern: each policy
intrinsically scopes itself to the resource. Trade-off accepted: more badges (bookkeeper-mel,
bookkeeper-epl, bookkeeper-mel+epl) instead of one bookkeeper badge Γ subsidiary field, in
exchange for every badge name being a complete grant on its own β no "soft override" confusion
where someone holds a badge but a separate subsidiary field denies it. Auditing reduces to
"show me this user's badges" β done.
Granularity β working defaults (refine on contact with reality)¶
Action vocabulary picked as read Β· write Β· delete Β· approve Β· configure (CRUD + approve +
configure, where write covers create+update). Resource granularity picked as per logical
resource within each surface (so accounting has transactions, adjustments, period,
journals, not just one accounting:*). Both were the recommended defaults at the
AskUserQuestion prompt the owner answered "Proceed as discussed" on β the owner didn't
explicitly pick either, so they're flagged as working defaults: if a real per-resource
case comes up where these don't fit (e.g. wanting separate create vs update, or a bespoke
verb like accounting:transactions:post), refine the table at the top of this section.
Evidence β owner, 2026-06-23 (verbatim)¶
"Even though it's complicated, but I'd imagine that the access for each place and the actions one could do there should have a check box for enabling and disabling, that could be saved as a preset or to call them access badges (if we wanna be cute about it), eventually, if one role wants to have access to different places, they should have multiple badges (in the concept of a key) that grants them the right to do certain things."
"for MEL, there should only be an Administrator handling the management of MEL; an Administrator handling the management of EPL, while a bookkeeping (which manages the administrating of the company's book that sums that operation of MEL+EPL), an auditing accountant and directors of ERL. And each of these roles should have certain access to the places on EOP."
"ERL (the legal front)=the Accounting page+Bank Access+records+subscription (also everything is under ERL's name legally)+everything under MEL+EPL that uses ERL's legal front."
"Would you think that it's stupid if I would like the subsidiary to bake into each badge?"
"For the badges that you've came up with, I'd actually like you to either sub-divide it into smaller badges, so that I can control the read/ write and actions on certain pages too (like the concept of IAM)."
"Proceed as discussed please" (approval to lock the rescope after the granularity question was interrupted; recommended defaults applied with a flag in the granularity section above.)
2026-06-25 β closed (steps 1-7 merged, migration applied, strict rules live)¶
- β
Attestation (User Management): read
AGENTS.md; closing-the-loop entry per the rule β outcome + commits + blast-radius note + the GH workflow + IAM fix that surfaced during deploy. - Source: User Management Β· https://claude.ai/code/session_01GGT5n9vCxKWoUQSRAfMSiW
- Outcome / verdict β what landed:
- PR #797 (steps 1-6, badge model + page rules + actions + Security Rules helpers +
user migration script + admin UI badge picker) merged to
mainatf3d34df1. Dormant until the migration script ran; the only live behavioural change at this point was the additive/userswrite rule admittingsuper-adminbadge holders alongside the legacy role check. scripts/migrate-users-to-badges.ts --applyrun on prod aote-system 2026-06-25. 4 users mapped:me@jefferochan.com(super_admin) β['super-admin'],alisonhytang@gmail.com(admin) β['super-admin'],jake@establishrecords.com(admin) β['super-admin'],jeffero.c@gmail.com(auditing_accountant+ allAccess) β['auditing-accountant-erl','bookkeeper-epl','bookkeeper-mel']. Idempotency-confirmed (re-running dry-run reported zero diffs). Both bootstrap directors holdsuper-admin(the rescue valve named in the step 7 runbook).- PR #804 (step 7 β strict-rules flip) merged to
mainatcfcac3a4. Permissiveallow β¦ if isAuthenticated()lines across all fourfirestore.*.rulesfiles removed and replaced with the capability gates from step 4 (badge OR legacy role bridge). deploy-firestore-rulesGitHub workflow run28139736974β initial run failed withserviceusage.services.get 403; grantedfirebase-rules-deployer@aote-pmstheroles/serviceusage.serviceUsageConsumerrole via the cloud-resource-manager REST API (cloud-agent SA had IAM admin); re-ran the failed job; second attempt completed green in 6 seconds. Strict rules now live across all four databases on theaote-pmsFirebase project. The same 403 had silently failed PR #797 + PR #424's deploys previously β every rules push since 2026-05-18 had been failing to land in prod until this fix; documented in this entry so the next agent who touches the workflow knows the IAM dependency.- Commits:
f3d34df1(PR #797),cfcac3a4(PR #804). Migration script + IAM fix have no commit (operator-action against prod from the cloud-agent sandbox; recorded here for audit). - What's NOT done in this task (deliberate):
RBAC_ENABLED=truein Vercel β the env-var flip is a separate, deliberate operator action 24h after the strict-rules deploy. The bridge means the rules behave correctly with the flag still false; flipping it tightens the server-sidecanPerformcheck on top.- Drop the legacy
rolehalf of the dual gate β tracked under T-107. Soak window: ~1-2 weeks of normal operation with strict rules live, zeroPERMISSION_DENIEDspikes attributable to a missing badge. - Blast-radius note for other agents:
- Strict Firestore rules ARE live now. Every user-facing client-SDK read/write goes
through the strict gates. The badge claim is on every active user's token; the legacy
roleclaim too. The bridge admits either, so role-only paths still work for the soak window. deploy-firestore-rules.ymlworkflow needsserviceusage.serviceUsageConsumeron its SA. Don't strip this grant without a replacement plan. The drift guard (__tests__/lib/rbac/strictRulesBadgeDrift.test.ts) protects the badge-id literals in the rules files but doesn't catch IAM regressions.- The 8-badge starter registry (
lib/rbac/badges.ts BADGES) is now load-bearing for real prod access. Renaming a badge id requires editing all four rules files in lockstep- the drift guard (which fails loudly on miss).
- Evidence β owner, 2026-06-25 (verbatim):
"I need you to proceed with all While you'll be running the script for me as well" (explicit authorization to run the migration script on prod + drive the merges/deploy end-to-end.)