9abc162394c5f8eea6bdc48b7c4ee633eca87093
9
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
9abc162394 |
fix(aiui): the content surface renders what the assistant found
Four defects, one visible symptom: a correct prose answer beside an
empty grid.
1. The assistant's curated RPC bridge had an arm only for
`content.list-mine`. `tools.rs` mapped the `peers`, `purchased` and
`films` scopes onto three real, dispatcher-registered handlers that
`assistant_dispatch_tool` had never heard of, so every non-"own"
scope died on its catch-all. Downstream that read as "the peers have
no content" — it was a missing match arm, and the tool never ran.
Regression test added: every scope the schema advertises must reach a
real handler.
2. `content.browse-all-peers` wrapped its whole fan-out in one
`timeout(..).unwrap_or_default()`, which DISCARDED every completed
batch the moment the budget expired. One slow peer turned a
partly-successful browse into "0 reached, 16 unreachable". Observed
live on archi-dev-box: back-to-back calls returned real peer items,
then nothing. Now accumulates per batch and checks a deadline between
them, so partial results always survive. Budget 20s -> 45s: two
batches of eight at a 10s per-peer timeout had no headroom at all.
3. `assistant.chat` returned only `{ text }`. The structured results of
any content tool the turn ran were dropped inside the loop, so the
surface had nothing to render. The turn now carries them through
(captured raw, before the untrusted wrap, since they go to a renderer
that treats every field as inert data, never back into the prompt).
4. The adapter classified images as 'excluded' and dropped them. A node
sharing mostly photos rendered as an empty grid while AIUI's image
grid sat unused. Images now have a bucket, with the paid-lock and
extension-fallback handling audio and video already had.
Also: the panel says "Loading…" while a turn is in flight and "Nothing
found" when it comes back empty, instead of leaving the previous
query's heading standing as though it answered this one; the system
prompt tells the model to call the content tool and summarise rather
than re-list what the cards already show; and a refused tool now names
its permission category so the trusted chrome can offer the settings
screen instead of leaving "I don't have a tool for that" as the only
clue.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
8ba6041251 |
feat(13-13): D-05 prepaid budget — arithmetic ceiling, hard stop, D-04 chain complete
Completes D-04's backend chain: Ollama -> Claude -> Routstr (budget-gated), and wires D-05's operator-set prepaid allowance as a hard, arithmetic stop a prompt-injected model can never cross. - assistant/mod.rs: `AssistantBudget` (allowance_sats/spent_sats, persisted 0600 under data_dir/assistant/budget.json, mirroring Grants::load/save exactly — a missing/corrupt file defaults to a ZERO allowance, D-16's "default closed" applied to money). `payment_policy()` builds a `PaymentPolicy` from ONLY these two persisted fields — no parameter accepts anything model/tool/provider-influenced, which is what makes the ceiling arithmetic rather than a policy an injected model could argue with. `record_spend()` persists a successful payment and raises a one-time 80%-threshold owner notice (AI-SPEC §7b). New typed `BudgetExhausted` error (downcastable via anyhow) is the signal `loop_.rs` distinguishes from an ordinary transport error. - assistant/loop_.rs: `run_loop` downcasts a `BudgetExhausted` out of the backend's `Err` and returns `Ok` with a plain-language stop message — no retry, no re-price, no partial spend, no fall-through to a different provider at a different price. Verified to actually matter: temporarily replaced the terminating `return` with `continue` and confirmed `zero_budget_stops_loop_without_retry` goes red (the backend gets retried 8x to MAX_TURNS and the turn errors instead of stopping cleanly); restored and reconfirmed green (13-13-SUMMARY.md records the observed failure). - assistant/backends/mod.rs: `select_backend` now takes `&RpcHandler` (was `&Path`) to also read the Tor-proxy config; completes the D-04 chain — Routstr never selected when the operator's allowance is zero (Claude alone instead), otherwise chained as Claude's fallback (Ollama -> Claude -> Routstr, each leg reached only when the priors are unavailable). New `BackendId::Routstr` variant. - assistant/backends/routstr.rs: the payment-decline arm now returns the typed `BudgetExhausted` (was a plain bail in Task 2's commit, per the plan's own "handled in Task 3" note); a successful payment records spend against the persisted budget immediately (the Cashu proofs are already committed at that point, regardless of whether the subsequent chat HTTP call itself succeeds). - api/rpc/assistant_chat.rs: `assistant.budget-get`/`assistant.budget-set` RPCs (routed through the existing single `assistant.` dispatcher arm — dispatcher.rs untouched) and a `nostr_tor_proxy()` accessor for select_backend's onion-preference decision. Named tests (assistant::tests::): zero_budget_stops_loop_without_retry (S-12), zero_allowance_never_selects_routstr, ceiling_is_not_a_function_of_model_output, injection_loop_against_low_budget_does_not_overspend (EV-17) — all pass. Full assistant:: suite: 91/91. Full crate suite: 1235/1235 (2 pre-existing ignored, unrelated). dispatcher.rs and Cargo.toml untouched. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
f1e50fbfd8 |
feat(13-12): G-B3 rate limit + read-only injection loop bound and owner notices
rate_limit.rs: assistant.chat gets its own request log keyed by AUTHENTICATED SESSION (not client IP, per 13-AI-SPEC.md §6 G-B3's own spec — an operator's session can roam across IPs within one sitting), on the SAME EndpointRateLimiter struct rather than a second limiter type. check_session/record_session_request enforce a hard ceiling (60/5min); session_soft_threshold_reached (30/5min) is checked separately so the call site can raise an owner notice before the hard refusal ever fires. Wired into assistant_chat.rs's handle_assistant_chat (Rule 3 — the plan's own declared intent, "assistant.chat is rate-limited per authenticated session," has no other call site to reach the real RPC surface) and into the existing 5-minute cleanup task in api/rpc/mod.rs. loop_.rs: run_loop now tracks whether D-10-wrapped untrusted content is present in context (seeded and re-checked as new tool results arrive mid-loop), counts grant refusals split by that flag via AssistantCounters::note_grant_refusal (a burst WITH untrusted content raises a Security notice — something in shared content may be trying to trigger actions; the same burst WITHOUT it raises a Ux/config notice instead, so probing is never confused with misconfiguration, T-13-83), counts turns-per-request, and counts MAX_TURNS-reached (3+ in one session raises an owner notice) right before the loop's own bail — this is EV-13's read-only injection loop, the one case the confirm gate structurally cannot see because reads never confirm. mod.rs: ToolExecCtx gains a `counters: Arc<AssistantCounters>` field (defaulting to the process-wide global_counters(), overridable per-test via with_confirm_gate_and_counters) so loop_.rs's counting has somewhere to write and tests can assert against an isolated instance without polluting concurrently-running tests. read_only_injection_loop_terminates_and_is_counted (EV-13) and grant_refusals_with_untrusted_content_are_a_security_signal (T-13-83) both pass. Full `cargo test --package archipelago` (1211 tests) green — the existing rate-limited RPC methods are unaffected by the new session-keyed limiter. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
265ba5ab19 |
feat(13-12): D-10 untrusted-content boundary — wrap_untrusted, per-call random token
assistant/untrusted.rs: wrap_untrusted(label, text) wraps peer-supplied text (filenames, log lines, mesh/peer status) in a delimiter block whose token is freshly randomized on every call via the in-tree rand crate — never a module constant, never derived from content. A forged closing boundary using a guessed/fixed token cannot terminate the real block early (EV-11). tools.rs: wrap_tool_result_if_untrusted wires this in for content_list, app_logs and mesh_status (the tools whose results carry peer-authored text); every other tool result passes through unwrapped. loop_.rs's execute_tool calls it at the exact point a successful ToolResult is constructed, before that content ever becomes part of a ChatMessage. No pattern-stripping or keyword-blocklist filter was added (D-10 rejects that approach by name) — the delimiter and D-11's confirm gate are two independent layers. Four scripted-worst-case tests in mod.rs prove the gate still holds even when a compromised model acts on an injected imperative (injected_instruction_does_not_grant_authority), a forged closing delimiter plus fake operator turn (forged_closing_delimiter_does_not_escape_block), or an injected mislabel attempting to hide the real action from the human (injected_mislabel_still_confirms_real_action) — plus wrap_untrusted_token_is_per_call (tools.rs) asserting the per-call token itself. Zero packages added — rand 0.8.5 already in-tree. 56/56 assistant:: tests pass in this task's own isolated state. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
3da9928cc9 |
feat(13-10): node-side chat history, scoped by caller, compacted not truncated
history.rs persists the ChatMessage transcript under data_dir (D-08), keyed by a HistoryKey derived from CallerScope so an operator's AIUI session and a mesh peer's transcript are structurally distinct files, not two rows a filter could forget. Writes are atomic (temp sibling + rename, matching music/index.rs::save_atomic's precedent) and 0600, following grants.rs's convention. Tool results longer than MAX_TOOL_RESULT_CHARS are truncated with a visible marker before entering history -- a new, assistant-scoped constant, never assist.rs's LoRa-airtime-tuned reply cap. Once the transcript exceeds KEEP_VERBATIM_TURNS, older turns fold into a running summary extended incrementally as turns age out, never regenerated from the full transcript. Wallet/files-category tool-call arguments are never persisted (AI-SPEC §7b's field policy applied to storage, not only tracing) -- categories are resolved by the caller from the same tools registry execute_tool uses, so history.rs never re-derives a second, driftable category list. Nothing reachable from confirm.rs's pending- confirmation state has a parameter path into this module at all (S-09 stays true structurally). assistant.history / assistant.clear-history route through 13-01's existing assistant.* dispatcher arm (dispatcher.rs untouched), each scoped to the calling session's own HistoryKey. run_loop (loop_.rs) now returns (answer, full_history) instead of just the answer string -- structurally necessary so chat() (mod.rs) can persist the tool-call/tool-result messages the loop built internally, not only the user question and final answer (Rule 3, mirroring 13-05's precedent of touching a file outside its own plan's files_modified list when the plan's own intent requires it). chat() persists this turn after run_loop returns; it does not yet feed prior persisted turns back into live model context -- a documented, deliberately scoped follow-up (see mod.rs's chat() doc comment and the plan SUMMARY). 8 new tests under assistant::history::tests::, including operator_and_mesh_transcripts_are_separate and wallet_tool_arguments_never_reach_the_transcript (asserted against both the deserialized struct and the raw on-disk bytes). Full assistant:: suite: 50/50 (42 baseline-after-Task-1 + 8 new); confirm::tests:: restart_drops_pending_not_executes still passes -- S-09 not weakened. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
2d1f09d8c8 |
fix(13-08): declined actions never re-prompt + timeout chain covers the human wait
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> |
||
|
|
fc09d7a292 |
wip(13-08): checkpoint before operator session restart — Task 1 GREEN (28/28), Task 2 in progress
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> |
||
|
|
90706fe053 |
feat(13-05): expand curated tool registry to full D-06/D-09 allowlist, wire D-16 default-closed grants
Task 1: registry() grows from the tracer's single system_disk_status tool to the full 13-tool D-06 curated allowlist (9 read tools, 4 destructive write tools), each hand-written with its own JSON Schema, PermissionCategory and destructive flag -- nothing derived from api::rpc's method table. Adds EXCLUDED_AUTHORITY_TERMS (D-09's excluded authority, scanned by Task 3's registry-wide test), SETTABLE_KEYS/READABLE_SETTINGS_KEYS (AIUI-02's hand-picked settings surface, claude_api_key permanently absent from SETTABLE_KEYS), tools::dispatch (per-tool RPC dispatch) and tools::validate_business_rules (allowlisted-key / installed-app-id validation that runs before the destructive/confirm gate so a plainly-wrong request is refused with the real reason instead of the generic "not yet implemented" placeholder). assistant_dispatch_tool gains a params argument and the RPC method table Task 1's tools need. Task 2: grants.rs adds Grants (D-16 default-closed permission-category store, persisted 0600 under data_dir/assistant/grants.json; a missing file is default_closed(), never permissive). CallerScope::granted_categories becomes async and reads the persisted store instead of a hardcoded default; CallerScope::Mesh gains an `authorized` field so a mesh peer's ceiling is never wider than the operator's own grants. ToolExecCtx gains the AI-SPEC S-13 consecutive-validation-failure counter (>2 failures for the same tool name aborts the turn with an apology, checked in run_loop). build_system_prompt appends only currently-granted-category tools' names/descriptions -- an ungranted tool never appears in the prompt string (defense in depth; the execute_tool grant re-check is the actual gate). assistant_chat.rs adds assistant.list-tools / assistant.grants-get / assistant.grants-set, all routed through the existing single assistant.* dispatcher arm (dispatcher.rs untouched, verified by git diff --exit-code). dispatcher.rs is not touched -- all new RPC surface goes through 13-01's assistant.* prefix arm. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
fe6ccff73c |
feat(13-01): Rust assistant spine — one curated tool, one backend, one RPC surface
D-01/D-02/D-06 tracer slice: a new crate::assistant module (CallerScope,
PermissionCategory, ToolExecCtx, chat()) runs a multi-turn tool-calling loop
(run_loop/execute_tool, MAX_TURNS=8) against a curated single-tool registry
(system_disk_status, hand-written JSON Schema — no schemars) via a Claude
Messages API backend. execute_tool is the single choke point: unknown tools
are refused not ignored, D-16 category grants are re-checked even though the
system prompt already omits ungranted tools, and every real tool dispatches
through the SAME handle_system_disk_status RPC handler every other
authenticated caller uses (assistant_dispatch_tool bridge in
api/rpc/assistant_chat.rs) — never an AI-only backdoor.
assistant.chat is registered in dispatcher.rs as a single guarded
`m if m.starts_with("assistant.")` arm reached only after the existing
session-cookie + CSRF + role.can_access() gate in api/rpc/mod.rs — asserted
directly by assistant_methods_require_session against the live
UNAUTHENTICATED_METHODS list (visibility only widened to pub(crate) for that
assertion; the list's contents are untouched, per the Phase-10 hard
constraint).
Key read from data_dir/secrets/claude-api-key — the same path
mesh/rpc/mesh/assistant.rs already probes — never a second key location.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|