Two changes, one binary batch:
1. app_install/app_uninstall (task 3): '!ai please install bitcoin knots'
correctly said it can't. Both tools are category-Apps, destructive, and
ride the 13-08 confirm gate (node-authored descriptions added). Install
validates catalog membership BEFORE the dialog (a typo never spends an
approval); uninstall resolves installed ids. Both reach the SAME
package.install/package.uninstall spawns every authenticated caller
uses, via a curated Arc-taking sibling of assistant_dispatch_tool.
2. S6: cloud legs no longer strip prior USER turns from replayed history.
Turn-minimality's allowlist is now the whole conversation's operator
turns (the node's own D-08 transcript, same trust class as this turn),
still mechanically matched, B1 secret scan and 64KB cap unchanged,
fabricated user messages still truncated. The model no longer sees its
own answers without the questions.
Co-Authored-By: Claude <noreply@anthropic.com>
Operator asked "what films are there to watch from my peers" and the model
answered, honestly, that it had no tool for it. It was right: content_list
mapped only to content.list-mine — this node's own shared files. Peer
catalogues and IndeeHub were unreachable from the assistant entirely.
content_list now takes scope: own | peers | purchased | films, dispatching to
content.list-mine / content.browse-all-peers / content.owned-list /
content.indeehub-projects. The model picks from a closed enum and never names
a method, so an invented scope falls back to "own" rather than reaching
anything it was not granted (T-13-34).
Two new RPCs behind it:
- content.browse-all-peers aggregates every federated peer in ONE call. The
dashboard fans this out client-side, but asking a model to enumerate peers
and loop is how it ends up claiming it has no tool. Rides FIPS —
PeerRequest::new(fips_npub, onion, "/content") with a 6s FIPS fast-fail then
Tor — so the onion is the peer's identity and FIPS is the transport.
Sequential with a per-peer timeout, not an unbounded fan-out: 02-08 traced a
real UI stall to browse-peer starving the connection pool. One peer being
down is the normal case and contributes nothing rather than failing the call.
- content.indeehub-projects fetches IndeeHub's catalogue, public plus (via a
node-signed NIP-98 login) the operator's private titles. Node-side because
signing that in the browser would put identity material next to the model,
which this phase rules out by name. Tolerant of IndeeHub's field spellings
across versions, and absent/stopped/empty all yield an empty list rather
than failing the caller.
action_key includes the scope, so listing peers cannot be replayed as listing
own files. 15/15 assistant::tools.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Two more on-device UAT findings:
1. Deny-retry loop: the model, told 'the user declined', simply called the
tool again — each retry minted a fresh pending and re-opened the dialog
(T-13-50 habituation, mechanized). ToolExecCtx now remembers declined
actions for the turn, keyed by confirm::action_key — the same canonical
(tool_name, validated_args) identity the nonce binds — and execute_tool
refuses a re-ask before the gate, minting nothing. Regression test
declined_action_never_reprompts_same_turn.
2. Timeout chain: rpcClient's 15s default aborted every confirmable turn
client-side while the node kept the pending alive — the next turn then
re-announced it (modal over and over) and every wait read as 'timed
out'. assistant.chat now rides a 420s timeout; AIUI's bridge goes
180s→430s so the host's error path (which also expires the dialog)
always fires first. Declined ToolResult text now also tells the model
to stop.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
On-device UAT: the operator was timed out mid-read (120s), the chat turn
returned 'declined' while the dialog was still up, and their Approve then
hit a dead entry ('no such pending confirmation', 13:37:12 log). Nothing
executed — the gate failed safe — but the UX was a lie in both directions.
- CONFIRM_TIMEOUT 120s→300s: human-speed per T-13-51's own rubric.
- ContextBroker dispatches aiui:tool-confirm-expired when a pending action
vanishes node-side (poll) or the turn ends; Chat.vue closes the modal on
it. Same host-only CustomEvent discipline; iframe has no path to it.
- Two new tests; 21/21 green across toolConfirm + chatAiuiEmbed.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Executor stopped deliberately for a session restart (bypass-permissions relaunch).
Executor's final report: 'cargo test assistant confirm-gate suite 28/28 green,
individual nonce test passes; committing Task 1 next — first verify the
tools.rs/grants.rs/backends diffs are formatting-only.'
Task 1 (D-07/D-11 confirm gate, backend) is implemented and test-green but this
checkpoint is verbatim-uncommitted-state, NOT the reviewed atomic Task 1 commit:
continuation executor should verify diffs, then reset --soft or commit-on-top
into proper feat(13-08) task commits. Task 2 (ToolConfirmModal.vue trusted
chrome, Chat.vue + contextBroker.ts wiring) is partially built, tests written.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- confirm.rs: ConfirmGate/PendingConfirmation/Confirmed/PendingSnapshot/
ResolveRefusal API skeleton (request/resolve/mint_nonce/build_description
still todo!()) plus the five named confirm tests: S-02 nonce binding,
S-03 no-model-text, S-08 distinct resources, S-09 restart drops pending,
timeout declines, and the no-shared-lock-across-the-wait case
- mod.rs: ToolExecCtx gains the confirm gate (global by default, injectable
for tests) and the S-01 destructive_tool_requires_confirm test with a
seeded installed-app snapshot
- verified RED: 7 new tests fail (todo! cores + unfilled destructive branch)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>