25 KiB
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:
// 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
}
What must change (do NOT copy as-is):
- Endpoint:
/api/generate→/api/chat(tool-calling requires the chat endpoint). - Request body needs a
messagesarray (not bareprompt) and atoolsarray ([{"type":"function","function":{"name","description","parameters"}}]). - Response parsing needs
message.tool_callsextraction; Ollama gives tool calls noid— 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 inassistant/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):
// 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):
// 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
}
}
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:
// 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:
// 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<serde_json::Value> {
Follow the same impl RpcHandler + pub(in crate::api::rpc) async fn handle_* + Result<serde_json::Value>
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):
"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:
<!-- Source: neode-ui/src/components/NostrSignConsent.vue:1-20 (VERIFIED) -->
<template>
<Teleport to="body">
<Transition name="modal">
<div
v-if="show"
class="fixed inset-0 z-[3000] flex items-center justify-center p-4"
@click="deny"
>
<div class="absolute inset-0 bg-black/60 backdrop-blur-sm"></div>
<div ref="modalRef" @click.stop class="glass-card p-6 max-w-md w-full relative z-10">
<div class="flex items-start justify-between gap-4 mb-4">
<h3 class="text-xl font-semibold text-white">Nostr Signing Request</h3>
<button @click="deny" class="p-2 rounded-lg hover:bg-white/10 ..." aria-label="Close" />
</div>
<!-- request-specific detail rendering here -->
<div class="flex gap-3">
<button @click="deny" class="glass-button flex-1 py-2.5 rounded-lg text-sm font-medium">Deny</button>
<button @click="approve" class="glass-button flex-1 py-2.5 rounded-lg text-sm font-medium text-orange-400 border-orange-400/30">Approve</button>
</div>
</div>
</div>
</Transition>
</Teleport>
</template>
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):
// 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):
// Source: neode-ui/src/api/filebrowser-client.ts:172-176 (VERIFIED)
async streamUrl(path: string): Promise<string> {
// ...
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 <audio>/<video src> is
unavoidable, scope the token single-resource/single-use rather than reusing the general
FileBrowser session token shape.
neode-ui/src/composables/archyContentAdapter.ts (new, no analog — net-new adapter)
No existing adapter in neode-ui. Target shape is AIUI's own types
(/home/archipelago/Projects/AIUI/packages/core/src/types/content.ts, read directly):
// Source: AIUI packages/core/src/types/content.ts:7-53 (VERIFIED, read in full)
export interface Film {
// id, title, ... (lines 7-19)
posterUrl: string
// ...
sources: FilmSource[] // line 20
}
export interface FilmSource { /* type: 'plex'|'nextcloud'|... , url, ... */ }
export interface Song {
// ...
coverUrl?: string // line 50
sources?: SongSource[] // line 53
}
Source shape to map FROM (core/archipelago/src/content_server.rs, ContentItem):
// id, filename, mime_type, size_bytes, description, access, availability, added_at
This is Pitfall 4 in RESEARCH.md — there is genuinely no shape overlap; the adapter is
hand-written mapping logic, not a pass-through. Pin the mapping with fixture-based tests
(archyContentAdapter.test.ts, listed as a Wave 0 gap in RESEARCH.md's Validation Architecture).
FilmGrid.vue/SongGrid.vue themselves need zero code changes — D-12 is explicit that only
the data source behind the existing props changes.
AIUI: packages/app/src/composables/useAI.ts (modified — replace direct-proxy calls)
Analog: itself. The current streamClaude/streamOpenRouter functions call
${BASE}api/claude/v1/messages / ${BASE}api/openrouter directly (the port-3142
claude-api-proxy.py passthrough, verified live and unauthenticated in RESEARCH.md). Replace
these call sites with the new chat:request/chat:response postMessage exchange to
contextBroker.ts, matching the shape useArchy.ts already uses for its existing
readFile/tailLogs postMessage calls (grep useArchy.ts for its existing postMessage-send
pattern and mirror it — do not invent a third transport convention on the AIUI side).
scripts/build-aiui.sh (new, config/build script)
Analog: scripts/deploy-to-target.sh (AIUI rsync section) and scripts/setup-aiui-server.sh
— both already encode the VITE_BASE_PATH=/aiui/ requirement and the rsync-to-node path.
D-15 requires this be made deliberate (enforced by the script, not remembered) with a
post-deploy check that fetches a live asset — model the fetch-and-verify step on the project's
general "grep the built bundle for new strings before shipping" convention from CLAUDE.md
("Frontend build — verify dist changed" feedback note), translated into an automated curl
check rather than a manual grep.
Shared Patterns
Session/CSRF/RBAC gating (applies to every new assistant.* and content.* RPC)
Source: core/archipelago/src/api/rpc/mod.rs:264-330
Apply to: assistant_chat.rs, all new dispatcher entries.
Every RPC method reaching the match in dispatcher.rs already passed session-cookie + CSRF +
role.can_access(&method) checks upstream — no bespoke auth code needed in the new handlers
themselves, only correct registration in the existing table.
Anti-spoofing confirm gate (D-11, applies to every destructive tool)
Source: neode-ui/src/services/contextBroker.ts:140-196 (browser half) +
13-AI-SPEC.md §4's execute_tool sketch (Rust half, no in-repo precedent).
Apply to: confirm.rs, ToolConfirmModal.vue, assistant_chat.rs's confirm-tool handler.
Backend-key-at-rest pattern (D-01/D-04)
Source: core/archipelago/src/api/rpc/mesh/assistant.rs:27-30 (data_dir/secrets/claude-api-key)
Apply to: backends/claude.rs — reuse the exact key path; do not introduce a parallel key
location (this is also the fix for the port-3142 proxy's separate ANTHROPIC_API_KEY — see
Open Question 1 in RESEARCH.md, which the plan must explicitly resolve).
Teleport-to-body modal (project-mandated pattern, repeatedly reinforced in CLAUDE.md)
Source: neode-ui/src/components/NostrSignConsent.vue
Apply to: ToolConfirmModal.vue — full-screen backdrop, Teleport to="body", never
rendered inside the iframe.
Scoped-token minting via authenticated RPC (never a long-lived credential in a URL)
Source: neode-ui/src/api/filebrowser-client.ts (pattern good) / same file (streamUrl,
leak to avoid)
Apply to: assistant-client.ts and any new content/tool streaming URL construction.
Budget-capped payment, never errors, degrades to None
Source: core/archipelago/src/swarm/payment.rs::auto_pay_token
Apply to: backends/routstr.rs — reuse verbatim, do not reimplement Cashu token building.
No Analog Found
| File | Role | Data Flow | Reason |
|---|---|---|---|
core/archipelago/src/assistant/loop_.rs |
service | event-driven multi-turn | First tool-calling agent loop in this codebase (confirmed by RESEARCH.md/AI-SPEC.md); build from the AI-SPEC §3/§4 sketch directly, not from an in-repo analog. |
core/archipelago/src/assistant/tools.rs |
model/schema | transform | No curated-tool-registry precedent exists; D-06 explicitly rejects deriving it from dispatcher.rs. Build from AI-SPEC §4b.1's ToolDef/schemars sketch. |
core/archipelago/src/assistant/backends/routstr.rs (HTTP client half) |
service | request-response | No OpenAI-compatible client exists in this codebase; wire format is CITED (medium confidence) from docs.routstr.com, not independently verified — RESEARCH.md recommends a live-relay spike before hand-writing this file. |
core/archipelago/src/music/tags.rs |
utility | transform | New lofty dependency, no existing audio-tag-extraction code in this codebase; gate cargo add lofty behind checkpoint:human-verify per RESEARCH.md's package-legitimacy note. |
neode-ui/src/composables/archyContentAdapter.ts |
utility (adapter) | transform | No shape-mapping precedent between Archy's ContentItem and any external metadata-rich type; must be hand-written and fixture-pinned (Pitfall 4). |
Iframe sandbox enforcement mechanism (Chat.vue sandbox/CSP change, file TBD by the plan) |
config | — | Open Question 2 in RESEARCH.md is explicitly unresolved — no existing sandbox/CSP-scoping code to copy; the plan must pick a mechanism (iframe sandbox attribute vs. connect-src scoping vs. accepted residual risk) before a file/pattern can be assigned. |
Port-3142 claude-api-proxy.py retirement/gating (file TBD by the plan — nginx config edit, script edit, or deletion) |
config | — | Open Question 1 in RESEARCH.md is explicitly unresolved (delete vs. gate vs. defer); no pattern to extract until the plan decides which. |
Metadata
Analog search scope: core/archipelago/src/{mesh,api/rpc,swarm,streaming}/,
neode-ui/src/{services,components,composables,api,types}/,
/home/archipelago/Projects/AIUI/packages/{app,core}/src/
Files scanned: ~20 read/grepped directly across both repos
Pattern extraction date: 2026-08-03