docs(13): research phase domain — AIUI conversational control
Verifies the AIUI-01 gating question against source (no tool-calling anywhere in this codebase today; Pine's HA intents are read-only Q&A, not an action-executing loop), surfaces a live unauthenticated Claude-proxy exposure (port 3142) and a same-origin iframe sandbox gap not previously named, and maps existing Cashu/Nostr primitives onto the Routstr integration. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
3b8ac7cb1c
commit
7134ae903d
+518
@@ -0,0 +1,518 @@
|
||||
# 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 `archipelago` binary 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 `ContextBroker` keeps only what must run in the browser (`navigate`, `open-app`, `launch-app`, `theme`) and remains the consent surface pushing `permissions: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_METHODS` hard-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/AIUI` branch `development`.
|
||||
|
||||
### Claude's Discretion
|
||||
- What the music library indexes over (own filebrowser `Music` folder, 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.vue` and 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.vue` architecture.
|
||||
</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:**
|
||||
```bash
|
||||
# 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-verify` before `cargo 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
|
||||
|
||||
```text
|
||||
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`):**
|
||||
```typescript
|
||||
// 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-side `ContextBroker`/`aiui-protocol.ts` sandbox 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-key` as the ONLY key ledger while the `claude-api-proxy.py`/port-3142 path with its separate `ANTHROPIC_API_KEY` env 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)
|
||||
```rust
|
||||
// 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)
|
||||
```rust
|
||||
// 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)
|
||||
```nginx
|
||||
# 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 — once `assistant.chat` exists, 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-chats` middleware 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
|
||||
|
||||
1. **What happens to the port-3142 `claude-api-proxy.py` and 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.
|
||||
|
||||
2. **What is the actual enforcement mechanism for AIUI-04's "sandboxed by construction"?**
|
||||
- What we know: today there is no `sandbox` iframe attribute, no origin split, and a permissive same-origin CSP.
|
||||
- What's unclear: whether the plan should add a `sandbox` attribute (and handle the `microphone` permission + `allow-same-origin` interaction 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.
|
||||
|
||||
3. **Is Routstr's documented protocol (kind 38421, header names) accurate against the live `routstr-core`/`routstrd` implementation?**
|
||||
- What we know: `docs.routstr.com` describes 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.
|
||||
|
||||
4. **Does the RBAC `role.can_access(&method)` check apply to whatever new `assistant.*` 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.
|
||||
|
||||
## 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` + full `npm 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 existing `contextBroker.test.ts` pattern).
|
||||
- [ ] `neode-ui/src/composables/__tests__/archyContentAdapter.test.ts` — pins the `ContentItem` → `Film`/`Song`/`Podcast` mapping (Pitfall 4).
|
||||
- [ ] `scripts/build-aiui.sh` (or equivalent) — does not exist; D-15's `VITE_BASE_PATH` enforcement 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.json` was 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_claude`
|
||||
- `core/archipelago/src/api/rpc/mesh/assistant.rs`, `core/archipelago/src/api/rpc/dispatcher.rs` (grepped in full for `pine.`/`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`/`AccessControl` structs)
|
||||
- `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, `development` branch, HEAD `6e8b96d`) — `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 for `configureServer`), `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/routstr` org listing — fetched via WebFetch; confirms no existing Rust SDK, so a hand-written `reqwest`-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).
|
||||
Reference in New Issue
Block a user