Files

32 KiB

phase, plan, type, wave, depends_on, files_modified, autonomous, requirements, must_haves
phase plan type wave depends_on files_modified autonomous requirements must_haves
13-aiui-functional-conversational-node-control-and-content-surf 01 execute 1
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
true
AIUI-01
truths artifacts key_links
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 verification
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 backstop
path provides contains
core/archipelago/src/assistant/mod.rs The D-02 shared assistant service root: CallerScope, PermissionCategory, chat() entry pub enum CallerScope
path provides contains
core/archipelago/src/assistant/tools.rs D-06 curated tool registry — ToolDef and the first read-only tool pub struct ToolDef
path provides contains
core/archipelago/src/assistant/loop_.rs run_loop + execute_tool — the single choke point every tool call passes through async fn execute_tool
path provides contains
core/archipelago/src/assistant/backends/mod.rs Backend trait + BackendTurn — the wire-format-agnostic seam pub trait Backend
path provides contains
core/archipelago/src/api/rpc/assistant_chat.rs assistant.* RPC sub-dispatcher and handle_assistant_chat handle_assistant
path provides contains
neode-ui/src/services/contextBroker.ts chat:request / chat:response transport over the existing origin-checked postMessage channel chat:request
from to via pattern
/home/archipelago/Projects/AIUI/packages/app/src/composables/useAI.ts neode-ui/src/services/contextBroker.ts archyBridge.sendChat() postMessage when embedded — replaces the direct api/claude fetch sendChat
from to via pattern
neode-ui/src/services/contextBroker.ts core/archipelago/src/api/rpc/assistant_chat.rs rpcClient.call({ method: 'assistant.chat' }) on the page's own session cookie + CSRF token assistant.chat
from to via pattern
core/archipelago/src/assistant/loop_.rs core/archipelago/src/api/rpc/dispatcher.rs execute_tool dispatches to handle_system_disk_status — the same handler every authenticated caller uses 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.

<assumption_delta_decision> 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. </assumption_delta_decision>

<flagged_assumptions> 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. </flagged_assumptions>

<artifacts_this_phase_produces> 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) </artifacts_this_phase_produces>

<execution_context> @$HOME/.claude/gsd-core/workflows/execute-plan.md @$HOME/.claude/gsd-core/templates/summary.md </execution_context>

@.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 <assumption_delta_decision> block above: pub enum CallerScope { Mesh { peer_id: String }, LocalOperator { session_id: String } } with fn granted_categories(&self) -> BTreeSet<PermissionCategory>, 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<RpcHandler> }, and the public pub async fn chat(ctx, user_text) -> Result<String> 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::<SystemDiskStatusArgs> 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<BackendTurn> } and pub enum BackendTurn { Text(String), ToolCalls(Vec<ToolCall>) }, 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<BackendTurn>, 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<Value> 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 <acceptance_criteria>

  • 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.rsgrep -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 </acceptance_criteria> 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 <acceptance_criteria>

  • 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 </acceptance_criteria> 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 <acceptance_criteria>

  • 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 </acceptance_criteria> 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.

<threat_model>

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
</threat_model>
- `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

<success_criteria> 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. </success_criteria>

Create `.planning/phases/13-aiui-functional-conversational-node-control-and-content-surf/13-01-SUMMARY.md` when done