--- phase: 13-aiui-functional-conversational-node-control-and-content-surf plan: 01 type: execute wave: 1 depends_on: [] files_modified: - core/archipelago/src/assistant/mod.rs - core/archipelago/src/assistant/tools.rs - core/archipelago/src/assistant/loop_.rs - core/archipelago/src/assistant/backends/mod.rs - core/archipelago/src/assistant/backends/claude.rs - core/archipelago/src/assistant/backends/scripted.rs - core/archipelago/src/api/rpc/assistant_chat.rs - core/archipelago/src/api/rpc/dispatcher.rs - core/archipelago/src/main.rs - neode-ui/src/types/aiui-protocol.ts - neode-ui/src/services/contextBroker.ts - /home/archipelago/Projects/AIUI/packages/app/src/services/archyBridge.ts - /home/archipelago/Projects/AIUI/packages/app/src/composables/useAI.ts autonomous: true requirements: [AIUI-01] must_haves: truths: - "An operator types a plain-language question in the embedded AIUI chat and gets an answer computed from real node state (D-01)" - "The Claude API key never leaves the node — no model key is present in any bundle neode-ui or AIUI ships to the browser (D-01)" - "assistant.chat is unreachable without an authenticated session; UNAUTHENTICATED_METHODS is not widened (Phase-10 hard constraint)" - "A tool the model names but which is not in the curated registry returns a `no such tool` error turn, never an execution (D-06)" - "AIUI still runs standalone with its own dev proxy when `embedded` is false (D-17)" - "A pending confirmation is in-memory only: a daemon restart mid-wait resolves it as declined and never executes it, and a second user message while one is pending neither clears nor auto-approves it (edge: AIUI-01 concurrency)" - statement: "With two browser tabs open on the same node, the first valid confirmation nonce wins and the second is refused as a nonce mismatch rather than executing twice" verification: backstop artifacts: - path: "core/archipelago/src/assistant/mod.rs" provides: "The D-02 shared assistant service root: CallerScope, PermissionCategory, chat() entry" contains: "pub enum CallerScope" - path: "core/archipelago/src/assistant/tools.rs" provides: "D-06 curated tool registry — ToolDef and the first read-only tool" contains: "pub struct ToolDef" - path: "core/archipelago/src/assistant/loop_.rs" provides: "run_loop + execute_tool — the single choke point every tool call passes through" contains: "async fn execute_tool" - path: "core/archipelago/src/assistant/backends/mod.rs" provides: "Backend trait + BackendTurn — the wire-format-agnostic seam" contains: "pub trait Backend" - path: "core/archipelago/src/api/rpc/assistant_chat.rs" provides: "assistant.* RPC sub-dispatcher and handle_assistant_chat" contains: "handle_assistant" - path: "neode-ui/src/services/contextBroker.ts" provides: "chat:request / chat:response transport over the existing origin-checked postMessage channel" contains: "chat:request" key_links: - from: "/home/archipelago/Projects/AIUI/packages/app/src/composables/useAI.ts" to: "neode-ui/src/services/contextBroker.ts" via: "archyBridge.sendChat() postMessage when embedded — replaces the direct api/claude fetch" pattern: "sendChat" - from: "neode-ui/src/services/contextBroker.ts" to: "core/archipelago/src/api/rpc/assistant_chat.rs" via: "rpcClient.call({ method: 'assistant.chat' }) on the page's own session cookie + CSRF token" pattern: "assistant\\.chat" - from: "core/archipelago/src/assistant/loop_.rs" to: "core/archipelago/src/api/rpc/dispatcher.rs" via: "execute_tool dispatches to handle_system_disk_status — the same handler every authenticated caller uses" pattern: "handle_system_disk_status" --- Prove the whole spine end-to-end with one read-only tool: a typed question in the embedded AIUI chat travels over the existing origin-checked postMessage channel to neode-ui's broker, onto the node over the page's authenticated RPC session, into a Rust agent loop that calls a model, executes exactly one curated tool against the node's real `system.disk-status` handler, feeds the result back to the model, and returns a real answer that renders in the chat. This is the tracer slice for Phase 13 (D-01, D-02, D-06). It is production-quality, not a prototype — every later plan expands out from it: more tools (13-05), the confirm gate (13-08), more backends (13-10, 13-13), content grids (13-06). Nothing in it is a stub that would need an architectural change to fill. Purpose: catch an architectural dead-end after one commit instead of after ten. The four layers this phase spans (Rust agent service, RPC dispatch, neode-ui broker, AIUI client) have never been wired together; if the shape is wrong, it is wrong here. Output: `core/archipelago/src/assistant/`, the `assistant.*` RPC surface, the `chat:*` postMessage message types, and AIUI's embedded-mode chat branch. **Noun that is now primary:** a **caller scope** — a caller identity carrying the permission scope its tool calls resolve authority through. "A mesh peer" and "the local operator in AIUI" are two variants of it; Pine voice will be a third. **Decision: `promote`.** Rationale: D-02's stated intent is "callers distinguished by permission scope", and today's `mesh/listener/assist.rs` shapes its peer-facing controls (`trusted_only`, `allowed_contacts`, `denied_askers`) around the single mesh caller. Adding AIUI's permissions *alongside* a still-mesh-shaped model would recreate exactly the two divergent security models D-02 exists to prevent — the seam where they diverge is the seam where a future tool gets the wrong authority. Concretely: `assistant/mod.rs` defines `CallerScope` as the primary representation, with variants `Mesh { peer_id }` and `LocalOperator { session_id }` (and a documented, not-yet-built `Voice` slot). `CallerScope::granted_categories()` is the **only** source of authority `execute_tool` reads. The mesh controls are demoted to inputs that the `Mesh` variant resolves its granted set from — they keep working unchanged for mesh/LoRa callers, they just stop being the shape everything else is bolted onto. **Suggested (not required) invariant test:** `assistant::tests::every_caller_variant_resolves_authority_through_caller_scope` — iterate every `CallerScope` variant, assert each one's tool authority comes from `granted_categories()` and that no `execute_tool` branch reads a mesh-specific field directly. Goes red if a future phase reintroduces the mesh-only assumption. None in this plan. **Edge-probe accounting for AIUI-01.** Its probe resolved `covered` and produced two findings, both here: the pending-confirmation lifecycle truth tagged `(edge: AIUI-01 concurrency)` above, and the two-tab nonce finding carried as a `verification: backstop` scalar rather than a plain truth. The four probes that returned `unclassified` belong to other requirements and are surfaced where those requirements live — AIUI-02 in 13-05, AIUI-04 and AIUI-05 in 13-09, AIUI-06 in 13-15. Six requirements probed, two `covered`, four `unclassified`, nothing dropped; the full reconciliation with its counts is in `13-VALIDATION.md` § Edge-Probe Reconciliation. Symbols created by **this plan** (excluded from drift verification — they do not exist yet): **Rust — `core/archipelago/src/assistant/`** - `mod.rs`: `CallerScope` (enum: `Mesh`, `LocalOperator`), `PermissionCategory` (enum, D-16's 10 categories), `ToolExecCtx` (struct), `pub async fn chat(...)`, `AssistantError` - `tools.rs`: `ToolDef` (struct), `ToolRegistry` (struct), `ToolCall`, `ToolResult`, `ChatMessage`, `Role`, `fn registry()`, `fn system_disk_status_tool()`, `struct SystemDiskStatusArgs`, `ToolDef::validate` - `loop_.rs`: `pub async fn run_loop`, `async fn execute_tool`, `const MAX_TURNS` - `backends/mod.rs`: `pub trait Backend`, `enum BackendTurn`, `fn select_backend` - `backends/claude.rs`: `struct ClaudeBackend`, `const CLAUDE_MODEL`, `const ASSISTANT_HTTP_TIMEOUT`, `const ASSISTANT_MAX_TOKENS` - `backends/scripted.rs`: `struct ScriptedBackend` (`#[cfg(test)]` only) **Rust — RPC** - `core/archipelago/src/api/rpc/assistant_chat.rs`: `handle_assistant` (prefix sub-dispatcher), `handle_assistant_chat` - New RPC method names: `assistant.chat` - `core/archipelago/src/main.rs`: `mod assistant;` **TypeScript — neode-ui** - `types/aiui-protocol.ts`: `AIUIChatRequest`, `ArchyChatResponse` (added to the `AIUIRequest` / `ArchyResponse` unions) - `services/contextBroker.ts`: `handleChatRequest` (private method) **TypeScript — AIUI (`/home/archipelago/Projects/AIUI`, branch `development`)** - `services/archyBridge.ts`: `sendChat(text, onToken)` exported on `archyBridge` - `composables/useAI.ts`: `streamViaArchy` (embedded-mode branch) @$HOME/.claude/gsd-core/workflows/execute-plan.md @$HOME/.claude/gsd-core/templates/summary.md @.planning/PROJECT.md @.planning/ROADMAP.md @.planning/STATE.md @CLAUDE.md @.planning/phases/13-aiui-functional-conversational-node-control-and-content-surf/13-CONTEXT.md @.planning/phases/13-aiui-functional-conversational-node-control-and-content-surf/13-AI-SPEC.md @.planning/phases/13-aiui-functional-conversational-node-control-and-content-surf/13-PATTERNS.md Task 1: End-to-end "how much space is left" — the Rust spine, one tool, one backend core/archipelago/src/assistant/mod.rs, core/archipelago/src/assistant/tools.rs, core/archipelago/src/assistant/loop_.rs, core/archipelago/src/assistant/backends/mod.rs, core/archipelago/src/assistant/backends/claude.rs, core/archipelago/src/assistant/backends/scripted.rs, core/archipelago/src/api/rpc/assistant_chat.rs, core/archipelago/src/api/rpc/dispatcher.rs, core/archipelago/src/main.rs - `.planning/phases/13-aiui-functional-conversational-node-control-and-content-surf/13-AI-SPEC.md` §3 (the `run_loop` sketch, the `Backend` trait, `ScriptedBackend`) and §4 (the `execute_tool` sketch). **This file IS the pattern source — 13-PATTERNS.md records "no analog exists in this codebase" for `loop_.rs` and `tools.rs`.** - `core/archipelago/src/mesh/listener/assist.rs` — the analog for `backends/claude.rs`'s HTTP client construction (`call_claude`), and for the "spawned off the loop so it never blocks" concurrency discipline. Read `call_ollama`/`call_claude`/`run_assist`/`is_sender_allowed` in full. - `core/archipelago/src/api/rpc/mesh/assistant.rs` — the exact analog for `assistant_chat.rs`'s handler shape (`impl RpcHandler` + `pub(in crate::api::rpc) async fn handle_* -> Result`) and for the `data_dir/secrets/claude-api-key` availability probe at lines 27-30. - `core/archipelago/src/api/rpc/dispatcher.rs` lines 440-480 — the registration block; note `"mesh.assistant-status"` at 445 and `"system.disk-status" => self.handle_system_disk_status()` at 470. - `core/archipelago/src/api/rpc/mod.rs` lines 264-330 — the session-cookie + CSRF + `role.can_access(&method)` gate every dispatched method already passes through. - `core/archipelago/src/api/rpc/middleware.rs` — `UNAUTHENTICATED_METHODS`. Read it to confirm you are not adding to it. - `core/archipelago/src/swarm/payment.rs` — read the `#[tokio::test]` module at the bottom for this codebase's async unit-test convention. Create the `assistant` module — the D-02 shared service — following AI-SPEC §3's structure exactly, and register `mod assistant;` in `core/archipelago/src/main.rs` (this is a binary-only crate; there is no `lib.rs`, so all tests are in-crate `#[cfg(test)] mod tests`). `mod.rs` defines the promoted primary noun per the `` block above: `pub enum CallerScope { Mesh { peer_id: String }, LocalOperator { session_id: String } }` with `fn granted_categories(&self) -> BTreeSet`, plus `pub enum PermissionCategory` carrying D-16's ten variants (`Apps`, `System`, `Network`, `Wallet`, `Files`, `Media`, `Search`, `AiLocal`, `Notes`, `Bitcoin`), a `ToolExecCtx { registry, caller: CallerScope, handler: Arc }`, and the public `pub async fn chat(ctx, user_text) -> Result` entry. For this tracer `LocalOperator::granted_categories` returns `{System}` sourced from a hardcoded default set — 13-05 replaces that source with the persisted D-16 default-closed grants store, which is a data-source change, not an architectural one. `Mesh::granted_categories` resolves from the existing `trusted_only`/`allowed_contacts`/`denied_askers` inputs so mesh callers behave exactly as today. `tools.rs` defines `ToolDef { name: &'static str, description: &'static str, parameters: serde_json::Value, category: PermissionCategory, destructive: bool }`, the normalized `ChatMessage`/`Role`/`ToolCall { id, name, arguments: Value }`/`ToolResult { call_id, content, is_error }` types from AI-SPEC §3, a `ToolRegistry` wrapping a `&'static [ToolDef]`-backed lookup by name, and exactly ONE tool: `system_disk_status` — category `System`, `destructive: false`, description naming that it reports free and total disk space on this node. **Do NOT add the `schemars` crate** — it is not in `Cargo.toml` and is not covered by 13-RESEARCH.md's Package Legitimacy Audit, so adding it would bypass the package-legitimacy gate. Instead hand-write `parameters` as a `serde_json::json!` JSON Schema object literal adjacent to a `#[derive(Deserialize)] struct SystemDiskStatusArgs` (empty for this tool), and add a unit test that round-trips the schema's declared `required` keys through `serde_json::from_value::` so the schema and the deserialization target cannot drift apart silently. `ToolDef::validate(&self, raw: &Value)` deserializes-and-refuses per AI-SPEC §4b.1 — never coerce, never guess, never panic. `backends/mod.rs` defines `#[async_trait] pub trait Backend { async fn send(&self, system: &str, tools: &[ToolDef], history: &[ChatMessage]) -> Result }` and `pub enum BackendTurn { Text(String), ToolCalls(Vec) }`, plus `select_backend()` returning the first available backend in D-04's order. For this tracer only the Claude leg is implemented; `select_backend` must be written so `backends/ollama.rs` (13-10) and `backends/routstr.rs` (13-13) slot in ahead of and behind it without changing the trait — that is the architectural commitment this tracer is proving. `backends/claude.rs` implements `Backend` against the Anthropic Messages API: key read from `self.config.data_dir.join("secrets/claude-api-key")` (the SAME path `mesh/rpc/mesh/assistant.rs` probes — do not introduce a second key location), model `claude-haiku-4-5-20251001`, `max_tokens: 2048`, `tools` mapped from `ToolDef.parameters` into Anthropic's `input_schema` field, `tool_choice: {"type":"auto","disable_parallel_tool_use":true}` per AI-SPEC §3 Pitfall 5, `stream: false`. Parse `content` blocks of `type: "tool_use"` into `BackendTurn::ToolCalls` echoing `tool_use.id` into `ToolCall.id`; parse `type: "text"` into `BackendTurn::Text`. Define NEW module-scoped constants `ASSISTANT_HTTP_TIMEOUT` (180s) and `ASSISTANT_MAX_TOKENS` (2048) — do NOT import `OLLAMA_TIMEOUT`/`MAX_REPLY_CHARS`/`CHUNK_CHARS` from `assist.rs`, which are LoRa-airtime-tuned (AI-SPEC §3 Pitfall 6). `backends/scripted.rs` is `#[cfg(test)]`-gated and implements `Backend` by replaying a canned `Vec`, per AI-SPEC §5. It must never compile into the shipped binary. `loop_.rs` implements `run_loop` and `execute_tool` per AI-SPEC §3/§4 with `const MAX_TURNS: usize = 8`. `execute_tool` is the single choke point and, in this tracer, already enforces: unknown-tool refusal (returns an error turn naming the missing tool, never silently ignores), the `ctx.caller.granted_categories()` check, and `ToolDef::validate` before execution. The `destructive` branch is present and returns a not-yet-implemented error for any destructive tool — there are none in the registry yet, and 13-08 fills that branch with the real confirm gate. A tool's `execute` dispatches to the SAME `RpcHandler` method every other authenticated caller uses (`handle_system_disk_status`) — never a parallel AI-only code path. `api/rpc/assistant_chat.rs` adds `handle_assistant(&self, method: &str, params) -> Result` as a prefix sub-dispatcher plus `handle_assistant_chat`. Register in `dispatcher.rs` as a SINGLE guarded arm `m if m.starts_with("assistant.") => self.handle_assistant(m, params).await` placed adjacent to the `"mesh.assistant-*"` block at ~445, so every later `assistant.*` method (13-05's `list-tools`/`grants-*`, 13-08's `confirm-tool`, 13-10's `history`) is added inside `assistant_chat.rs` and `dispatcher.rs` is touched exactly once in this phase. This also settles RESEARCH Open Question 4: the `role.can_access(&rpc_req.method)` RBAC check runs upstream in `api/rpc/mod.rs` on the full method string BEFORE dispatch, so `assistant.*` inherits it unchanged with no bespoke auth — assert this rather than assume it, with the test named below. Add `#[cfg(test)] mod tests` in `loop_.rs` (or `mod.rs`) with: `disk_status_tool_executes` (a `ScriptedBackend` emitting one `ToolCalls` turn then one `Text` turn; asserts the tool ran and the real disk figures reached the final answer), `unknown_tool_is_refused_not_ignored`, and `assistant_methods_require_session` (asserts no string starting with `assistant.` appears in `UNAUTHENTICATED_METHODS`). cd core && CARGO_INCREMENTAL=0 cargo test --package archipelago assistant:: 2>&1 | tail -20 cd core && CARGO_INCREMENTAL=0 cargo test --package archipelago assistant_methods_require_session grep -c 'schemars' core/archipelago/Cargo.toml | grep -qx 0 - `core/archipelago/src/assistant/mod.rs` contains `pub enum CallerScope` with both a `Mesh` and a `LocalOperator` variant, and `fn granted_categories` - `core/archipelago/src/assistant/tools.rs` contains `pub struct ToolDef` with fields `category` and `destructive` - `core/archipelago/src/assistant/backends/mod.rs` contains `pub trait Backend` and `pub enum BackendTurn` - `core/archipelago/src/assistant/loop_.rs` contains `async fn execute_tool` and `const MAX_TURNS: usize = 8` - `core/archipelago/src/assistant/backends/scripted.rs` opens with a `#![cfg(test)]` or is declared behind `#[cfg(test)] mod scripted;` in `backends/mod.rs` — `grep -n 'cfg(test)' core/archipelago/src/assistant/backends/mod.rs` returns a match - `cd core && cargo test --package archipelago assistant::` exits 0 with `disk_status_tool_executes`, `unknown_tool_is_refused_not_ignored` and `assistant_methods_require_session` all listed as passing - `grep -n 'assistant\.' core/archipelago/src/api/rpc/middleware.rs` returns no match (UNAUTHENTICATED_METHODS not widened) - `grep -c 'starts_with("assistant.")' core/archipelago/src/api/rpc/dispatcher.rs` returns 1 — exactly one dispatcher arm for the whole `assistant.*` surface - `grep -n 'secrets/claude-api-key' core/archipelago/src/assistant/backends/claude.rs` returns a match, and `grep -rn 'ANTHROPIC_API_KEY' core/archipelago/src/assistant/` returns no match (one key ledger, D-01) - `grep -rnE 'OLLAMA_TIMEOUT|MAX_REPLY_CHARS|CHUNK_CHARS' core/archipelago/src/assistant/` returns no match (AI-SPEC §3 Pitfall 6) - `grep -c 'schemars' core/archipelago/Cargo.toml` returns 0 — no unaudited crate added D-01 makes the `assistant.*` RPC surface a contract AIUI and later the voice pipeline are written against; re-homing the loop browser-side afterwards means re-implementing every tool in TypeScript and moving key handling. Flagged per CONTEXT.md's own rating, not gated. A `ScriptedBackend` turn naming `system_disk_status` causes the real `handle_system_disk_status` to run and its real figures to appear in the loop's final answer; an unknown tool name returns an error turn; no `assistant.*` method is reachable unauthenticated. Task 2: neode-ui carries chat over the existing origin-checked bridge neode-ui/src/types/aiui-protocol.ts, neode-ui/src/services/contextBroker.ts - `neode-ui/src/types/aiui-protocol.ts` (full file, 98 lines) — `AIContextCategory`, `AIActionType`, the `AIUIRequest`/`ArchyResponse` unions, `AIUI_PROTOCOL_VERSION`, `AIUI_MESSAGE_PREFIX`. - `neode-ui/src/services/contextBroker.ts` (full file) — the constructor's `allowedOrigin` derivation (lines 26-33), the `event.origin !== this.allowedOrigin` guard at line 65, the `handleMessage` switch at lines 71-84, the `install-app` confirm block at 140-196, and `postToIframe` at 620. - `neode-ui/src/services/__tests__/contextBroker.test.ts` — the existing suite this change must keep green. - `neode-ui/src/api/rpc-client.ts` — the `rpcClient.call({ method, params })` signature used throughout the broker. Extend the protocol and the broker with a chat transport. This is D-03's split made concrete: the browser keeps only what only it can do; everything that reads or changes the node goes over the node-side registry. In `aiui-protocol.ts` add `export interface AIUIChatRequest { type: 'chat:request'; id: string; text: string }` and `export interface ArchyChatResponse { type: 'chat:response'; id: string; success: boolean; text?: string; error?: string }`, and add each to the `AIUIRequest` and `ArchyResponse` unions respectively. Do NOT add a `tool-call` member to `AIActionType` — tool selection is node-side by D-01/D-03 and must never be expressible as an AIUI-originated action. In `contextBroker.ts` add `case 'chat:request': this.handleChatRequest(msg.id, msg.text); break;` to the existing `handleMessage` switch, and a private `async handleChatRequest(id, text)` that calls `rpcClient.call<{ text: string }>({ method: 'assistant.chat', params: { text } })` and posts the result back through the existing `postToIframe` helper as a `chat:response`. Use the existing `this.allowedOrigin` transport primitive — do NOT add a second postMessage channel and do NOT relax the origin check. On RPC failure post `{ success: false, error }` with the error message, never the raw exception object. The broker must NOT pass a permission category through for chat: authority is resolved node-side from `CallerScope` (Task 1), and duplicating a browser-side gate here would create the second security model D-02 exists to prevent. Add a comment at the handler naming that reason so a future reader does not "helpfully" add one back. cd neode-ui && npx vitest run src/services/__tests__/contextBroker.test.ts cd neode-ui && npx vue-tsc --noEmit - `grep -q "chat:request" neode-ui/src/types/aiui-protocol.ts` and `grep -q "ArchyChatResponse" neode-ui/src/types/aiui-protocol.ts` - `grep -q "assistant.chat" neode-ui/src/services/contextBroker.ts` - `grep -c "tool-call" neode-ui/src/types/aiui-protocol.ts` returns 0 — `AIActionType` was not widened - `cd neode-ui && npx vitest run src/services/__tests__/contextBroker.test.ts` exits 0 (existing suite still green) - `cd neode-ui && npx vue-tsc --noEmit` exits 0 - `grep -c "allowedOrigin" neode-ui/src/services/contextBroker.ts` is unchanged or higher — the origin guard was not removed or loosened A `chat:request` postMessage from the allowed origin produces an `assistant.chat` RPC on the page's own session and a `chat:response` back to the iframe; a message from any other origin is still dropped. Task 3: AIUI delegates the loop to the node when embedded, keeps its own when not /home/archipelago/Projects/AIUI/packages/app/src/services/archyBridge.ts, /home/archipelago/Projects/AIUI/packages/app/src/composables/useAI.ts - `/home/archipelago/Projects/AIUI/packages/app/src/services/archyBridge.ts` (full file) — `postToParent`, the `allowedOrigin` validation at line 52, `deriveParentOrigin()` at lines 95-115, and the `archyBridge` export object at line 117. - `/home/archipelago/Projects/AIUI/packages/app/src/composables/useAI.ts` — `BASE`/`CLAUDE_PATH`/`OPENROUTER_PATH` at lines 16-18, `streamClaude` at 261, `streamOpenRouter` at 326, and the three call sites at 564/566, 647/649, 759/761. - `/home/archipelago/Projects/AIUI/packages/app/src/composables/useArchy.ts` — the `__AIUI_EMBEDDED__` detection at lines 80-81 and the existing `archyBridge.requestContext` usage at 134. **Mirror this postMessage convention; do not invent a third transport.** - `.planning/phases/13-.../13-CONTEXT.md` D-17 — embedded delegates to the node, standalone keeps its own proxy and its own fast dev loop. Work in `/home/archipelago/Projects/AIUI` on branch `development` (push access is confirmed — D-18 is satisfied, do not re-verify). Add `sendChat(text: string): Promise<{ text: string }>` to the `archyBridge` export object in `archyBridge.ts`, built on the existing `postToParent` + origin-validated listener pattern already used by `requestContext` — same request-id correlation, same `allowedOrigin` check, a 180s timeout matching the node's `ASSISTANT_HTTP_TIMEOUT`. Reject with a plain `Error` on timeout or on `success: false`. In `useAI.ts` add `streamViaArchy(history, onToken, onError, signal)` that calls `archyBridge.sendChat` with the latest user turn and emits the returned text through `onToken`. Branch each of the three existing send sites (lines ~564, ~647, ~759) on the same `__AIUI_EMBEDDED__` signal `useArchy.ts` already reads: when embedded, call `streamViaArchy`; otherwise keep `streamClaude`/`streamOpenRouter` exactly as they are. `streamClaude` and `streamOpenRouter` are NOT deleted — D-17 keeps standalone mode working with AIUI's own proxy for development and for anyone running AIUI outside a node. Do not remove `CLAUDE_PATH`/`OPENROUTER_PATH`; plan 13-02 changes what those paths resolve to on a node (a session-gated Rust forwarder) and 13-09 retires them, in that order. Commit and push on `development` with a message naming the Archy phase, per CLAUDE.md's commit-and-push-every-unit-of-work rule. Stage explicitly by path. cd /home/archipelago/Projects/AIUI/packages/app && npx vitest run cd /home/archipelago/Projects/AIUI/packages/app && npx vue-tsc --noEmit cd /home/archipelago/Projects/AIUI && git log --oneline -1 && git status --porcelain | grep -c . | grep -qx 0 - `grep -q "sendChat" /home/archipelago/Projects/AIUI/packages/app/src/services/archyBridge.ts` - `grep -q "streamViaArchy" /home/archipelago/Projects/AIUI/packages/app/src/composables/useAI.ts` - `grep -c "streamClaude" /home/archipelago/Projects/AIUI/packages/app/src/composables/useAI.ts` is ≥ 1 — standalone mode was not deleted (D-17) - `cd /home/archipelago/Projects/AIUI/packages/app && npx vitest run` exits 0 — AIUI's own test command is `vitest run` (**confirmed at plan time**, resolving 13-VALIDATION.md's Wave 0 "AIUI test command UNCONFIRMED" item) - `cd /home/archipelago/Projects/AIUI/packages/app && npx vue-tsc --noEmit` exits 0 - `cd /home/archipelago/Projects/AIUI && git status --porcelain` is empty and `git log --oneline -1` shows the new commit on `development` An embedded AIUI chat send produces a `chat:request` postMessage instead of a direct `api/claude` fetch; a standalone AIUI chat send still uses `streamClaude`; both test suites are green and the AIUI commit is pushed. ## Trust Boundaries | Boundary | Description | |----------|-------------| | AIUI iframe → neode-ui page | Untrusted-by-design content crosses via postMessage; origin-checked, but same-origin today (no browser-enforced sandbox — see 13-09) | | neode-ui page → node `/rpc` | Authenticated: session cookie + CSRF + `role.can_access()` (`api/rpc/mod.rs:264-330`) | | model output → `execute_tool` | The model's output is an **input** to the check, never the check. This is the phase's load-bearing boundary | | node → api.anthropic.com | The only egress in this plan; carries the system prompt, tool schemas and the turn | ## STRIDE Threat Register | Threat ID | Category | Component | Severity | Disposition | Mitigation Plan | |-----------|----------|-----------|----------|-------------|-----------------| | T-13-01 | Elevation of Privilege | `assistant.chat` RPC | high | mitigate | Registered in the normal `dispatcher.rs` table so the existing session + CSRF + RBAC gate runs before dispatch; asserted by `assistant_methods_require_session`. `UNAUTHENTICATED_METHODS` untouched (Phase-10 hard constraint) | | T-13-02 | Elevation of Privilege | `execute_tool` unknown-tool path | high | mitigate | D-06 curated allowlist; an unregistered name returns an error turn, never a dispatch. Asserted by `unknown_tool_is_refused_not_ignored` | | T-13-03 | Information Disclosure | Claude API key | critical | mitigate | Key read server-side from `data_dir/secrets/claude-api-key` inside `backends/claude.rs`; never serialized into any RPC response and never present in a browser bundle. Asserted by the no-`ANTHROPIC_API_KEY`-in-`assistant/` grep | | T-13-04 | Tampering | AIUI forging a `chat:request` from another origin | medium | mitigate | The broker's existing `event.origin !== this.allowedOrigin` guard is reused unchanged; no second postMessage channel is added | | T-13-05 | Denial of Service | Model loops without terminating | medium | mitigate | `MAX_TURNS = 8` hard stop in `run_loop`; the loop bails with a user-facing error rather than spinning | | T-13-06 | Spoofing | Model claims a tool ran that did not | medium | accept | Not structurally preventable — no gate constrains prose. Measured behaviourally as E-01's integrity half in 13-14; recorded as prohibition P-1 there | | T-13-07 | Elevation of Privilege | Two live Claude credential paths (`secrets/claude-api-key` vs the port-3142 proxy's `ANTHROPIC_API_KEY`) | high | mitigate | Out of this plan's scope by sequencing: 13-02 collapses them to one ledger in the same wave. This plan is forbidden from creating a third — asserted by the grep above | | T-13-SC | Tampering | npm/pip/cargo installs | high | mitigate | This plan adds **zero** new packages. `schemars` was explicitly rejected because it is absent from 13-RESEARCH.md's Package Legitimacy Audit; JSON Schema is hand-written instead. Asserted by the `schemars` count-0 gate | - `cd core && CARGO_INCREMENTAL=0 cargo test --package archipelago assistant::` green - `cd neode-ui && npx vitest run src/services/__tests__/contextBroker.test.ts && npx vitest run src/views/__tests__/chatAiuiEmbed.test.ts` green (both pre-existing suites) - `cd /home/archipelago/Projects/AIUI/packages/app && npx vitest run` green - On a running node with a valid session cookie and CSRF token, `assistant.chat` with `{"text":"how much space is left"}` returns a body containing the node's real free-space figure — the same number `system.disk-status` returns directly The spine is proven: typed chat in the embedded AIUI reaches a curated node tool and a real answer comes back, over authenticated transport, with the model key never leaving the node — and every later plan in this phase can be built as an expansion of this slice rather than a parallel mechanism. Create `.planning/phases/13-aiui-functional-conversational-node-control-and-content-surf/13-01-SUMMARY.md` when done