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
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.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.
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 run | Receipt |
|---|---|
| 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 read | 126,464 input tokens |
| Total measured spend | $0.01017 / ₹0.8848 |
| Wall time | 130.8 seconds |
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.
| Requirement | Proof |
|---|---|
| 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 |
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