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 for | Answer |
|---|---|
| Own firm, assigned/admin-visible matter | matter 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 to | the same NotFound — by code AND by UUID |
| Ambiguous name in an accessible matter | candidates for the disambiguation flow — never an auto-pick |
| Name that matches nothing | no_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
- The DB proofs were run against a real Postgres with RLS enforced by the non-superuser runtime role: cross-firm and cross-matter NotFound paths, member positive control, archived-document exclusion, ranking order, and audit-row contents are all asserted from the red side of a failing lookup (the uniform 404 IS the denial proof — a leak would return candidates or a different error).
- Registry consistency tests fail closed by construction: a READ tool without a handler, or a catalog entry without the grant, goes red (existing harness, extended to this tool).
Files
src/shared/agent_tools/registry.py— the registry entry (the leash)src/shared/agent_tools/handlers.py— param validation, binding verification, audit emit, handlersrc/domains/document/service.py—resolve_matter_document_candidates(RLS-bounded read)src/domains/document/dtos/document.py—MatterDocumentCandidate/MatterDocumentResolutionsrc/domains/channel/service.py— publicbinding_belongs_to_callersrc/shared/audit/registry.py—document_resolutionsubject- Tests:
tests/unit/agent_tools/test_resolve_matter_document_params.py, registry/catalog updates,tests/integration/test_resolve_matter_document_tool.py(11 DB proofs incl. CC-20 cross-firm/cross-matter denial + audit)