Story-3385 · functions lane · RFD-MATTER-006 F1

Authority now follows the matter, not the firm

A firm role answers what a person may do in the firm. It does not answer whether that person is staffed on a particular matter. Matter mutations now cross a resource-aware boundary that checks the active assignment for the exact matter in the request.

staffed on matter A + team:staff(A)  → allowed
staffed on matter A + team:staff(B)  → 403
firm admin + content:write(B)        → allowed without a roster row
system/agent context                 → pass through

The capability boundary

require_matter_capability(matter_id, capability) supports content:write and team:staff. Human members and editors need an active firm membership plus a same-firm assignment on that exact matter. Firm admins retain their explicit bypass. Internal sessions without current_user_id pass through, matching the existing matter-detail entry pattern.

The Wave-2 entry gate, can_read_matter, content RLS, assignment schema, lead eligibility, and vacancy repair behavior are unchanged. This story supplies the authorization foundation only.

One blanket guard became four precise decisions

GuardDecision
_require_staffedAny active staffed member/editor, or firm admin, may perform non-destructive team staffing.
_require_can_remove(target)Applies the DL15 actor/target ladder to the roster on this matter.
lead/destructive guardLead counsel or firm admin only; retained for destructive changes and occupied-seat control.
_require_can_claim_vacant_leadRequires a vacant seat plus a staffed actor or admin, without changing who may become lead.

DL15 removal ladder

Actor on this matterMay remove
MemberOther non-lead members
EditorNon-lead members and editors
LeadAny non-lead roster member, including a roster-only firm admin
Firm adminAnyone

Target evaluation uses the existing protected roster reader, because ordinary assignment RLS intentionally exposes only the caller's own row. An unstaffed caller is rejected before that roster reader is reached.

The route inventory is executable

CI walks FastAPI's resolved dependency graph and inventories every member-enabled mutation route containing {matter_id}. It fails when a route has neither capability, and it pins the eight current mutation routes to their expected content:write or team:staff guard. A planted unguarded route proves the detector goes red.

Proof

ScenarioEvidence
S11 / S54A member staffed on matter A can mutate A; the same content and team requests against matter B return 403, and B's assignment remains.
S24–S30The full DL15 member/editor/lead/admin target matrix is exercised against real PostgreSQL RLS.
Capability matrixStaffed member, staffed editor, lead, admin, unstaffed, inactive, cross-matter, cross-firm, and system contexts are covered.
Route inventoryThe detector was observed red with five live member-enabled routes missing a resource guard, then green after all five were wired.
Regressionmake test: 6,187 passed, 3 skipped.