diff --git a/.planning/phases/13-aiui-functional-conversational-node-control-and-content-surf/13-PATTERNS.md b/.planning/phases/13-aiui-functional-conversational-node-control-and-content-surf/13-PATTERNS.md new file mode 100644 index 00000000..f9dad82f --- /dev/null +++ b/.planning/phases/13-aiui-functional-conversational-node-control-and-content-surf/13-PATTERNS.md @@ -0,0 +1,430 @@ +# 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 `