pramaan-functions · EPIC-2834 W2 · RFD-AGT-013 D10 (T2b-b)
Story-2866 — Command broker: translator + own-access gate + origin tag
Mira's map_command route (pramaan-agents Story-2864) emits a typed
command_resolution.v1 — a registered command or a refusal — and never
executes anything. This story builds what happens next on the functions side:
the resolution is translated into a kind='mira_command'
launch on the existing clerk rail, the sender's own matter access
decides whether it may land (killing the firm_admin landing backdoor),
the draft is tagged channel-originated on the ratify rail, and the
terminal result posts back into the same WhatsApp thread.
The path, end to end
command_resolution.v1 (map_command turn, agents side — MERGED)
│
▼
src/domains/channel/command_broker.py ← NEW (this PR)
parse_command_resolution fail-closed: wrong schema / missing block / refused ⇒ no launch
check_sender_matter_access OWN-ACCESS GATE: business.matters read under the SENDER's
real RLS identity (firm + user + membership role, 0077 rule)
reserve_launch_run_id request_id = channel-command:<mapping_run_id> (redrive-safe)
translate_resolution → AgentCommand kind='mira_command'
+ channel_origin='whatsapp' + channel_binding_id
│ (existing clerk-rail SQS, $1.50 command cap)
▼
src/clerk_worker/runner.py → mira_command executor (unchanged)
│ turn.completed
▼
src/clerk_worker/mira_command.py ← BACKDOOR KILLED
_set_sender_scope: the landing transaction runs as command.user_id +
the runner-stamped firm_role — NEVER 'firm_admin'. Record-level RLS
(business.can_read_matter) refuses any matter the sender cannot access.
│ landed as a PROPOSED frame version, requires_counsel_approval=true,
│ run_manifest.channel_origin='whatsapp' ← ORIGIN TAG on the draft
▼
report_channel_command_result (runner hook, fail-open)
→ dispatch_whatsapp_text kind='answer' into the SAME thread:
"draft ready — review and confirm in the app" (ratify stays in-app, CC-13)
What changed
| File | Change |
|---|---|
src/domains/channel/command_broker.py |
NEW — typed resolution parse (fail-closed), translator, own-access gate, refusal dispatch (static, zero-LLM), launch reserve+enqueue, report-back. |
src/clerk_worker/mira_command.py |
_set_firm_scope (hardcoded firm_admin) DELETED →
_set_sender_scope (sender's user_id + real role; fail-closed on a
missing user). channel_origin threaded into the ingest request. |
src/shared/agent_commands.py |
AgentCommand.channel_origin + channel_binding_id —
typed, launch-only, travel as a pair, dropped from the wire when absent so
app envelopes stay byte-identical. |
src/domains/matter_frame/{dtos,service}.py |
MatterFrameCommandIngestRequest.channel_origin; stamped into the
landed version's provenance.run_manifest. |
src/clerk_worker/{runner,handler}.py |
ClerkDeps.report_channel_command seam: on a channel-originated
command, report the landing result (or its honest failure) into the thread —
fail-open; the delivery record is its own terminal state (R5). |
Why the gate is the RLS rule itself
The broker's pre-enqueue check does not re-spell the access predicate: it sets the
sender's scope transaction-locally and reads business.matters — the row is
visible exactly when the 0077 can_read_matter record rule says so
(firm_admin/firm_editor firm-wide, an associate only via assignment, never
cross-firm). The landing then runs under the same sender identity, so even a launch
that bypassed the broker cannot land on a matter the sender cannot access.
One spelling, two enforcement points, nothing to drift.
Proof (CC-06 / CC-13 / CC-46)
- CC-06 (
tests/steps/test_channel_command_broker.py) — registered frame-edit → exactly one launch, taggedchannel_origin='whatsapp'; redriven resolution dedupes onto the same run; landing writes ONE proposed version withrequires_counsel_approval=trueand the origin tag in its run_manifest; report-back writes a durableanswerdelivery record. - CC-13 — an "apply the edit" ask arrives as a REFUSED resolution; the broker launches nothing and the proposal still awaits in-app review. No channel code path can accept a frame version.
- CC-46 — a
firm_memberwith no assignment: refused before anything is enqueued (zero frame rows, zero run reservations, one static refusal delivery); a second leg drives the LANDING seam directly and proves the sender-scoped refusal. - Red-first — with the old
firm_adminscope planted back into the landing, the CC-46 landing-seam test FAILS (the frame lands); with this PR's code it passes. The gate has been observed red. - Unit:
tests/unit/test_channel_command_translator.py(fail-closed parse matrix, envelope shape, wire round-trip, provenance-pair validation) + 4 new runner seam tests intests/unit/clerk_worker/test_runner_mira_command.py.
Deliberately out of scope
- The channel-ingress consumer that launches
map_commandruns and calls this broker — its wave sibling owns that wiring; every seam here is directly driven by the CC suite until it lands. - The instant "✅ Registered." ack — the mapper's turn markdown is the ack, delivered by the channel turn-delivery path (D5 formatter).
- Executor substrate (EventBridge vs SQS fan-out) — parked with EPIC-2831/Spike-2832 per the RFD.
Zero migrations. Zero OpenAPI drift. No new cloud capability — the path rides the existing clerk-rail queue, sender Lambda invoke, and RLS grants.