a7368b8b1dfd56122916ed0f803939bf0bba934a
8
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>
|
||
|
|
58c759c149 |
feat(assistant): content_list gains scope — peers, purchased and IndeeHub films
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
c098124d93 |
test(13-05): assert the D-09 ceiling over the whole registry, not a hardcoded tool list
Task 3: four registry-wide structural tests in tools.rs, iterating registry() so a future tool that crosses the D-09 ceiling fails CI rather than depending on a reviewer noticing: - registry_never_exposes_excluded_authority (S-04/T-13-24): scans every ToolDef's name+description for EXCLUDED_AUTHORITY_TERMS. - read_tools_never_confirm (S-07/T-13-31): every non-destructive tool executes via the real execute_tool choke point without raising anything confirmation-shaped. bitcoin_status/network_status excluded from live execution (their handlers make real outbound network calls that would make this test flaky on a sandboxed box); their destructive:false placement is still covered by the other assertions. - loop_is_bounded (S-13/D-05): MAX_TURNS is enforced, and 3 consecutive malformed-argument calls for the same tool name abort the turn with an apology before a 4th scripted backend turn is ever polled. - every_tool_has_explicit_category_and_destructive: sanity-checks the registry has exactly the 13 hand-written tools (4 destructive) that made it in, as a runtime backstop to the acceptance criteria's static grep for `..Default::default()`. Negative-case demonstration (per the plan's acceptance criteria): a hypothetical `wallet_send_sats` tool with a description mentioning "spending sats" trips EXCLUDED_AUTHORITY_TERMS's "spend" term, verified by tracing the exact haystack-contains logic registry_never_exposes_excluded_authority runs (see 13-05-SUMMARY.md for why this was traced rather than executed). Co-Authored-By: Claude Opus 5 (1M context) <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>
|