feat(13-06): content:request/content:push channel on the existing bridge

- aiui-protocol.ts: AIUIContentRequest (kind + optional scope, no RPC
  method or params) and ArchyContentPush (adapted bundle + permitted
  flag).
- contextBroker.ts: handleContentRequest gates on the media/files
  permission categories (either grants access), resolves scope to
  content.list-mine / content.browse-peer (fanned out across every known
  federation peer) / content.owned-list, and routes results through
  archyContentAdapter's adaptContentItems before crossing the iframe
  boundary. contentRequestSeq is a monotonic guard: a stale RPC response
  that resolves after a newer content:request has started is discarded
  rather than posted (AIUI-03 concurrency edge).
- contextBroker.test.ts: permission-denied, own-scope, and stale/
  out-of-order coverage. Fixed a pre-existing latent flake risk in this
  file — perms.toggle() is not idempotent across tests because the
  permissions store persists to localStorage, which vi.clearAllMocks()
  does not reset; switched the new tests to perms.enableAll().

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
archipelago
2026-08-03 19:36:50 -04:00
co-authored by Claude Opus 5
parent f7691fd1bb
commit ce7a2b2cd4
3 changed files with 270 additions and 0 deletions
+34
View File
@@ -5,6 +5,8 @@
* Archy acts as a context broker — AIUI never directly accesses node data.
*/
import type { ArchyContentBundle } from '@/composables/archyContentAdapter'
/** Data categories that AIUI can request access to */
export type AIContextCategory =
| 'apps'
@@ -57,12 +59,27 @@ export interface AIUIChatRequest {
text: string
}
/**
* A content-grid request from AIUI's embedded-mode client. Carries only a
* `kind` discriminator and an optional `scope` — the iframe never names an
* RPC method or params (T-13-34); the broker decides the call. A single
* generic channel (not one per content type) so 13-11's music-library wave
* can extend `kind` without touching this file again.
*/
export interface AIUIContentRequest {
type: 'content:request'
id: string
kind: 'films' | 'songs' | 'podcasts' | 'all'
scope?: 'own' | 'peers' | 'owned'
}
export type AIUIRequest =
| AIUIContextRequest
| AIUIActionRequest
| AIUIReadyMessage
| AIUIThemeRequest
| AIUIChatRequest
| AIUIContentRequest
// ─── Archy → AIUI (Responses) ──────────────────────────────────────────────
@@ -104,12 +121,29 @@ export interface ArchyChatResponse {
error?: string
}
/**
* The node's answer to a `content:request` — adapted grid records
* (`archyContentAdapter.ts`'s `adaptContentItems` output) for whichever
* buckets the RPC scope produced. `permitted: false` means neither the
* `media` nor the `files` category is granted (content:request is treated
* as permitted if either is enabled — see `handleContentRequest`); the
* three arrays are empty in that case, never omitted, so AIUI can render an
* empty-state instead of hanging on an unresolved request.
*/
export type ArchyContentPush = {
type: 'content:push'
id: string
kind: string
permitted: boolean
} & Partial<ArchyContentBundle>
export type ArchyResponse =
| ArchyContextResponse
| ArchyActionResponse
| ArchyThemeResponse
| ArchyPermissionsUpdate
| ArchyChatResponse
| ArchyContentPush
// ─── All messages ───────────────────────────────────────────────────────────