feat(13-01): neode-ui carries chat over the existing origin-checked bridge

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) <noreply@anthropic.com>
This commit is contained in:
archipelago
2026-08-03 14:37:41 -04:00
co-authored by Claude Opus 5
parent fe6ccff73c
commit 0ab9bdc7d3
2 changed files with 56 additions and 0 deletions
+32
View File
@@ -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)
}
}
+24
View File
@@ -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 ───────────────────────────────────────────────────────────