# Phase 13: AIUI — Conversational Node Control & Content Surfaces - Pattern Map **Mapped:** 2026-08-03 **Files analyzed:** 24 (net-new + modified, both repos) **Analogs found:** 17 / 24 (7 have no strong precedent — flagged below) **Scope note:** this phase spans two repos: `/home/archipelago/Projects/archy` (Rust `core/`, Vue `neode-ui/`) and `/home/archipelago/Projects/AIUI` (Vue, branch `development`). File paths below are absolute-repo-relative and prefixed accordingly. --- ## File Classification | New/Modified File | Role | Data Flow | Closest Analog | Match Quality | |---|---|---|---|---| | `core/archipelago/src/assistant/mod.rs` | service | request-response (loop) | `core/archipelago/src/mesh/listener/assist.rs` | role-match (Q&A→tool-loop, no precedent for the loop itself) | | `core/archipelago/src/assistant/tools.rs` | model/schema | transform | `core/archipelago/src/api/rpc/mesh/assistant.rs` (config shape) | weak — **no existing curated-tool-registry precedent in this codebase** | | `core/archipelago/src/assistant/backends/ollama.rs` | service | request-response | `core/archipelago/src/mesh/listener/assist.rs::call_ollama` | exact (endpoint/shape differs, HTTP client pattern identical) | | `core/archipelago/src/assistant/backends/claude.rs` | service | request-response | `core/archipelago/src/mesh/listener/assist.rs::call_claude` | exact (endpoint/shape differs, HTTP client pattern identical) | | `core/archipelago/src/assistant/backends/routstr.rs` | service | request-response + payment | `core/archipelago/src/swarm/payment.rs::auto_pay_token` (payment half) + `call_claude` (HTTP half) | partial — **no existing OpenAI-compatible client in this codebase; net-new** | | `core/archipelago/src/assistant/loop_.rs` | service | event-driven (multi-turn) | none in this codebase | **no analog — first tool-calling loop; see AI-SPEC §3 for the sketch instead** | | `core/archipelago/src/assistant/confirm.rs` | service | pub-sub (pending-queue) | `neode-ui/src/services/contextBroker.ts` install-app confirm flow (cross-repo, browser-side half only) | partial — Rust-side pending-queue has no precedent | | `core/archipelago/src/assistant/history.rs` | model/storage | CRUD | `core/archipelago/src/streaming/session.rs` (data_dir-scoped persisted state) | role-match | | `core/archipelago/src/api/rpc/assistant_chat.rs` | route (RPC handler) | request-response | `core/archipelago/src/api/rpc/mesh/assistant.rs` | exact | | `core/archipelago/src/api/rpc/dispatcher.rs` (modified) | route (registry) | request-response | itself — extend `"mesh.assistant-*"` block at ~445 | exact | | `core/archipelago/src/music/mod.rs` | service | batch/CRUD | `core/archipelago/src/content_server.rs` (catalog load/scan shape) | role-match | | `core/archipelago/src/music/index.rs` | model/storage | CRUD | `core/archipelago/src/content_server.rs::load_catalog` | role-match | | `core/archipelago/src/music/tags.rs` | utility | transform | none — new `lofty`-based extractor | **no analog — net-new dependency, gate behind checkpoint:human-verify per RESEARCH.md** | | `neode-ui/src/services/contextBroker.ts` (modified) | service (browser bridge) | pub-sub (postMessage) | itself — extend existing `handleMessage` switch and the install-app confirm block (lines 140-196) | exact | | `neode-ui/src/types/aiui-protocol.ts` (modified) | model (protocol types) | transform | itself — extend `AIActionType` union | exact | | `neode-ui/src/components/ToolConfirmModal.vue` (new) | component | event-driven | `neode-ui/src/components/NostrSignConsent.vue` | exact (Teleport-to-body approve/deny modal) | | `neode-ui/src/composables/archyContentAdapter.ts` (new) | utility (adapter) | transform | none in neode-ui — **net-new**, shape target is AIUI's `Film`/`Song`/`Podcast` types | no analog — see AIUI content types below | | `neode-ui/src/api/assistant-client.ts` (new, optional) | service (RPC client wrapper) | request-response | `neode-ui/src/api/filebrowser-client.ts` (scoped-token pattern) | role-match — **do not copy the `streamUrl` JWT-in-query leak (line 176)** | | `scripts/build-aiui.sh` (new) | config/build script | batch | `scripts/deploy-to-target.sh` (AIUI rsync section, `setup-aiui-server.sh`) | role-match | | `AIUI: packages/app/src/composables/useAI.ts` (modified) | service (chat client) | streaming | itself — replace `streamClaude`/`streamOpenRouter` direct-to-proxy calls | exact (modify in place) | | `AIUI: packages/app/src/composables/useArchy.ts` (modified) | service (bridge client) | request-response | itself — extend `buildArchyContext()`/postMessage senders | exact | | `AIUI: packages/app/src/composables/contentExtraction.ts` (modified/deprecated for Archy content) | utility (transform) | transform | itself — `updatePanelFromText` regex path stays for non-Archy content, bypassed for Archy-sourced grids | exact (partial deprecation) | | `AIUI: packages/app/src/components/content/FilmGrid.vue` / `SongGrid.vue` (consumers, unmodified props) | component | CRUD (prop-fed) | itself — no code change, just a new data source feeding existing props | exact — **props unchanged, D-12** | | `AIUI: packages/core/src/types/content.ts` (read, not modified) | model (types) | transform | itself — the target shape `archyContentAdapter.ts` must produce | exact reference | --- ## Pattern Assignments ### `core/archipelago/src/assistant/backends/ollama.rs` (service, request-response) **Analog:** `core/archipelago/src/mesh/listener/assist.rs` (lines 429-451, `call_ollama`) **What to copy — HTTP client construction:** ```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 { 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 } ``` **What must change (do NOT copy as-is):** - Endpoint: `/api/generate` → `/api/chat` (tool-calling requires the chat endpoint). - Request body needs a `messages` array (not bare `prompt`) and a `tools` array (`[{"type":"function","function":{"name","description","parameters"}}]`). - Response parsing needs `message.tool_calls` extraction; Ollama gives tool calls no `id` — synthesize one (monotonic counter within the turn), per AI-SPEC §3 Pitfall 3. - Do not reuse `OLLAMA_TIMEOUT` (60s, airtime-tuned for mesh) — define new constants in `assistant/` per AI-SPEC §3 Pitfall 6. **Error handling:** `assist.rs::call_claude`'s `anyhow::Result` propagation and `run_assist`'s catch-and-fall-back-to-next-backend pattern is the model for the D-04 backend chain (Ollama → Claude → Routstr fallback on error). --- ### `core/archipelago/src/assistant/backends/claude.rs` (service, request-response) **Analog:** `core/archipelago/src/mesh/listener/assist.rs` (`call_claude`) + `core/archipelago/src/api/rpc/mesh/assistant.rs` (key-file read pattern) **Key location pattern to copy** (lines 27-30 of `assistant.rs`): ```rust // Source: core/archipelago/src/api/rpc/mesh/assistant.rs:27-30 (VERIFIED) let claude_available = tokio::fs::metadata(self.config.data_dir.join("secrets/claude-api-key")) .await .is_ok(); ``` Reuse `data_dir/secrets/claude-api-key` as the key path — do not introduce a second key location. `call_claude`'s single-user-message Messages API POST is the HTTP-shape starting point; extend it with `tools: [...]`, `tool_choice: {"type":"auto","disable_parallel_tool_use":true}` (AI-SPEC §3 Pitfall 5), and `max_tokens: 2048` (raised from mesh's `512`). --- ### `core/archipelago/src/assistant/backends/routstr.rs` (service, request-response + payment) **No direct analog for the HTTP client** (first OpenAI-compatible client in this codebase). Compose from two existing pieces: **Payment half — copy verbatim as the reusable primitive** (`core/archipelago/src/swarm/payment.rs:77-101`): ```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> { 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 } } ``` Call this exactly as-is for D-05's budget cap; `loop_.rs` must treat `None` as "stop and ask," never retry. **Nostr discovery half:** `core/archipelago/src/nostr_discovery.rs::build_nostr_client` (Tor-proxy aware) — reuse this builder rather than constructing a second `nostr-sdk` client; subscribe to kind `38421` events for provider discovery. **HTTP half:** model the `reqwest::Client` construction on `call_claude`'s pattern (same crate, same TLS/socks features already in `Cargo.toml`), but the request/response shape is net-new (OpenAI `tools`/`tool_calls` JSON-string-encoded arguments — see AI-SPEC §3 Pitfall 2, do not confuse with Ollama's already-parsed object). --- ### `core/archipelago/src/assistant/loop_.rs` (service, event-driven multi-turn loop) **No analog exists in this codebase** — this is confirmed (RESEARCH.md, AI-SPEC.md) to be the first tool-calling loop ever written here. Do not attempt to derive it from `run_assist` (single-shot) or from Pine's HA intents (hardcoded read-only, no loop). Build directly from the `run_loop`/`execute_tool` sketch in `13-AI-SPEC.md` §3/§4 — that IS the pattern source for this file; there is no in-repo precedent to extract instead. **Concurrency discipline to copy from `assist.rs`'s own doc comment:** ``` // "Spawned off the radio loop so it never blocks" — VERIFIED, assist.rs's own doc comment. ``` Apply the same discipline: never hold a shared lock (e.g. `state.assistant.write().await`) across the confirm-gate `.await`, which can block for human-response-time. --- ### `core/archipelago/src/assistant/confirm.rs` (service, D-11 pending-confirmation queue) **Analog (browser-side half only, cross-repo):** `neode-ui/src/services/contextBroker.ts:140-196` — the install-app confirm flow (`CustomEvent('aiui:install-request')` / `aiui:install-response`, 60s timeout). This is the closest existing anti-spoofing confirm pattern in the whole codebase and is explicitly named in CONTEXT.md as the model to extend: ```typescript // Source: neode-ui/src/services/contextBroker.ts:140-196 (VERIFIED, read in full) 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) ``` **Do NOT reuse `aiui:install-request`/`aiui:install-response` directly** — CONTEXT.md and RESEARCH.md both specify a new, distinct event pair (`aiui:tool-confirm-request` / `aiui:tool-confirm-response`), because D-11 requires the pending-action text be RPC-fetched (node-authored), never postMessage-carried (which the iframe could forge). The Rust-side `confirm.rs` queue itself (keyed by `req_id`/`call_id`, in-memory only, never persisted across a daemon restart per AI-SPEC §4 "State Management") has no existing analog — build per the AI-SPEC sketch. --- ### `core/archipelago/src/api/rpc/assistant_chat.rs` (route, request-response) **Analog:** `core/archipelago/src/api/rpc/mesh/assistant.rs` (full file — `handle_mesh_assistant_status`, `handle_mesh_assistant_configure`) **RPC handler shape to copy:** ```rust // Source: core/archipelago/src/api/rpc/mesh/assistant.rs:13-16 (VERIFIED) impl RpcHandler { pub(in crate::api::rpc) async fn handle_mesh_assistant_status( &self, ) -> Result { ``` Follow the same `impl RpcHandler` + `pub(in crate::api::rpc) async fn handle_*` + `Result` convention for `handle_assistant_chat`, `handle_assistant_confirm_tool`, `handle_assistant_list_tools`, `handle_assistant_history`. **Registration pattern** (`core/archipelago/src/api/rpc/dispatcher.rs:445-446`): ```rust "mesh.assistant-status" => self.handle_mesh_assistant_status().await, "mesh.assistant-configure" => self.handle_mesh_assistant_configure(params).await, ``` Add new `"assistant.chat"`, `"assistant.confirm-tool"`, `"assistant.list-tools"`, `"assistant.history"` entries adjacent to this block. **Verify session/CSRF/RBAC gating applies automatically** — every method in this dispatch table already passes through `api/rpc/mod.rs:264-330`'s session-cookie + CSRF + `role.can_access()` check before reaching the `match`; no bespoke auth needed (per Open Question 4 in RESEARCH.md, confirm this applies rather than assume). --- ### `core/archipelago/src/music/index.rs` (model/storage, CRUD) **Analog:** `core/archipelago/src/content_server.rs::load_catalog` (catalog-scan-and-persist shape) — read this function's on-disk index load/save pattern under `data_dir` and follow the same convention for the music index (own subdirectory under `data_dir`, per D-13's discretion on exact location). --- ### `neode-ui/src/components/ToolConfirmModal.vue` (component, D-11 trusted-chrome modal) **Analog:** `neode-ui/src/components/NostrSignConsent.vue` (full file, 70 lines) — the project's canonical Teleport-to-body approve/deny modal, structurally identical to what D-11 needs. **Structure to copy:** ```vue ``` **Content difference from the analog:** D-11 requires the confirmation text be **RPC-fetched from the node's own pending-action description** (via `assistant.list-tools`/pending-confirmation poll or the `chat:response` channel that carries a `tool:confirm-request` payload), never model-authored text and never a postMessage-carried string from AIUI. Wire this modal's props from `contextBroker.ts`'s new confirm-handling code, not from anything AIUI sends. --- ### `neode-ui/src/services/contextBroker.ts` (modified — new `chat:*`/`tool:confirm-*` message types) **Analog:** itself — the existing `handleMessage` switch (imports at lines 1-13) and the install-app confirm block (lines 140-196, shown above under `confirm.rs`). **Imports pattern already in file** (lines 1-13): ```typescript // Source: neode-ui/src/services/contextBroker.ts:1-13 (VERIFIED) import type { Ref } from 'vue' import type { AIUIRequest, ArchyResponse, AIContextCategory, ArchyContextResponse, ArchyActionResponse, } from '@/types/aiui-protocol' import { useAIPermissionsStore } from '@/stores/aiPermissions' import { useAppStore } from '@/stores/app' import { useContainerStore, BUNDLED_APPS } from '@/stores/container' import { rpcClient } from '@/api/rpc-client' import { fileBrowserClient } from '@/api/filebrowser-client' ``` New `assistant-client.ts` (or direct `rpcClient.call('assistant.chat', ...)`) follows this same import convention. The class-level origin check (`this.allowedOrigin`, constructor lines 26-33) and the `postToIframe` helper are the transport primitives every new message type must use — do not add a second postMessage channel. --- ### `neode-ui/src/api/assistant-client.ts` (new, role-match to filebrowser-client.ts) **Analog:** `neode-ui/src/api/filebrowser-client.ts` — the scoped-token pattern D-01 follows for minting short-lived, purpose-scoped credentials via an authenticated RPC. **Known leak to fix, NOT propagate** (`neode-ui/src/api/filebrowser-client.ts:172-176`): ```typescript // Source: neode-ui/src/api/filebrowser-client.ts:172-176 (VERIFIED) async streamUrl(path: string): Promise { // ... return `${this.baseUrl}/api/raw${safePath}?auth=${token}` // ^^^^^^^^^^^^^ JWT in URL query string — // lands in browser history, server access // logs, Referer headers. } ``` For any new content/tool streaming URL construction in this phase, do NOT copy this `?auth=${token}` concatenation. Prefer header-based auth where the consumer can set headers (`fetchBlobUrl()`-style, per RESEARCH.md Pitfall 5); where a bare `