60 KiB
Phase 13: AIUI — Conversational Node Control & Content Surfaces - Research
Researched: 2026-08-03 Domain: LLM tool-calling bridge (Rust), Cashu/Nostr paid-inference integration, cross-repo Vue embed architecture, media/content indexing Confidence: MEDIUM-HIGH — the Rust and neode-ui sides are fully source-verified; the AIUI repo side is fully source-verified (cloned, read directly); Routstr protocol details are CITED from official docs (not independently protocol-tested against a live Routstr node, which does not exist in this environment).
<user_constraints>
User Constraints (from CONTEXT.md)
Locked Decisions
Where the agent loop lives
- D-01: The agent loop (model call → tool call → result → model) runs node-side in Rust. The
archipelagobinary owns the loop, the tool registry, and the model key. AIUI becomes a thin chat client. Rationale: the key never reaches the browser; tool authorization sits where session auth already lives; Pine/voice can reuse the same tools later. Reversibility: costly. - D-02: One assistant, many front doors. Extend the existing mesh assistant into a shared service: one tool registry, one backend selector, one place keys live. Mesh/LoRa, AIUI chat and (later) Pine voice are callers distinguished by permission scope. The existing peer-facing controls (
trusted_only,allowed_contacts,denied_askers) are a per-caller scope mechanism that already exists. - D-03: Split by nature. The node-side registry owns everything that reads or changes the node (system, bitcoin, network, wallet, files, media). The existing
ContextBrokerkeeps only what must run in the browser (navigate,open-app,launch-app,theme) and remains the consent surface pushingpermissions:update. - D-08: Chat history lives node-side in the per-node data dir (
/var/lib/archipelago), inheriting the node's backup/factory-reset/LUKS story.
Model backends
- D-04: Backend chain is local Ollama first, with Claude and Routstr as fallbacks. Node data never leaves the node when a local model is available. Routstr is explicitly in scope at the user's request — an OpenAI-compatible endpoint paid per request in Cashu ecash, providers/models/prices discovered over Nostr.
- D-05: Routstr spending is authorized by a prepaid budget the user sets. Inference spends silently within the allowance, then stops and asks. The ceiling is hard. Reversibility: reversible.
- D-07: The local model does get tools, and every write needs confirmation regardless of backend. Backend choice stays a privacy decision, not a safety one.
Authority and sandboxing
- D-06: Tools are a curated allowlist of hand-written tools — each with its own schema, permission category, and destructive/confirm flag. The model never sees the full RPC surface. No auto-generation from the dispatcher. Reversibility: reversible (adding tools is additive).
- D-09: First-cut authority is reads within granted categories + app lifecycle (start/stop/restart) + settings writes. Explicitly excluded from chat reach: keys, seeds, wallet spends, federation trust, factory reset. Reversibility: costly.
- D-10: Tool authority never derives from content. Peer-supplied text enters the context inside explicit untrusted-content delimiters marking it as data, not instructions. Pattern-stripping filters were considered and rejected.
- D-11: Write confirmations render in neode-ui's trusted chrome, outside the iframe, drawn by the host from the node's own description of the pending action — never by AIUI, never from model-authored text. Uses the Teleport-to-body modal pattern. Reversibility: costly — the load-bearing anti-spoofing property.
- D-16: All 10 permission categories (
apps,system,network,wallet,files,media,search,ai-local,notes,bitcoin) default closed on a fresh node. - Hard constraint from Phase 10: the
UNAUTHENTICATED_METHODShard-refuse gates and the loopback/auth boundaries must hold with AIUI on the other side of them. Not to be widened.
Content surfaces
- D-12: Feed the existing grids from Archy, replacing the LLM-synth source. AIUI's design is kept exactly; what fills
FilmGrid,SongGrid,NewsGrid, the detail views changes to real records. Reversibility: reversible — the grids are prop-driven. - D-13: Build a real music library — albums, artists, tracks, tag/metadata extraction, an index that stays fresh. Lands as its own wave inside Phase 13, not blocking the rest. Reversibility: one-way — a persisted data model with a migration cost once nodes have indexed libraries.
- D-14: IndeeHub and peer video are surfaced through the content + paid-unlock subsystem that already exists (invoices,
X-Payment-Token, Range streaming). No new payment rail. The cross-node "archipelago content source" is deferred.
Delivery and the two-repo split
- D-15: AIUI is built and shipped with the frontend, versioned and verified — rsync path kept but made deliberate: AIUI's commit pinned,
VITE_BASE_PATH=/aiui/enforced by the build script, a post-deploy check that fetches a live asset. Making AIUI a signed-catalog app was considered and rejected for this phase. - D-17: AIUI keeps its standalone mode; embedded mode delegates to the node. Dev loop stays fast — no node required to work on the UI.
- D-18: Push access to the AIUI repo is confirmed before planning starts — CONFIRMED by the orchestrator per the phase brief; plan freely against
git.tx1138.com/lfg2025/AIUIbranchdevelopment.
Claude's Discretion
- What the music library indexes over (own filebrowser
Musicfolder, peer audio, or both), the tag-extraction library, and where the index lives — within D-13's bounds. - Streaming/token delivery for chat responses; context-window budgeting over node data.
- Which specific tools make the first curated allowlist, within D-09's authority ceiling.
- Routstr provider selection strategy among Nostr-advertised providers.
- Per-category mapping of the 10 permission categories onto individual tools.
Deferred Ideas (OUT OF SCOPE)
- Cross-node "archipelago content source" with payments (federation distribution + payments feature; own phase).
- AIUI Nostr integration polish (carried from Phase 2's deferred list).
- Reviving
ContentPanel.vueand the plugin-renderer path (considered and rejected; dead-code cleanup, not architecture). - AIUI's dev-only Vite middleware beyond what D-12 replaces (
vite-tmdb,vite-rss,vite-web-search,vite-fs,vite-music-search,vite-dev-chats). - Pine voice reusing the tool registry (D-02 makes it possible; wiring voice is not in this phase).
- Not in scope (from the phase Domain block): cross-node content distribution with payments; wallet spends, seed/key operations, federation trust changes, factory reset as chat-reachable actions; Nostr integration polish; reviving the dead
ContentPanel.vuearchitecture. </user_constraints>
<phase_requirements>
Phase Requirements
| ID | Description | Research Support |
|---|---|---|
| AIUI-01 | Human-language node control — typed chat request reaches a real node action | §1 (gating question, verified), §2 (tool-calling loop), §4 (confirm gate) settle the mechanism; §"Curated Tool Allowlist" gives concrete RPC candidates |
| AIUI-02 | Conversational settings — system settings reachable by conversation, scoped to grants | Same tool-registry mechanism as AIUI-01; system.settings.get/system.settings.set are existing RPCs to wrap as tools |
| AIUI-03 | Content surfaces made real — peer files, music, IndeeHub movies, owned/paid content render live | §5 (content surfaces) maps ContentItem/content.* RPCs onto AIUI's Film/Song/Podcast prop shapes; §6 (music library) covers the one genuinely new data domain |
| AIUI-04 | Sandboxed by construction, permissioned by the user | §4 (confirm gate mechanism) + new §"Same-Origin Sandbox Gap" (a verified architectural finding: today's iframe embed has no hard browser-enforced boundary) |
| AIUI-05 | Delivery and build — AIUI reaches nodes through a real, verifiable update path | §7 (delivery) — current deploy scripts, the VITE_BASE_PATH requirement, and the missing live-asset check are all verified from source |
| AIUI-06 | Verified on device, embedded iframe, mobile included | §Validation Architecture |
| </phase_requirements> |
Summary
AIUI today is not a dormant blank canvas waiting for wiring — it is an actively working, unauthenticated, key-holding proxy straight to Anthropic, running in production nginx config on every node that has had setup-aiui-server.sh run against it. The canonical image-recipe/configs/nginx-archipelago.conf proxies /aiui/api/claude/ to a standalone Python HTTP server (claude-api-proxy.py, port 3142, its own systemd unit, its own ANTHROPIC_API_KEY env var — a different key than the Rust daemon's data_dir/secrets/claude-api-key) with no session-cookie gate at all ("API key managed by proxy, no session gate needed" — verified in the nginx config comment). This proxy bypasses the entire Rust JSON-RPC dispatcher: no UNAUTHENTICATED_METHODS gate, no CSRF check, no RBAC role.can_access() check, no involvement of session::extract_session_cookie. Anyone who can reach the node's web port can spend the node owner's Claude API budget with zero authentication. This is a pre-existing, currently-live exposure this phase's D-01 (move the agent loop node-side into the authenticated Rust surface) directly closes as a side effect of doing the phase correctly — but it needs to be named explicitly as a finding, because it is more severe than "chat can't act on the node" and is not mentioned in CONTEXT.md.
Both halves of CONTEXT.md's central gating claim are verified true against source: mesh/listener/assist.rs's call_ollama posts to Ollama's /api/generate (not /api/chat) with a bare prompt string and no tools field; call_claude posts to the Anthropic Messages API with a single user message and no tool_use/tools field. Both are single-shot Q&A, no loop. dispatcher.rs registers only mesh.assistant-status/mesh.assistant-configure — grep for "pine. across the entire dispatcher returns nothing; Pine has no RPC surface, and what CONTEXT.md called "the intent→action path Pine already proves" is, on inspection, Home Assistant's own intent_script/Assist framework seeded by package/pine_ha.rs — four read-only hardcoded intents (block height, peer count, sync status, Lightning balance) answered from REST-sensor state, not a Rust-side action-executing loop. HA's Claude conversation agent does get real LLM tool-calling via llm_hass_api: ["assist"], but only over HA's own intents — none of which write to the node. So even Pine's voice path does not yet prove an action-taking loop; it proves Q&A-with-structured-intents at the HA layer. This corrects CONTEXT.md's framing and matters for scoping AIUI-01's "first tool-calling loop in this codebase" honestly.
On the AIUI side (cloned, development branch, read directly): useAI.ts's chat send path calls streamClaude/streamOpenRouter against ${BASE}api/claude/v1/messages / ${BASE}api/openrouter — i.e., exactly the nginx proxy above, or the browser's own vaulted API key. There is no client-side tool-calling either: the "Archy actions" AIUI's system prompt describes (open-app, install-app, read-file, tail-logs, navigate) are informational prose injected into the system prompt by useArchy.ts's buildArchyContext(); only readFile/tailLogs are ever actually invoked by AIUI code, and both are called directly by UI components — never parsed out of a model response. AIUI has zero machinery today for turning an LLM's stated intent into an executed action; everything the model "does" today is either prose or a [[tag:...]] regex match consumed by contentExtraction.ts/useContentPanel.ts to render a content card. This phase must build the tool-calling protocol from scratch on both sides.
A real anti-spoofing confirmation pattern already exists to extend, not invent: contextBroker.ts's install-app handler dispatches a CustomEvent('aiui:install-request') for neode-ui's own UI to render a confirmation, then awaits aiui:install-response with a 60s timeout — this is D-11's mechanism today, just for one action type. It needs a second: aiui-protocol.ts's AIActionType union has no tool-call/confirm member yet.
For Routstr, the phase is not starting from zero on the payment side: crate::swarm::payment::auto_pay_token (used today by streaming.prepare-payment, the swarm content-payment path) already does exactly D-05's job — build a cashuA token for a given price against a set of accepted_mints, hard-capped by a PaymentPolicy::with_budget, degrading to None (never erroring) when unaffordable. Routstr's documented contract (CITED, not independently tested) accepts payment as Authorization: Bearer cashuA... or an X-Cashu header on an OpenAI-compatible POST /v1/chat/completions, and advertises providers via Nostr kind 38421 events — nostr-sdk = "0.44" is already a dependency with a working build_nostr_client (Tor-proxy aware) in nostr_discovery.rs to subscribe from.
For content surfaces, ArchyAppsGrid.vue is confirmed dead (only referenced by dead ContentPanel.vue and its own test). All six of AIUI's "real data" Vite plugins are confirmed configureServer-only or configureServer+configurePreviewServer-only (verified per-file), so none run against the static dist/ a node actually serves. vite-fs.ts's hardcoded /Users/dorian/Projects is further confirmation this was never meant to reach a node. The grids' prop shapes (Film, Song, Podcast, etc. — all display-oriented with posterUrl/coverUrl/sources[]) do not match Archy's ContentItem (id, filename, mime_type, access, availability) — an adapter layer is required, not a straight pass-through.
Primary recommendation: Build one new node-side "assistant" service module (extending, not replacing, mesh/listener/assist.rs's backend-calling code) that owns a hand-written tool registry, a multi-turn loop per backend, and new RPC methods (assistant.chat, assistant.confirm-tool, assistant.list-tools, assistant.history) reached only via neode-ui's contextBroker.ts (new chat:request/chat:response/tool:confirm-request postMessage types) — never by AIUI fetching the RPC endpoint directly, even though nothing currently stops it (see the Same-Origin Sandbox Gap finding). Ship content surfaces as a straight content.* → grid adapter first (D-12, no new backend work beyond what exists), then land the music library (D-13) as its own wave using lofty for tag extraction. Treat the currently-live Claude proxy exposure and the iframe's lack of a hard sandbox boundary as findings the plan must explicitly decide how to handle (fix, accept-with-mitigation, or defer with a named risk) rather than silently working around.
Architectural Responsibility Map
| Capability | Primary Tier | Secondary Tier | Rationale |
|---|---|---|---|
| Agent loop (model call → tool call → result) | API/Backend (Rust daemon) | — | D-01: key + tool authority must stay server-side |
| Tool registry + permission scoping | API/Backend (Rust daemon) | — | D-06/D-09: curated allowlist, RBAC-adjacent, must not be derivable from the browser |
| Chat transport (AIUI ↔ Archy) | Browser/Client (postMessage bridge) | API/Backend (RPC over the neode-ui session) | AIUI has no session cookie path of its own by design; neode-ui's contextBroker.ts is the only channel today, and D-03 keeps it that way |
| Write-confirmation UI | Frontend Server / Browser (neode-ui trusted chrome) | — | D-11: must render outside the iframe, Teleport-to-body, drawn from node-authored text |
| Browser-only actions (navigate, open-app, theme) | Browser/Client (ContextBroker) |
— | D-03: nothing server-side can perform a client-side navigation |
| Routstr payment (Cashu token build) | API/Backend (Rust daemon, swarm::payment) |
— | Wallet/mint state is server-side; reuses existing auto_pay_token |
| Routstr provider discovery (Nostr) | API/Backend (Rust daemon, nostr-sdk) |
— | Relay connections should route through the node's existing Tor-proxy-aware Nostr client, not the browser |
| Content surfaces (peer files, IndeeHub, paid content) | API/Backend (content_server.rs, content.* RPCs) |
Browser/Client (AIUI grids, prop-adapted) | Data ownership and access control (AccessControl::Paid) must stay server-enforced; AIUI only renders |
| Music library index | Database/Storage (/var/lib/archipelago) + API/Backend (indexer) |
Browser/Client (SongGrid consumer) |
D-13: a persisted, migration-sensitive data model — indexing must not run in the browser |
| Media playback (Range streaming) | API/Backend (existing /content/<id>, /api/raw, peer proxy) |
Browser/Client (GlobalAudioPlayer) |
Already solved; grids need data, not a new transport |
| AIUI static delivery | CDN/Static (nginx /aiui/ location, frontend rsync) |
— | D-15: built artifact, not a live service |
Standard Stack
Core
| Library | Version | Purpose | Why Standard |
|---|---|---|---|
reqwest |
0.11 (already in archipelago/Cargo.toml, rustls-tls+socks+json+stream features) |
HTTP client for Ollama/Claude/Routstr calls | Already the codebase's only HTTP client; socks feature already present for Tor-proxied calls |
serde_json |
1.0 (in-tree) | Tool-call schema construction, RPC params | Already universal in this codebase |
nostr-sdk |
0.44 (in-tree, nip04+nip44 features) |
Routstr provider discovery (kind 38421 subscribe) | Already a dependency with a working Tor-aware client builder (nostr_discovery.rs::build_nostr_client) — no new crate needed |
tokio |
1, full features (in-tree) |
Async runtime for the multi-turn tool loop | Already universal |
Supporting (new, for the music library — D-13)
| Library | Version | Purpose | When to Use |
|---|---|---|---|
lofty [ASSUMED — training-knowledge recommendation, not yet added to Cargo.toml; registry existence confirmed] |
0.24 (crates.io max_version, VERIFIED: crates.io API, 808K downloads, repo github.com/Serial-ATA/lofty-rs) |
Read ID3/FLAC/M4A/OGG/WAV/APE tag metadata (title/artist/album/track/duration) in one unified API | Primary recommendation for D-13's tag extraction — broad multi-format support in one crate, avoids needing a separate parser per container format |
Alternatives Considered
| Instead of | Could Use | Tradeoff |
|---|---|---|
lofty (metadata-only) |
symphonia (0.6, VERIFIED: crates.io, 9.4M downloads, github.com/pdeljanov/Symphonia) |
Symphonia is a full audio decoder (needed for playback/transcoding, not tagging) — much heavier dependency surface for a job that's purely "read tags." Not needed here since playback already goes through the existing Range-streaming path, not server-side decode. |
lofty (multi-format) |
id3 (1.17.1, VERIFIED: crates.io, 11.2M downloads, codeberg.org/polyfloyd/rust-id3) |
ID3-only (MP3). Higher download count reflects broad MP3-tagging use elsewhere, not superiority for a library that must also cover FLAC/M4A/OGG. |
| Curated hand-written tool allowlist (D-06, locked) | Auto-generate tool schemas from dispatcher.rs's method table |
Explicitly rejected by D-06 — the model must never see the full RPC surface; every capability must be a deliberate decision |
Installation:
# cargo add is run from core/ per CLAUDE.md
cd core && cargo add lofty --package archipelago
Version verification: lofty 0.24.0, symphonia 0.6.0, id3 1.17.1 confirmed live via the crates.io API (crates.io/api/v1/crates/<name>) on 2026-08-03 — VERIFIED: crates.io registry, not merely a training-data guess. reqwest/serde_json/nostr-sdk/tokio versions read directly from core/archipelago/Cargo.toml — VERIFIED: in-tree Cargo.toml.
Package Legitimacy Audit
| Package | Registry | Age | Downloads | Source Repo | Verdict | Disposition |
|---|---|---|---|---|---|---|
lofty |
crates.io | Long-running project (Serial-ATA/lofty-rs, active) | 808,246 total | github.com/Serial-ATA/lofty-rs | Not run through gsd-tools query package-legitimacy check in this session (tool unavailable in this environment) — manually checked: real GitHub org, active repo, substantial download count, no suspicious signals found |
[ASSUMED — recommend a checkpoint:human-verifybeforecargo add] |
symphonia |
crates.io | Long-running (pdeljanov/Symphonia) | 9,452,628 total | github.com/pdeljanov/Symphonia | Same manual-check basis — not needed for this phase's scope (tagging only), listed for completeness | Not adopted — informational only |
id3 |
crates.io | Long-running (rust-id3) | 11,202,064 total | codeberg.org/polyfloyd/rust-id3 | Same manual-check basis | Not adopted — informational only |
Packages removed due to [SLOP] verdict: none.
Packages flagged as suspicious [SUS]: none by manual inspection, but lofty was not run through the automated package-legitimacy check seam (tool unavailable in this research session) — the plan must gate its cargo add behind a checkpoint:human-verify per the package-legitimacy protocol's own fallback rule for [ASSUMED] packages.
Architecture Patterns
System Architecture Diagram
Browser (neode-ui page, authenticated session)
┌─────────────────────────────────────────────────────────────────────┐
│ Chat.vue │
│ ┌───────────────────────────────┐ postMessage (same-origin, │
│ │ <iframe src="/aiui/..."> │◄──origin-checked by broker)──┐ │
│ │ AIUI (thin chat client) │ │ │
│ │ - renders chat UI │──chat:request (userText)───►│ │
│ │ - NO model key, NO RPC │◄─chat:response (token/done)─┤ │
│ │ session of its own │ │ │
│ │ - content grids (prop-fed) │◄─context:response (films, │ │
│ │ │ songs, ...)────────────────┤ │
│ └───────────────────────────────┘ │ │
│ ▼ │
│ ContextBroker (contextBroker.ts) ── rpcClient.call() ── uses the │
│ page's OWN session cookie + CSRF token (same auth as every other │
│ neode-ui RPC call) │
│ │ │
│ │ new: assistant.chat / assistant.confirm-tool / │
│ │ content.* / streaming.* RPCs (HTTP POST, session-gated) │
└───────────┼───────────────────────────────────────────────────────────┘
▼
┌───────────────────────────────────────────────────────────────────────┐
│ archipelago daemon (Rust) │
│ │
│ api::rpc::dispatcher — session + CSRF + RBAC gate (mod.rs:264-330) │
│ │ │
│ ▼ │
│ NEW: assistant service (extends mesh/listener/assist.rs's backend │
│ callers) — owns: │
│ - tool registry (D-06 curated allowlist, permission-tagged) │
│ - multi-turn loop per backend (Ollama /api/chat tools, │
│ Claude Messages API tool_use, Routstr OpenAI-shape) │
│ - pending-confirmation queue (D-11: node authors the confirm text) │
│ - chat history persisted under data_dir (D-08) │
│ │ │ │
│ ▼ tool call, permission-checked ▼ pending write │
│ existing RPC handlers (system.*, package.*, → confirm:request │
│ container-*, bitcoin.*, content.*, mesh.*) pushed to broker → │
│ — the SAME handlers every other authenticated neode-ui trusted │
│ caller uses, no new "AI-only" backdoor chrome modal │
│ │
│ Backend selection: Ollama (local, free) → Claude (secrets/claude- │
│ api-key) → Routstr (Nostr-discovered provider + Cashu budget via │
│ swarm::payment::auto_pay_token, D-05 hard cap) │
└───────────────────────────────────────────────────────────────────────┘
Recommended Project Structure
core/archipelago/src/
├── assistant/ # NEW — the D-02 shared service
│ ├── mod.rs # public API: chat(), confirm_tool(), list_tools()
│ ├── tools.rs # D-06 curated tool registry + schemas
│ ├── backends/
│ │ ├── ollama.rs # /api/chat with tools[] (extends assist.rs::call_ollama)
│ │ ├── claude.rs # Messages API with tools[] / tool_use blocks
│ │ └── routstr.rs # OpenAI-shape POST + Cashu payment attach
│ ├── loop_.rs # multi-turn tool-call loop, backend-agnostic
│ ├── confirm.rs # D-11 pending-confirmation queue
│ └── history.rs # D-08 node-side chat persistence
├── api/rpc/
│ └── assistant_chat.rs # NEW RPC handlers: assistant.chat, .confirm-tool, etc.
├── music/ # NEW — D-13 music library (own wave)
│ ├── index.rs # on-disk index format + freshness
│ ├── tags.rs # lofty-based extraction
│ └── mod.rs
neode-ui/src/
├── services/contextBroker.ts # EXTENDED — new chat:*, tool:confirm-* message types
├── types/aiui-protocol.ts # EXTENDED — new AIUIRequest/ArchyResponse variants
└── components/ # NEW — trusted-chrome confirm modal (Teleport to body)
Pattern 1: Tool-call confirmation via node-authored text (D-11)
What: The Rust assistant, not AIUI and not the model's raw text, writes the human-readable description of a pending destructive action. That description is pushed to neode-ui via a new postMessage type; neode-ui renders it in a Teleport-to-body modal outside the iframe; the user's yes/no is sent back over the same authenticated RPC channel (not postMessage) so the iframe cannot forge it.
When to use: Every tool call where destructive: true or confirm: true in the D-06 tool schema — which per D-07 is every write, regardless of backend.
Example (extends the existing install-app pattern, contextBroker.ts):
// Source: existing pattern at neode-ui/src/services/contextBroker.ts:140-196
// (install-app confirm flow — the model to extend for tool-call confirms)
window.dispatchEvent(new CustomEvent('aiui:install-request', {
detail: { requestId: id, appId, marketplaceUrl, version },
}))
const responseHandler = (e: Event) => {
const detail = (e as CustomEvent).detail as { requestId: string; confirmed: boolean }
if (detail.requestId !== id) return
window.removeEventListener('aiui:install-response', responseHandler)
// ... proceed or decline
}
window.addEventListener('aiui:install-response', responseHandler)
setTimeout(() => window.removeEventListener('aiui:install-response', responseHandler), 60000)
The new tool-confirm flow should NOT reuse aiui:install-request (that event is install-specific); it needs its own aiui:tool-confirm-request/response pair, driven by RPC-fetched (not postMessage-fetched) pending-action text so the iframe cannot inject the description.
Pattern 2: Backend-agnostic tool-call loop shape
What: Ollama's /api/chat (not /api/generate, which call_ollama uses today) accepts a tools array of {type: "function", function: {name, description, parameters}} and returns message.tool_calls. Anthropic's Messages API accepts tools: [{name, description, input_schema}] and returns content blocks of type: "tool_use"; the loop must send a follow-up tool_result content block keyed by tool_use_id. Both require re-invoking the backend after executing the tool, i.e. a real loop rather than the single call_ollama/call_claude request-response used by mesh assist today.
When to use: All three backends (Ollama, Claude, Routstr — Routstr is OpenAI-compatible, so its tool-calling shape matches OpenAI's tools/tool_calls, distinct from both Ollama's and Anthropic's shapes — three distinct wire formats to normalize).
Note: assist.rs's OLLAMA_TIMEOUT (60s) and MAX_REPLY_CHARS/chunking constants are mesh-airtime-specific and should NOT be reused as-is for the AIUI path, which has no radio bandwidth constraint — the new assistant module needs its own timeout/streaming constants.
Anti-Patterns to Avoid
- Auto-generating tool schemas from
dispatcher.rs's method table: explicitly rejected by D-06. Every tool must be a hand-written, reviewed decision — this is the only way "the model never sees the full RPC surface" stays true rather than becoming an implementation detail nobody re-checks. - AIUI fetching
/rpc(or any authenticated endpoint) directly: nothing in the current CSP or iframe attributes technically prevents this (see "Same-Origin Sandbox Gap" below) — but doing so would make the browser-sideContextBroker/aiui-protocol.tssandbox purely decorative. All new capability must be added as new postMessage message types, never as a new same-origin fetch from AIUI's own code. - Reusing
data_dir/secrets/claude-api-keyas the ONLY key ledger while theclaude-api-proxy.py/port-3142 path with its separateANTHROPIC_API_KEYenv var still exists: two live Claude credential paths with different auth postures is itself a landmine (see below) — the plan must decide to retire, consolidate, or explicitly gate the legacy proxy, not silently leave both running.
Don't Hand-Roll
| Problem | Don't Build | Use Instead | Why |
|---|---|---|---|
| Cashu token construction for Routstr payment | A new BDHKE/Cashu wallet client | crate::wallet::ecash + crate::swarm::payment::auto_pay_token (already in-tree, already budget-capped, already degrades to None on any failure) |
Exact fit for D-05's "prepaid budget, silent spend, hard stop" requirement — already tested (over_budget_declines_without_touching_wallet, zero_budget_is_origin_only) |
| Nostr provider discovery (Routstr kind 38421) | A raw WebSocket relay client | nostr-sdk = "0.44" + nostr_discovery.rs::build_nostr_client (Tor-proxy aware) |
Already a dependency, already has the Tor-routing pattern this codebase requires for all Nostr traffic |
| Audio/video tag extraction for the music library | A hand-rolled ID3/FLAC/MP4 parser | lofty (D-13) |
Multi-format tag parsing is a well-solved, edge-case-heavy problem (ID3v1 vs v2.2/2.3/2.4, FLAC Vorbis comments, MP4 atoms) — not worth re-implementing |
| Confirmation UI anti-spoofing | A new "trust the iframe's postMessage payload" confirm dialog | The existing Teleport-to-body / outside-iframe pattern (D-11), extending aiui:install-request's shape |
The codebase already has one correct instance of this pattern; a second bespoke one risks diverging in a security-relevant way |
Key insight: almost every primitive D-04/D-05/D-13 need already exists somewhere in this codebase in a slightly different shape (mesh assist's backend calls, swarm's payment auto-pay, the install-app confirm flow, the FileBrowser scoped-token pattern). The phase's real net-new work is a tool-calling loop and a grid-data adapter — not new payment/discovery/confirmation primitives.
Common Pitfalls
Pitfall 1: Treating the live claude-api-proxy.py (port 3142) as dormant or as "the Claude assistant"
What goes wrong: A plan that assumes "AIUI's Claude chat isn't wired to anything yet" will miss that on any node where setup-aiui-server.sh has run, /aiui/api/claude/ is a working, unauthenticated passthrough to api.anthropic.com using a node-owner-funded key, entirely bypassing the Rust RPC auth stack. This is verified in image-recipe/configs/nginx-archipelago.conf:49-60 and scripts/deploy-to-target.sh:875-940 (the embedded claude-api-proxy.py).
Why it happens: The proxy was built as a pragmatic stopgap to get AIUI's chat "working" during the demo/UI-design phase (Phase 2), predating any of the D-01..D-11 security decisions this phase makes.
How to avoid: The plan must explicitly decide what happens to this proxy: (a) delete it and the nginx location block once assistant.chat (D-01) exists, (b) gate it behind session auth as an interim step, or (c) something else — but it cannot be silently left running alongside the new authenticated path, or the phase ships a second, worse, unauthenticated door into the same capability it just spent effort locking down.
Warning signs: Any verification step that only tests the new assistant.chat RPC's auth and never checks whether /aiui/api/claude/ is still reachable unauthenticated is incomplete.
Pitfall 2: Assuming the iframe boundary is a hard sandbox
What goes wrong: AIUI-04 ("sandboxed by construction") is easy to read as "the browser enforces this." Verified from source: the AIUI iframe (Chat.vue:34-42) has no sandbox attribute, is served same-origin (/aiui/, confirmed via aiuiUrl computed and the nginx location /aiui/ block sharing the same server block as neode-ui), and the site's CSP (connect-src 'self' ws: wss: http://$host:* https:) does not restrict same-origin fetches. This means AIUI's own JavaScript, running in the user's authenticated session, is not browser-prevented from calling /rpc directly with the ambient session cookie — the entire "AIUI never gets an RPC session" property is a code-discipline convention (AIUI's code simply doesn't do this today), not an enforced boundary.
Why it happens: The embed was built for a same-origin production deploy (nginx path-based routing) specifically so cookies/theming could flow naturally — origin isolation was never a design goal until this phase's threat model made it one.
How to avoid: The plan needs to explicitly decide the sandbox's actual mechanism: a sandbox iframe attribute (careful — allow-scripts allow-same-origin together is a well-known escape pattern and must NOT both be set unless there is a compensating origin split), a stricter connect-src CSP scoped only to the /aiui/ response (e.g. disallow connect-src to /rpc from that document), or accepting the convention-based boundary explicitly as a residual risk with compensating controls (e.g. server-side rate limiting / anomaly detection on assistant.chat regardless of caller). Silence on this in the plan is itself a gap.
Warning signs: A plan that says "AIUI can't reach the RPC surface" without naming the specific enforcement mechanism.
Pitfall 3: Conflating Pine's "intent→action path" with a working action-executing loop
What goes wrong: Scoping AIUI-01 as "expose what Pine already does for voice" undersells the actual work: Pine's HA-side intents (package/pine_ha.rs) are four hardcoded, read-only Q&A intents (block height/peers/sync/balance) resolved from REST-sensor state, not a general tool-calling framework, and they never write to the node.
Why it happens: CONTEXT.md's phrasing ("the Pine stack already proves the intent→action path exists for voice") reads as if a generalized action framework exists; verification shows only Q&A exists anywhere in this codebase today (mesh assist AND Pine/HA).
How to avoid: Scope AIUI-01's tool-calling loop as genuinely new engineering (the first action-executing agent loop in this codebase), not as "extending an existing action mechanism." The reusable parts are the backend-calling code shape (call_ollama/call_claude) and the permission-gating pattern (is_sender_allowed), not an existing loop.
Warning signs: A plan task that says "wire AIUI into the existing Pine action framework" — there isn't one to wire into.
Pitfall 4: Assuming AIUI's grid components can consume ContentItem directly
What goes wrong: content_server.rs::ContentItem (id, filename, mime_type, size_bytes, description, access, availability, added_at) has no overlap in shape with AIUI's Film/Song/Podcast types (posterUrl, coverUrl, sources: FilmSource[] with type: 'plex'|'nextcloud'|..., genres, runtime, director, etc.). A plan that treats this as "just point the grid at the RPC" will produce broken/empty cards.
Why it happens: AIUI's types were designed for a rich third-party metadata catalog (TMDB-style); Archy's content model is a generic file-sharing record with access control.
How to avoid: Build an explicit adapter layer (Rust RPC response shape → AIUI prop shape, or a thin mapping function on the AIUI side) as its own task, with test fixtures pinning the mapping (e.g., what sources[].type value represents "this node's own file" vs "a peer's file" vs "IndeeHub").
Warning signs: A plan that has no explicit "map ContentItem → Film/Song" task.
Pitfall 5: The filebrowser-client.ts JWT-in-URL pattern being copied for the new tool/content RPCs
What goes wrong: filebrowser-client.ts::streamUrl() embeds a JWT in the query string (?auth=${token}), justified as "short-lived JWT so exposure in URL is acceptable" — but CONTEXT.md flags this as "the known leak to resolve rather than propagate." A new content-streaming path built by copying this pattern propagates the same leak (URLs land in browser history, server access logs, Referer headers).
Why it happens: It's the path of least resistance for <audio>/<video> src attributes, which cannot set custom headers.
How to avoid: For any new streaming URL construction in this phase (peer content via AIUI, paid content), prefer the existing fetchBlobUrl()-style header-auth pattern where the consumer can set headers, and where a <source>/<video> element is unavoidable, scope the token tightly (single-resource, single-use) rather than reusing the general FileBrowser session token.
Warning signs: Any new ...&auth=${token} or ...&token=${token} string concatenation for a long-lived credential.
Code Examples
The Ollama single-shot call being replaced (what NOT to build on top of as-is)
// Source: core/archipelago/src/mesh/listener/assist.rs:429-451 (VERIFIED, read in full)
async fn call_ollama(model: &str, prompt: &str) -> anyhow::Result<String> {
let client = reqwest::Client::builder().timeout(OLLAMA_TIMEOUT).build()?;
let body = serde_json::json!({
"model": model,
"prompt": prompt,
"stream": false,
});
let resp = client.post(OLLAMA_URL).json(&body).send().await?;
// ... no `tools` field, no multi-turn loop — /api/generate, not /api/chat
}
The existing budget-capped Cashu payment primitive (reusable for Routstr, D-05)
// Source: core/archipelago/src/swarm/payment.rs:77-101 (VERIFIED, read in full)
pub async fn auto_pay_token(
data_dir: &Path,
policy: &PaymentPolicy, // budget_sats + max_fee_sats
accepted_mints: &[String],
price_sats: u64,
) -> Result<Option<String>> {
if !policy.affords(price_sats) { return Ok(None); } // hard cap, D-05
match ecash::build_payment_token(data_dir, accepted_mints, price_sats, policy.max_fee_sats).await {
Ok(token) => Ok(Some(token)),
Err(e) => Ok(None), // never errors on a wallet/mint problem — origin always wins
}
}
The nginx block that must be reconciled with D-01 (the currently-live unauthenticated proxy)
# Source: image-recipe/configs/nginx-archipelago.conf:49-60 (VERIFIED, canonical production config)
location /aiui/api/claude/ {
proxy_pass http://127.0.0.1:3142/; # claude-api-proxy.py, own ANTHROPIC_API_KEY, no auth check
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_buffering off;
proxy_cache off;
}
Routstr chat-completions call shape (CITED: docs.routstr.com, not independently tested)
POST https://api.routstr.com/v1/chat/completions
Authorization: Bearer cashuAeyJ0... (or: X-Cashu: cashuAeyJ0...)
Content-Type: application/json
{"model":"gpt-4","messages":[{"role":"user","content":"..."}],"stream":false}
Discovery: Nostr kind 38421, tags including ["d","routstr-provider"], content carrying endpoints (http/onion), models, pricing. Default relays cited in docs: wss://relay.damus.io, wss://relay.nostr.band, wss://nos.lol — [CITED: docs.routstr.com — MEDIUM confidence, not cross-verified against a second source or a live provider event].
State of the Art
| Old Approach | Current Approach | When Changed | Impact |
|---|---|---|---|
Mesh assist: /api/generate, single prompt, no tools |
This phase: /api/chat with tools[], multi-turn loop |
This phase (net-new) | Ollama backend needs its own request builder distinct from call_ollama |
| AIUI chat → nginx proxy → Anthropic directly (client-vaulted or proxy-baked key) | AIUI chat → postMessage → neode-ui RPC (session-authed) → Rust assistant service → model | This phase (D-01) | The nginx /aiui/api/claude/, /aiui/api/openrouter/, /aiui/api/ollama/ proxy blocks become legacy/dead once migrated — must be explicitly retired or gated, not left dangling |
AIUI content grids fed by regex-parsed model prose (updatePanelFromText) against fixture catalogs baked into the system prompt |
Grids fed by real content.* RPC data via an adapter |
This phase (D-12) | System prompt shrinks (no more filmContext/songContext/podcastContext fixture dumps for Archy-sourced content — though AIUI's general recommendation feature for content NOT on this node may still want some fixture/tag mechanism, that's a design choice for the plan) |
Deprecated/outdated:
claude-api-proxy.py(port 3142) and its nginx blocks — onceassistant.chatexists, this is a strictly worse, unauthenticated duplicate of the same capability and should not coexist indefinitely.- AIUI's
vite-fs/vite-tmdb/vite-rss/vite-web-search/vite-music-search/vite-dev-chatsmiddleware as a "real data" story for production — confirmed dev-only; only the D-12-covered slice (Archy content) gets a production answer this phase, the rest stays explicitly deferred per CONTEXT.md.
Assumptions Log
| # | Claim | Section | Risk if Wrong |
|---|---|---|---|
| A1 | lofty is the right tag-extraction crate for D-13 |
Standard Stack / Don't Hand-Roll | Low — swappable later since it's an internal indexer implementation detail behind the music index's own schema; D-13's own note already flags the entity model (not the crate) as the one-way cost |
| A2 | Routstr's exact wire contract (headers, kind 38421 tag names, default relay list) as CITED from docs.routstr.com |
Code Examples / State of the Art | Medium — if the docs site's content has drifted from the actual routstr-core implementation, the Rust client's header names or the Nostr filter subscription could be wrong on first integration attempt; the plan should budget a task to test against a real Routstr provider event before hand-writing the full client, not just against the docs |
| A3 | No gsd-tools package-legitimacy check was run against lofty/symphonia/id3 (tool unavailable in this research session) — legitimacy assessed by manual crates.io inspection only |
Package Legitimacy Audit | Low-Medium — crates.io download counts and repo links were checked directly via the crates.io API, which is the same signal the automated check would use, but the automated seam's full heuristic set was not run |
| A4 | The claude-api-proxy.py/port-3142 path is reachable without authentication on every node that has run setup-aiui-server.sh, not just the specific nodes checked in this session |
Common Pitfalls / Summary | High if wrong in the safe direction (i.e. if some nodes actually do have it gated some other way this research didn't find) — but the canonical image-recipe/configs/nginx-archipelago.conf (the ISO-shipped, non-manual-script config) has no gate either, so this is the default state for any node built from the current image recipe, which is HIGH confidence, not just this-node-specific |
If this table is empty: N/A — see entries above; none are structural blockers, but A2 and A4 both warrant explicit plan tasks (a live-Routstr-provider smoke test; an audit of which fleet nodes currently expose the unauthenticated Claude proxy).
Open Questions
-
What happens to the port-3142
claude-api-proxy.pyand its nginx blocks?- What we know: it is live, unauthenticated, and holds its own API key separate from
secrets/claude-api-key. - What's unclear: whether any currently-deployed node's users rely on it continuing to work exactly as-is during the migration window, and whether deleting it is this phase's job or a follow-up.
- Recommendation: the plan should make an explicit decision (delete-and-replace vs. gate-then-deprecate) with a checkpoint, not leave it implicit.
- What we know: it is live, unauthenticated, and holds its own API key separate from
-
What is the actual enforcement mechanism for AIUI-04's "sandboxed by construction"?
- What we know: today there is no
sandboxiframe attribute, no origin split, and a permissive same-origin CSP. - What's unclear: whether the plan should add a
sandboxattribute (and handle themicrophonepermission +allow-same-origininteraction correctly), tighten CSP for the/aiui/response specifically, or explicitly accept the convention-based boundary with compensating server-side controls. - Recommendation: name this as its own task with a concrete decision, since D-11's whole premise ("the iframe cannot spoof... the confirmation dialog") assumes the postMessage channel is the only channel — which is true only by convention today.
- What we know: today there is no
-
Is Routstr's documented protocol (kind 38421, header names) accurate against the live
routstr-core/routstrdimplementation?- What we know:
docs.routstr.comdescribes the shape (CITED, medium confidence). - What's unclear: whether a live provider on the default relays actually publishes exactly this event shape today, given Routstr is a young, actively-developed project.
- Recommendation: budget an early spike task that subscribes to the real relays and inspects at least one live kind-38421 event before writing the parser against the docs alone.
- What we know:
-
Does the RBAC
role.can_access(&method)check apply to whatever newassistant.*RPC methods this phase adds, and should it?- What we know: every existing authenticated RPC method goes through
user.role.can_access(&rpc_req.method)(api/rpc/mod.rs:296-307). - What's unclear: whether the AI permission-category model (10 categories, D-16) should be layered on top of, integrated with, or kept fully separate from the existing role/RBAC system.
- Recommendation: the planner should decide explicitly rather than let this fall out implicitly from wherever the new RPC methods happen to get registered.
- What we know: every existing authenticated RPC method goes through
Environment Availability
| Dependency | Required By | Available | Version | Fallback |
|---|---|---|---|---|
cargo / Rust toolchain |
All Rust-side work | ✓ | 1.95.0 (VERIFIED, cargo --version) |
— |
~/Projects/AIUI clone, development branch |
All AIUI-side work | ✓ | HEAD 6e8b96d, clean working tree (VERIFIED, git status/git log) |
— |
Push access to git.tx1138.com/lfg2025/AIUI |
Landing AIUI-side commits (D-18) | Per phase brief: CONFIRMED by orchestrator | — | — |
| Ollama (local LLM) | D-04 primary backend | Not probed on a live node in this research session (no node reachable from this environment) | — | Detection code (detect_ollama(), mesh/rpc/mesh/assistant.rs:164-192) already exists and reports ollama_detected/models — reuse rather than re-probe |
| A live Routstr provider (for protocol verification) | Open Question 3 | ✗ (not reachable from this research environment) | — | Docs-only (CITED) until a spike task runs against real relays |
Node's own data_dir/secrets/claude-api-key |
Existing mesh assist Claude backend | Not probed (no live node in this environment) | — | Mesh assist code already handles its absence gracefully (call_claude returns an error, caught by run_assist) |
Missing dependencies with no fallback: none — every dependency either has an existing detection/fallback path in-tree or is deferred to a named spike task (Open Question 3).
Validation Architecture
Test Framework
| Property | Value |
|---|---|
| Framework (Rust) | cargo test (in-tree unit/integration tests, e.g. swarm/payment.rs's #[tokio::test] suite, pine_ha.rs's #[test] suite) |
| Framework (neode-ui) | Vitest 3.1 (neode-ui/package.json — "test": "vitest run"), existing contextBroker.test.ts/chatAiuiEmbed.test.ts to keep green |
| Framework (AIUI) | Not yet inspected in this research pass — packages/app/src/__tests__/ and composables/__tests__/ exist (contentExtraction.test.ts, useAI.test.ts) — planner should confirm AIUI's own package.json test command before relying on it |
| Config file | core/archipelago/Cargo.toml (Rust); neode-ui/vitest.config.ts (frontend) |
| Quick run command | cd core && cargo test --package archipelago assistant:: (once the module exists); cd neode-ui && npx vitest run src/services/__tests__/contextBroker.test.ts |
| Full suite command | cd core && cargo test (release-profile per CARGO_INCREMENTAL=0 if lld errors appear, per CLAUDE.md); cd neode-ui && npm run test |
Phase Requirements → Test Map
| Req ID | Behavior | Test Type | Automated Command | File Exists? |
|---|---|---|---|---|
| AIUI-01 | A typed chat request executes a real read-only tool (e.g. "how much space is left" → system.disk-status) and returns the real result |
integration (Rust) | cargo test assistant::tests::disk_status_tool_executes |
❌ Wave 0 — module doesn't exist yet |
| AIUI-01 | A typed chat request for a write action (e.g. "restart bitcoin") produces a pending confirmation, NOT an executed action, until confirmed | integration (Rust) + component (Vue) | cargo test assistant::tests::destructive_tool_requires_confirm; npx vitest run src/services/__tests__/toolConfirm.test.ts |
❌ Wave 0 |
| AIUI-01 | An unauthenticated caller cannot reach any new assistant.* RPC method |
integration (Rust) | cargo test rpc::middleware::tests::assistant_methods_require_session |
❌ Wave 0 |
| AIUI-02 | A conversational settings change (system.settings.set via tool call) is scoped to a granted permission category and refused when not granted |
unit (Rust) | cargo test assistant::tools::tests::settings_tool_respects_category_grant |
❌ Wave 0 |
| AIUI-03 | content.* RPC data renders correctly in FilmGrid/SongGrid via the new adapter (regression-pins the mapping named in Pitfall 4) |
unit (Vue/TS) | npx vitest run src/composables/__tests__/archyContentAdapter.test.ts |
❌ Wave 0 |
| AIUI-04 | AIUI's own code cannot reach /rpc or any authenticated endpoint directly (whatever mechanism Open Question 2 resolves to) |
integration/manual (per chosen mechanism) | Depends on Open Question 2's resolution | ❌ Wave 0 — mechanism undecided |
| AIUI-05 | AIUI's build enforces VITE_BASE_PATH=/aiui/ and a post-deploy check fetches a live asset by hash |
shell/CI | scripts/build-aiui.sh (new) exits non-zero if VITE_BASE_PATH unset; post-deploy curl check on a known asset path |
❌ Wave 0 — no such script exists today |
| AIUI-06 | Manual UAT: embedded iframe on archi-dev-box, desktop + mobile viewport | manual | N/A — real-device verification, not automatable | — |
Sampling Rate
- Per task commit: the relevant quick-run command for the touched module (Rust
assistant::tests, or the specific Vitest file). - Per wave merge: full
cargo test+ fullnpm run test(neode-ui) + AIUI's own test command (to be confirmed). - Phase gate: full suite green, plus the AIUI-06 manual on-device pass on archi-dev-box (desktop and mobile), before
/gsd-verify-work.
Wave 0 Gaps
core/archipelago/src/assistant/mod.rs+ its#[cfg(test)]module — the entire tool-calling loop is net-new, zero existing test coverage.neode-ui/src/services/__tests__/toolConfirm.test.ts— new confirm-flow coverage (extends the existingcontextBroker.test.tspattern).neode-ui/src/composables/__tests__/archyContentAdapter.test.ts— pins theContentItem→Film/Song/Podcastmapping (Pitfall 4).scripts/build-aiui.sh(or equivalent) — does not exist; D-15'sVITE_BASE_PATHenforcement and commit-pinning have no automated check today.- AIUI's own test command/framework — confirm before wave planning assumes Vitest parity (not verified in this research pass; AIUI's
package.jsonwas read for build scripts only).
Security Domain
Applicable ASVS Categories
| ASVS Category | Applies | Standard Control |
|---|---|---|
| V2 Authentication | yes | New assistant.* RPCs go through the existing session-cookie + CSRF stack (api/rpc/mod.rs:264-330) — no bespoke auth |
| V3 Session Management | yes | Chat history/pending-confirmation state must be scoped to the authenticated session/node, not a separate identity |
| V4 Access Control | yes | D-06 curated tool allowlist + D-16 default-closed permission categories + (Open Question 4) RBAC integration decision |
| V5 Input Validation | yes | D-10: peer-supplied content must enter the model context inside explicit untrusted-content delimiters; tool-call arguments from the model must be schema-validated against each tool's declared parameters before execution (not just trusted because the model emitted well-formed JSON) |
| V6 Cryptography | yes | Routstr Cashu payments reuse crate::wallet::ecash/bdhke.rs — never hand-roll token construction; model API keys stay server-side (secrets/claude-api-key pattern) |
| V13 API and Web Service | yes | The port-3142 claude-api-proxy.py is a standing V13 violation (unauthenticated proxy to a paid third-party API) that this phase's D-01 should resolve, per Common Pitfall 1 |
Known Threat Patterns for this stack
| Pattern | STRIDE | Standard Mitigation |
|---|---|---|
| Prompt injection via peer-supplied content (filenames, mesh chat, Nostr posts) driving unintended tool calls | Elevation of Privilege | D-10: untrusted-content delimiters + D-11: human confirmation naming the REAL action for every write, regardless of what the model claims it's doing |
| Unauthenticated proxy to a paid API (the live port-3142 finding) | Spoofing / Elevation of Privilege / Denial of Service (budget exhaustion) | Retire or session-gate the legacy proxy (Common Pitfall 1) |
| Iframe escaping its intended postMessage-only channel via ambient same-origin session cookie | Elevation of Privilege | Resolve Open Question 2 (sandbox attribute / CSP scoping / accepted residual risk with compensating controls) |
| Routstr budget exhaustion via repeated/looped tool calls | Denial of Service (financial) | D-05's hard budget cap in PaymentPolicy — already proven to degrade to None rather than error, must be wired so the loop actually stops and surfaces to the user rather than silently retrying |
| Confirmation-dialog spoofing (model-authored text presented as a system confirmation) | Spoofing / Tampering | D-11: node-authored text only, rendered outside the iframe |
Sources
Primary (HIGH confidence — read directly from source in this session)
core/archipelago/src/mesh/listener/assist.rs(full file) —run_assist,is_sender_allowed,call_ollama,call_claudecore/archipelago/src/api/rpc/mesh/assistant.rs,core/archipelago/src/api/rpc/dispatcher.rs(grepped in full forpine./mesh.assistant/method registry)core/archipelago/src/api/rpc/pine_status.rs,core/archipelago/src/api/rpc/package/pine_ha.rs(full files)core/archipelago/src/streaming/mod.rs,streaming/gate.rs,api/rpc/streaming.rs,swarm/payment.rs(full files)core/archipelago/src/nostr_discovery.rs(partial),core/archipelago/Cargo.toml(grepped)core/archipelago/src/api/rpc/middleware.rs,core/archipelago/src/api/rpc/mod.rs(auth/CSRF/RBAC flow)core/archipelago/src/content_server.rs(ContentItem/AccessControlstructs)neode-ui/src/types/aiui-protocol.ts,neode-ui/src/services/contextBroker.ts(full files)neode-ui/src/api/filebrowser-client.ts(grepped),neode-ui/src/views/Chat.vue(partial)/home/archipelago/Projects/AIUI(cloned repo,developmentbranch, HEAD6e8b96d) —packages/app/src/composables/useAI.ts,useArchy.ts(full files),contentExtraction.ts(partial),vite-fs.ts/vite-tmdb.ts/vite-rss.ts/vite-web-search.ts/vite-music-search.ts/vite-dev-chats.ts(grepped forconfigureServer),components/content/FilmGrid.vue/SongGrid.vue(partial),packages/core/src/types/content.ts(partial)image-recipe/configs/nginx-archipelago.conf,scripts/setup-aiui-server.sh,scripts/deploy-to-target.sh(all grepped/read for the AIUI/Claude-proxy deploy path)apps/aiui/manifest.yml(full file)- crates.io API (
crates.io/api/v1/crates/lofty,/symphonia,/id3) — VERIFIED: crates.io registry, queried live in this session
Secondary (MEDIUM confidence)
docs.routstr.com(/,/api/endpoints/,/client/integration/,/provider/discovery/) — fetched via WebFetch in this session; official documentation but not cross-verified against a live Routstr node or a second independent source — [CITED: docs.routstr.com]github.com/routstrorg listing — fetched via WebFetch; confirms no existing Rust SDK, so a hand-writtenreqwest-based client is the correct approach — [CITED: github.com/routstr]
Tertiary (LOW confidence)
- None used as load-bearing claims; all
[ASSUMED]items are logged in the Assumptions table above.
Metadata
Confidence breakdown:
- Standard stack (Rust-side reuse: reqwest/nostr-sdk/swarm payment): HIGH — all read directly from in-tree source
- Standard stack (lofty for music tagging): MEDIUM — crate choice is a training-knowledge recommendation cross-checked against live crates.io data, but not run through the automated package-legitimacy seam
- Architecture (tool-calling loop, confirm-gate mechanism, content adapter need): HIGH — every claim traced to specific file:line evidence in both repos
- Architecture (Routstr wire protocol): MEDIUM — CITED from official docs only, not independently protocol-tested
- Pitfalls (live unauthenticated Claude proxy, same-origin sandbox gap, Pine-is-Q&A-only): HIGH — all independently re-derived from source, not merely repeating CONTEXT.md's claims (and in the live-proxy and same-origin cases, going beyond what CONTEXT.md flagged at all)
Research date: 2026-08-03
Valid until: ~14 days for the Rust/neode-ui findings (stable, slow-moving codebase areas); ~7 days for the Routstr protocol claims (young, actively-developed external project — re-verify against a live relay before implementation) and for the AIUI repo state (actively developed, development branch may move).