Story-3390 · functions lane · RFD-MATTER-006 F6
Destructive matter operations are classified before they ship
A generated registry now discovers custody-reducing API operations from the mounted FastAPI application. Every matter-governed operation declares an authorization class; every destructive operation outside that boundary has an explicit disposition. CI regenerates the file and fails if a new route is unclassified or the committed snapshot is stale.
mounted DELETE / destructive POST / destructive PATCH semantic
↓
classified matter guard or explicit disposition
↓
openapi/destructive-operation-manifest.json + CI diff gate
The authorization rule
All 19 matter-governed operations currently use the
staffed-editor class: a staffed firm editor, the active matter
lead, or a firm admin may proceed. An ordinary staffed member receives 403.
The guard reuses F1's active, same-matter staffing boundary and takes a row
lock on the assignment while authorizing the change.
Generated matter-operation manifest
| Method | Operation | Selector | Auth class |
|---|---|---|---|
| DELETE | /app/agent-sessions/{session_id} | all | staffed-editor |
| POST | /app/agent-sessions/{session_id}/archive | all | staffed-editor |
| POST | /app/agent-sessions/{session_id}/clear | all | staffed-editor |
| DELETE | /app/documents/{document_id}/annotations/{annotation_id} | all | staffed-editor |
| PATCH | /app/matters/{matter_id} | status=archived | staffed-editor |
| PATCH | /app/matters/{matter_id}/adversary-reads/{read_id}/state | state=archived | staffed-editor |
| DELETE | /app/matters/{matter_id}/claims/{claim_id} | all | staffed-editor |
| DELETE | /app/matters/{matter_id}/claims/{claim_id}/block-refs/{ref_id} | all | staffed-editor |
| DELETE | /app/matters/{matter_id}/claims/{claim_id}/rbr-links/{rbr_id} | all | staffed-editor |
| POST | /app/matters/{matter_id}/document-relationships/{relationship_id}/revoke | all | staffed-editor |
| DELETE | /app/matters/{matter_id}/document-saved-views/{view_id} | all | staffed-editor |
| DELETE | /app/matters/{matter_id}/documents | all | staffed-editor |
| DELETE | /app/matters/{matter_id}/documents/{doc_id} | all | staffed-editor |
| POST | /app/matters/{matter_id}/documents/{doc_id}/collation/undo | all | staffed-editor |
| DELETE | /app/matters/{matter_id}/documents/{doc_id}/legal-hold | all | staffed-editor |
| DELETE | /app/matters/{matter_id}/frame/locks/{field} | all | staffed-editor |
| DELETE | /app/matters/{matter_id}/participants/{participant_id} | all | staffed-editor |
| PATCH | /app/matters/{matter_id}/proceeding-types/{matter_proceeding_type_id} | status=archived | staffed-editor |
| PATCH | /app/matters/{matter_id}/proceedings/{proceeding_id}/stages/{stage_id} | status=archived | staffed-editor |
Field-level and non-matter decisions
PATCH /app/matters/{matter_id} remains available for ordinary
edits. The service checks status=archived after resolving the
matter and before mutating any field, so a mixed title-and-archive request
is rejected atomically. closed remains an ordinary visible
lifecycle milestone; it does not hide the matter or reduce custody, so it is
not classified as destructive.
Matter-bound chat sessions are matter-governed even though their routes are
keyed by session ID: the dependency resolves the owned session's real
matter_id before applying the guard. Document annotations use
the same pattern and bind both annotation ID and document ID before
authorization. Affiliation deletion is deliberately different under DL23:
an affiliation is a firm-shared identity edge with no matter_id,
so it keeps an explicit firm-editor-or-admin gate rather than borrowing
authority from any one matter roster.
Red-first and behavioral proof
| Scenario | Evidence |
|---|---|
| S13-S16 | Real PostgreSQL/RLS test admits a staffed editor, member-lead, and firm admin while rejecting a plain staffed member and an unstaffed member through the single named guard used by every manifest operation. |
| S17 / C4 | A member's mixed title + status=archived update raises 403 inside MatterService.update_matter; the title and status are unchanged afterward. |
| S18 / DL23 | A member staffed on matter A cannot delete a firm-shared affiliation; firm editor and firm admin requests succeed. |
| S19 | The live claim→RBR DELETE guard was temporarily removed. make gen-destructive-ops failed with that exact route in unclassified=[...]; restoring the guard returned the generator and snapshot test to green. |
| Inventory drift | The contract test compares the generated registry to the committed JSON, rejects unsupported classes, pins the archive selector, and plants an independent unclassified DELETE. |