Story-988: resume held HITL run with one-time allowlist

Story 988Epic 928๐Ÿ“œ The Evidentiary RecordMatter Intelligence13 files ยท +999/โˆ’8

PR #20 ยท pramaan-io/pramaan-agents ยท merged 2026-06-02

How it flows

sequenceDiagram
  autonumber
  participant human as Human / Counsel
  participant resume as Resume command
  participant store as Store / DB
  participant hook as Tool hook
  participant tool as Tool execution
  human->>resume: signs warrant (tool + input hash)
  resume->>store: present approval_request_id
  store->>resume: load approved warrant, scoped by firm
  resume->>hook: mount allowlist recorder
  hook->>tool: exact tool+hash โ†’ allow
  hook-->>tool: any other tool โ†’ block
  tool->>store: claim idempotency key (burns the warrant)

src/pramaan_agents/db/store.py ยท src/pramaan_agents/runtime/tool_hooks.py

Methodin โ†’ outwhat it does
get_release_authorized_request(approval_request_id, firm_id, run_id, matter_id) โ†’ row | Noneloads the approved warrant scoped by firm โ€” kills cross-firm replay
claim_tool_effect_once(firm_id, run_id, tool_name, input_sha256, effect_ref, ) -> bool)atomically claims the tool-effect idempotency key โ€” kills duplicate execution
release_run_from_hitl(run_id, firm_id, approval_request_id) โ†’ Noneflips the held run to released โ€” kills the stuck queue

What changed, file by file

Database access layer: new methods to fetch the approved request scoped by firm, atomically claim a tool-effect idempotency key, and release a held run, so resume can replay safely.

new methodsignature
get_release_authorized_request(approval_request_id, firm_id, run_id, matter_id) โ†’ AgentApprovalRequestRecord | None
claim_tool_effect_once(firm_id, run_id, tool_name, input_sha256, effect_ref) โ†’ bool
release_run_from_hitl(run_id, firm_id, approval_request_id) โ†’ None
New fields: matter_id : UUIDrun_id : UUIDtool_name : strtool_input_hash : strapproval_status : str

Runtime entry point: adds a helper to pull the latest user message for re-feeding into the harness on resume replay.

new methodsignature
_latest_user_message(history) โ†’ str

Tool-call hook layer: introduces the allowlist recorder that blocks any tool the LLM proposes unless it matches the approved tool and input hash, and claims the idempotency key on the way through.

new methodsignature
claim_tool_effect_once(firm_id, run_id, tool_name, input_sha256, effect_ref, ) -> bool)
__init__(store, approval_request_id, tool_name, tool_input_hash) โ†’ None
before_tool_call(event) โ†’ UUID

Resume command payload: adds the approval_request_id field so non-resume commands carrying an approval id are rejected, and resume is bound to a specific warrant.

New fields: approval_request_id : UUID | None

Files changed (13)

Fileยฑ
docs/walkthroughs/story-988-walkthrough.html+116
scripts/check_story_walkthrough.py+3
src/pramaan_agents/db/reducer.py+2
src/pramaan_agents/db/store.py โ˜…+215
src/pramaan_agents/runtime/app.py โ˜…+77 โˆ’2
src/pramaan_agents/runtime/payloads.py+9
src/pramaan_agents/runtime/tool_hooks.py โ˜…+60
tests/runtime/test_payloads.py+51 โˆ’1
tests/runtime/test_tool_hooks.py+86
tests/test_reducer_events.py+41
tests/test_run_truth.py+156
tests/test_runtime_handler.py+153 โˆ’5
tests/test_story_walkthrough_check.py+30