From 0ab9bdc7d3e310802b063aac7f87fb6aa2abcacd Mon Sep 17 00:00:00 2001 From: archipelago Date: Mon, 3 Aug 2026 14:37:41 -0400 Subject: [PATCH] feat(13-01): neode-ui carries chat over the existing origin-checked bridge MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds chat:request/chat:response to the AIUI postMessage protocol (AIUIChatRequest, ArchyChatResponse) and a handleChatRequest handler in contextBroker.ts that calls assistant.chat over rpcClient on the page's own session, then posts the result back through the existing postToIframe helper. Reuses the broker's existing allowedOrigin guard unchanged — no second postMessage channel, no relaxed origin check. No permission category is threaded through the chat handler on purpose: authority is resolved node-side from CallerScope (Task 1), and duplicating a browser-side gate here would recreate the second, divergent security model D-02 exists to prevent. tool-call is deliberately NOT added to AIActionType — tool selection stays node-side by D-01/D-03. On RPC failure the handler posts only the error message, never the raw exception object. Verified: contextBroker.test.ts (16/16) and chatAiuiEmbed.test.ts (7/7) green; vue-tsc --noEmit clean. Co-Authored-By: Claude Opus 5 (1M context) --- neode-ui/src/services/contextBroker.ts | 32 ++++++++++++++++++++++++++ neode-ui/src/types/aiui-protocol.ts | 24 +++++++++++++++++++ 2 files changed, 56 insertions(+) diff --git a/neode-ui/src/services/contextBroker.ts b/neode-ui/src/services/contextBroker.ts index 30b7dec2..1ae55e8c 100644 --- a/neode-ui/src/services/contextBroker.ts +++ b/neode-ui/src/services/contextBroker.ts @@ -5,6 +5,7 @@ import type { AIContextCategory, ArchyContextResponse, ArchyActionResponse, + ArchyChatResponse, } from '@/types/aiui-protocol' import { useAIPermissionsStore } from '@/stores/aiPermissions' import { useAppStore } from '@/stores/app' @@ -81,6 +82,37 @@ export class ContextBroker { case 'theme:request': this.sendTheme() break + case 'chat:request': + this.handleChatRequest(msg.id, msg.text) + break + } + } + + // Note: no permission category is threaded through here on purpose. + // Authority for a chat turn is resolved node-side from the RPC session's + // CallerScope (assistant.chat, core/archipelago/src/assistant/mod.rs) — + // duplicating a browser-side gate here would recreate the second, + // divergent security model D-02 exists to prevent. Do not "helpfully" + // add a permission check back into this handler. + private async handleChatRequest(id: string, text: string) { + try { + const result = await rpcClient.call<{ text: string }>({ + method: 'assistant.chat', + params: { text }, + }) + this.postToIframe({ + type: 'chat:response', + id, + success: true, + text: result.text, + } satisfies ArchyChatResponse) + } catch (err) { + this.postToIframe({ + type: 'chat:response', + id, + success: false, + error: err instanceof Error ? err.message : 'Chat request failed', + } satisfies ArchyChatResponse) } } diff --git a/neode-ui/src/types/aiui-protocol.ts b/neode-ui/src/types/aiui-protocol.ts index a1aa2361..d799ba0a 100644 --- a/neode-ui/src/types/aiui-protocol.ts +++ b/neode-ui/src/types/aiui-protocol.ts @@ -45,11 +45,24 @@ export interface AIUIThemeRequest { type: 'theme:request' } +/** + * A chat turn from AIUI's embedded-mode client. Carries only the raw user + * text — tool selection is node-side (D-01/D-03) and must never be + * expressible as an AIUI-originated action, so this is deliberately NOT an + * `AIActionType` member. + */ +export interface AIUIChatRequest { + type: 'chat:request' + id: string + text: string +} + export type AIUIRequest = | AIUIContextRequest | AIUIActionRequest | AIUIReadyMessage | AIUIThemeRequest + | AIUIChatRequest // ─── Archy → AIUI (Responses) ────────────────────────────────────────────── @@ -81,11 +94,22 @@ export interface ArchyPermissionsUpdate { categories: AIContextCategory[] } +/** The node's answer to a `chat:request`. On RPC failure, `error` carries + * only the error message — never the raw exception object. */ +export interface ArchyChatResponse { + type: 'chat:response' + id: string + success: boolean + text?: string + error?: string +} + export type ArchyResponse = | ArchyContextResponse | ArchyActionResponse | ArchyThemeResponse | ArchyPermissionsUpdate + | ArchyChatResponse // ─── All messages ───────────────────────────────────────────────────────────