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 ───────────────────────────────────────────────────────────