Story-2855

resolve_matter_document: the channel document-fetch leash

RFD-AGT-013 T3a-a / D18, acceptance CC-20. Counsel asks Mira for a document on any channel. The brain never mints URLs and never guesses a document — it emits an intent for this one registered server-plane READ tool, and functions resolves it under the caller's Postgres authz/RLS, returning a ranked candidate list. The wrapper-link mint (T3b-1) consumes the chosen candidate's document id; this story ships only the registry entry, the handler, the RLS-bounded read, and the audit.

One registry entry, one handler (the existing leash pattern)

SERVER_TOOL_REGISTRY["resolve_matter_document"]   ← the leash (human PR edit)
  kind: READ · required_permission: matters:read
        │
        ▼  executor re-resolves authority from the SESSION (never the client)
READ_HANDLERS["resolve_matter_document"]
        │
        ├─ _resolve_document_params   (HR-21 boundary validation, total over
        │    LLM junk: null-sentinels absent, exactly ONE matter ref,
        │    limit clamped to 25)
        ├─ binding_belongs_to_caller  (a brain-supplied binding_id is
        │    provenance to VERIFY against firm+user, never to trust)
        ▼
DocumentService.resolve_matter_document_candidates
        ├─ matter by code (D13) or UUID — firm predicate + record-level RLS
        │    (can_read_matter): other firm / unassigned matter → uniform 404
        └─ live documents matching q, ranked exact > prefix > substring,
           then recency; no q → most recent. Bounded by limit.

CC-20 — resolution is an oracle for nothing

Caller asks forAnswer
Own firm, assigned/admin-visible mattermatter identity + ranked candidates (positive control)
Another firm's matter (UUID or colliding code)the same NotFound as a matter that does not exist
Same firm, matter the member is NOT assigned tothe same NotFound — by code AND by UUID
Ambiguous name in an accessible mattercandidates for the disambiguation flow — never an auto-pick
Name that matches nothingno_match, empty list — never a fabrication

The candidate payload is byte- and storage-free (CC-38): document id, filename, display name, type, status, dates — no object keys, no URLs. The RLS predicate is evaluated by the non-superuser pramaan_app role in the DB proofs, so the denials are enforced by Postgres, not by test hope.

Audit — every resolution logged with binding/firm/matter

Each executed resolution appends one document_resolution.resolved row (new audit-registry subject, matter area): firm + actor are session-derived by the audit writer, the matter rides the scope, and the metadata carries the tool, the outcome, the candidate count, and the channel binding — stamped only after verification. An unknown, another member's, or a revoked binding_id is rejected before any resolution runs, so the trail can never be poisoned. The query text is counsel prose and never rides the row. The generic agent_session.executed executor row still lands on top of this, as for every server tool.

Red-first evidence

Files