Story-3117 Walkthrough

Outcome: counsel can now ask Vani to change the Brief without giving Vani a write capability. Vani emits one terminal, server-plane request to Vega and the runtime answers exactly I've asked Vega.

counsel asks for a Brief change
  → Vani calls request_frame_update(instruction, scope)
  → SDK returns DeferredToolRequests.calls
  → brain pins matter_id + base_frame_version from the envelope
  → turn.completed: terminal request intent + "I've asked Vega."
  → Vega proposes a diff
  → broker apply/commit asks counsel for approval
  → a later Vani turn trusts current frame_version, never request memory
Boundary. The tool is an SDK ExternalToolset definition. There is no Python function for Vani to execute, no frame payload in the result, and no channel parameter. An injected document can at most cause the same request intent; it cannot reach the legal record through Vani.

Why approval is not on the request

The ticket and pramaan-docs ADR-178 literally pointed at requires_approval. The current SDK probe showed that this would be the wrong timing: requires_approval=True moves the call into DeferredToolRequests.approvals and never executes it. Vega would not have been asked, so Vani could not truthfully say she had asked.

The shipped call therefore uses normal SDK external deferral and lands immediately in DeferredToolRequests.calls. The human gate belongs on the dangerous operation—the broker's later apply/commit—after counsel can see Vega's proposal.

Streaming safety

Ordinary answer deltas remain immediate. Prompt hardening prohibits completed-write claims, a post-hoc output validator retries explicit claims, and the terminal body is authored by code. A live adversarial suite preserves the unresolved timing question by inspecting answer-channel turn.delta, not only turn.completed. Its mutation proof injects a false pre-tool delta and shows the named assertion turns red.

Final live runReceipt
20/20 request-only; 0/20 false-completion deltas 2026-07-29T15-51-30Z
Input / output / reasoning 134,936 / 6,930 / 5,432 tokens
Cache read126,464 input tokens
Total measured spend$0.01017 / ₹0.8848
Wall time130.8 seconds

Cached-prefix cost

The exact OpenAI-wire tool catalog is counted with o200k_base. The full catalog now adds 2,734 tokens / 12,418 UTF-8 bytes over the resolver-only baseline. Replacing the old zero-parameter request_frame_update stub with its real description and schema adds 237 tokens / 1,000 bytes.

Executable proof

RequirementProof
Terminal request; exact acknowledgement; no write test_injected_document_can_at_most_emit_a_terminal_request
Envelope matter and version beat model-supplied values same test asserts matter_id and base_frame_version=7
Post-hoc false-write catch test_output_validator_retries_an_explicit_completed_write_claim
Delta metric is non-vacuous test_adversarial_eval_detects_a_false_pre_tool_delta
Other 15 tools remain honest stubs test_every_stub_returns_unavailable_and_vani_says_it_is_not_built
Story-3101 delta guard remains test_rejected_unavailable_finding_never_reaches_the_delta_stream

Verification

Focused frame/resolver/stub tests      → 17 passed
Offline conversational adversarial     → 14 passed
Live frame_update --repeat 20          → 20 passed
uv run ruff check .                    → All checks passed
uv run pytest -q                       → 444 passed, 3 dependency warnings