@ggui-ai/iframe-runtime 0.2.0-alpha.3 → 0.3.0-rc.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/channel-transport.d.ts +20 -30
- package/dist/channel-transport.js +24 -24
- package/dist/channels/data.d.ts +5 -20
- package/dist/channels/data.js +4 -52
- package/dist/channels/index.d.ts +0 -1
- package/dist/channels/index.js +0 -1
- package/dist/channels/props-update.d.ts +9 -9
- package/dist/channels/props-update.js +9 -9
- package/dist/channels/render-frame.d.ts +5 -5
- package/dist/channels/render-frame.js +5 -5
- package/dist/compiled-validators.d.ts +2 -2
- package/dist/compiled-validators.js +1 -1
- package/dist/context-observer.d.ts +4 -4
- package/dist/context-observer.js +3 -3
- package/dist/events-polling.d.ts +7 -7
- package/dist/events-polling.js +5 -5
- package/dist/globals.d.ts +1 -1
- package/dist/host-context-emitter.d.ts +4 -4
- package/dist/host-context-emitter.js +9 -9
- package/dist/iframe-runtime.js +94 -76
- package/dist/index.d.ts +4 -4
- package/dist/index.js +1 -1
- package/dist/lifecycle.d.ts +3 -3
- package/dist/lifecycle.js +29 -6
- package/dist/mcp-app-iframe-host.d.ts +8 -8
- package/dist/mcp-app-iframe-host.js +4 -4
- package/dist/meta-parse.d.ts +3 -4
- package/dist/meta-parse.js +17 -44
- package/dist/observability.d.ts +14 -36
- package/dist/observability.js +2 -1
- package/dist/protocol-error.d.ts +38 -48
- package/dist/protocol-error.js +5 -23
- package/dist/provisional-renderer.js +3 -3
- package/dist/react-renderer.d.ts +16 -6
- package/dist/react-renderer.js +22 -40
- package/dist/registry-subscribe.d.ts +7 -7
- package/dist/registry-subscribe.js +28 -14
- package/dist/render-item.d.ts +22 -15
- package/dist/render-item.js +7 -14
- package/dist/runtime.d.ts +79 -105
- package/dist/runtime.js +219 -269
- package/dist/status-dom.d.ts +12 -12
- package/dist/status-dom.js +11 -11
- package/dist/system-cards/NoCredentialsCard.d.ts +1 -1
- package/dist/system-cards/NoCredentialsCard.js +1 -1
- package/dist/system-cards/ProtocolProbeCard.d.ts +13 -17
- package/dist/system-cards/ProtocolProbeCard.js +21 -25
- package/dist/system-cards/index.d.ts +5 -5
- package/dist/system-cards/index.js +5 -5
- package/dist/types.d.ts +29 -34
- package/dist/validation.d.ts +4 -5
- package/dist/validation.js +5 -6
- package/dist/wire-config.d.ts +43 -106
- package/dist/wire-config.js +29 -195
- package/package.json +12 -11
- package/dist/channels/feedback.d.ts +0 -14
- package/dist/channels/feedback.js +0 -19
package/dist/wire-config.d.ts
CHANGED
|
@@ -1,112 +1,65 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Per-render `WireConfig` factory for the renderer iframe.
|
|
3
3
|
*
|
|
4
|
-
* The renderer iframe mounts EXACTLY ONE {@link
|
|
4
|
+
* The renderer iframe mounts EXACTLY ONE {@link GguiSession} per iframe
|
|
5
5
|
* post-render-identity-collapse (2026-05-27). There is no per-item
|
|
6
6
|
* scoping factory anymore — the WireConfig is built once at boot,
|
|
7
|
-
* keyed by the bootstrap's `
|
|
7
|
+
* keyed by the bootstrap's `sessionId`, and the active render's
|
|
8
8
|
* `actionSpec` is wired in via the {@link buildRootWireConfig}'s
|
|
9
|
-
* `
|
|
9
|
+
* `getCurrentGguiSession` thunk.
|
|
10
10
|
*
|
|
11
|
-
*
|
|
12
|
-
* `@ggui-ai/wire`
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
11
|
+
* The envelope-build → validate → emit pipeline and the bus-backed
|
|
12
|
+
* subscribe seam live in `@ggui-ai/wire`'s shared `buildWireConfig`
|
|
13
|
+
* (one implementation for every first-party renderer — see the MCP
|
|
14
|
+
* Apps Compliance principle). This module is the iframe-side adapter:
|
|
15
|
+
* it injects the CSP-safe precompiled outbound validator, the
|
|
16
|
+
* ProtocolError dual-emission policy, and the tools/call-vs-WS
|
|
17
|
+
* dispatch transport.
|
|
17
18
|
*/
|
|
18
|
-
import type { ActionEnvelope,
|
|
19
|
-
import type {
|
|
20
|
-
import { ClientContractViolationError, type WireConfig } from '@ggui-ai/wire';
|
|
19
|
+
import type { ActionEnvelope, GguiSession } from '@ggui-ai/protocol';
|
|
20
|
+
import type { GguiSessionSeedInput } from './types.js';
|
|
21
|
+
import { ClientContractViolationError, StreamBus, type WireConfig } from '@ggui-ai/wire';
|
|
21
22
|
import type { WebSocketMessage } from '@ggui-ai/protocol/transport/websocket';
|
|
22
23
|
/**
|
|
23
24
|
* Outbound send surface — the minimal shape wire-config calls on the
|
|
24
25
|
* WS transport. Pre-B3b this read off `Pick<RendererWebSocketManager,
|
|
25
26
|
* 'send'>`; post-B3b the WS lives inside `@ggui-ai/live-channel`,
|
|
26
|
-
* but wire-config doesn't import the
|
|
27
|
+
* but wire-config doesn't import the library directly — it consumes
|
|
27
28
|
* the shape via this local type so tests can stub `{send: vi.fn()}`
|
|
28
29
|
* without dragging in the live-channel types.
|
|
29
30
|
*
|
|
30
|
-
* Frames sent here are always `{type: 'action', payload: ...}`
|
|
31
|
-
*
|
|
32
|
-
*
|
|
31
|
+
* Frames sent here are always `{type: 'action', payload: ...}`
|
|
32
|
+
* envelopes — wire-config never sends transport-layer frames
|
|
33
|
+
* (`ping`, `subscribe`).
|
|
33
34
|
*/
|
|
34
35
|
export interface RendererSendSurface {
|
|
35
36
|
readonly send: (message: WebSocketMessage) => void;
|
|
36
37
|
}
|
|
37
38
|
import { type ProtocolErrorEmitter } from './protocol-error.js';
|
|
38
|
-
|
|
39
|
-
/**
|
|
40
|
-
* Tiny fan-out bus keyed by channel name. `subscribe.ts`'s inbound-
|
|
41
|
-
* message handler pushes validated `data` envelopes here; wire's
|
|
42
|
-
* `useStream(channel)` subscribes per-component via the config's
|
|
43
|
-
* `subscribe()` method.
|
|
44
|
-
*
|
|
45
|
-
* Late-subscriber replay for reserved channels:
|
|
46
|
-
*
|
|
47
|
-
* The renderer iframe boots in a strict ordering:
|
|
48
|
-
* `subscribe()` → first `ack` → render stack items → mount
|
|
49
|
-
* `mountProvisional` → it then calls `streamBus.subscribe('_ggui:
|
|
50
|
-
* preview', …)`. Server-side replay frames for `_ggui:preview` (the
|
|
51
|
-
* provisional A2UI preamble fired during the agent's `ggui_render`
|
|
52
|
-
* BEFORE the user's browser navigated to the viewer) arrive over the
|
|
53
|
-
* WS in the window between ack-resolve and the provisional mount —
|
|
54
|
-
* they hit `emit()` before any listener has subscribed. Without a
|
|
55
|
-
* bounded reserved-channel ring, those frames vanish and the preview
|
|
56
|
-
* surface stays stuck on the spinner.
|
|
57
|
-
*
|
|
58
|
-
* Server-owned `_ggui:*` channels are buffered in a per-channel ring
|
|
59
|
-
* capped at {@link RESERVED_CHANNEL_REPLAY_MAX}. New subscribers
|
|
60
|
-
* receive the buffered envelopes synchronously before the unsubscribe
|
|
61
|
-
* handle returns — same mental model as the server-side
|
|
62
|
-
* `SessionStreamBuffer.replay(...)` walk over reserved channels at
|
|
63
|
-
* ack time, but mirrored at the inner bus boundary so the host
|
|
64
|
-
* transport stays portable (Claude Desktop / ChatGPT / Cursor /
|
|
65
|
-
* `<McpAppIframe>` all behave the same).
|
|
66
|
-
*
|
|
67
|
-
* Agent-declared (non-reserved) channels are NOT buffered here. Their
|
|
68
|
-
* replay is the server's job — the renderer's `subscribe()` handshake
|
|
69
|
-
* pulls history per `streamSpec.replay` policy when reconnecting with
|
|
70
|
-
* `fromSeq`. Buffering them at this layer would double-count and
|
|
71
|
-
* change the semantics of agent contracts.
|
|
72
|
-
*
|
|
73
|
-
* No observable mutation outside the renderer boundary — the bus is
|
|
74
|
-
* internal plumbing.
|
|
75
|
-
*/
|
|
76
|
-
export declare class StreamBus {
|
|
77
|
-
private listeners;
|
|
78
|
-
/**
|
|
79
|
-
* Per-channel bounded ring of envelopes for reserved (`_ggui:*`)
|
|
80
|
-
* channels only. FIFO eviction at `RESERVED_CHANNEL_REPLAY_MAX`.
|
|
81
|
-
* Replayed to every new subscriber for that channel.
|
|
82
|
-
*/
|
|
83
|
-
private reservedReplay;
|
|
84
|
-
subscribe(channel: string, listener: (env: StreamEnvelope) => void): () => void;
|
|
85
|
-
emit(envelope: StreamEnvelope): void;
|
|
86
|
-
}
|
|
39
|
+
export { StreamBus };
|
|
87
40
|
export interface BuildRootWireConfigOptions {
|
|
88
|
-
readonly
|
|
41
|
+
readonly sessionId: string;
|
|
89
42
|
readonly appId: string;
|
|
90
43
|
/**
|
|
91
|
-
* Read the currently-mounted {@link
|
|
44
|
+
* Read the currently-mounted {@link GguiSession}. The config's `dispatch`
|
|
92
45
|
* resolves the active render's `actionSpec` through this thunk so
|
|
93
|
-
* the
|
|
94
|
-
*
|
|
95
|
-
*
|
|
46
|
+
* the outbound validator stays coherent across props_update patches
|
|
47
|
+
* (which replace the render reference) without rebuilding the
|
|
48
|
+
* WireConfig.
|
|
96
49
|
*/
|
|
97
|
-
readonly
|
|
98
|
-
/** Handle to the renderer's WS manager; used for outbound `action`
|
|
50
|
+
readonly getCurrentGguiSession: () => GguiSession | GguiSessionSeedInput | null;
|
|
51
|
+
/** Handle to the renderer's WS manager; used for outbound `action` frames. */
|
|
99
52
|
readonly manager: RendererSendSurface;
|
|
100
53
|
/** Shared bus for inbound stream deliveries. */
|
|
101
54
|
readonly streamBus: StreamBus;
|
|
102
55
|
/**
|
|
103
56
|
* Optional sink for contract violations. Default: tagged
|
|
104
57
|
* `console.warn`. Tests inject a recorder. Matches
|
|
105
|
-
* `
|
|
58
|
+
* `GguiRender.onError` semantics on the client-contract path.
|
|
106
59
|
*
|
|
107
60
|
* Prefer {@link onProtocolError} for new integrations — it receives
|
|
108
61
|
* the widened {@link ProtocolError} union that covers every error
|
|
109
|
-
* the renderer classifies (transport, auth, protocol,
|
|
62
|
+
* the renderer classifies (transport, auth, protocol,
|
|
110
63
|
* bootstrap, version, unknown). The narrow `onContractViolation`
|
|
111
64
|
* sink stays for in-renderer tests that assert the raw class shape;
|
|
112
65
|
* every violation is ALSO forwarded through `onProtocolError` so
|
|
@@ -117,64 +70,48 @@ export interface BuildRootWireConfigOptions {
|
|
|
117
70
|
* Optional sink for every typed {@link ProtocolError} the renderer
|
|
118
71
|
* classifies. Default: {@link defaultProtocolErrorEmitter}
|
|
119
72
|
* (`console.warn` with a grep-friendly tag). The `<McpAppIframe>`
|
|
120
|
-
* host wrapper wires this to its `onError` prop
|
|
121
|
-
* variants are ALSO mirrored to the `_ggui:contract-error` WS
|
|
122
|
-
* envelope.
|
|
73
|
+
* host wrapper wires this to its `onError` prop.
|
|
123
74
|
*
|
|
124
75
|
* The emitter fires for CLIENT-side contract violations (via
|
|
125
76
|
* `fromClientContractViolation`). Other sites (subscribe failures,
|
|
126
|
-
* upgrade-required, transport errors
|
|
127
|
-
*
|
|
128
|
-
* a shape change.
|
|
77
|
+
* upgrade-required, transport errors) plumb through the same
|
|
78
|
+
* emitter without a shape change.
|
|
129
79
|
*/
|
|
130
80
|
readonly onProtocolError?: ProtocolErrorEmitter;
|
|
131
|
-
/**
|
|
132
|
-
* Optional {@link ObservabilityEmitter} sink. Fires a
|
|
133
|
-
* `wired-tool-invoked` event every time {@link dispatchByItem}
|
|
134
|
-
* successfully sends a wired-action envelope (i.e. the ctx's
|
|
135
|
-
* resolved `tool` is populated AND outbound validation passed).
|
|
136
|
-
*
|
|
137
|
-
* The event is client-side — `dispatchedAt` records when the
|
|
138
|
-
* envelope left the renderer, NOT when the server's router
|
|
139
|
-
* completed the tool. The SERVER independently emits a
|
|
140
|
-
* `wired-tool.invoked` telemetry event on its own sink for
|
|
141
|
-
* operational metrics; the two surfaces are deliberately separate
|
|
142
|
-
* (host inspector vs operator telemetry).
|
|
143
|
-
*/
|
|
144
|
-
readonly onObserve?: ObservabilityEmitter;
|
|
145
81
|
/**
|
|
146
82
|
* Optional outbound action sink. When provided, REPLACES the
|
|
147
83
|
* default WS-frame send (`manager.send({type:'action', payload})`).
|
|
148
84
|
*
|
|
149
85
|
* The WS live-channel exists for streamSpec subscriptions (inbound
|
|
150
|
-
* `ggui_emit` fanout + `props_update` + `
|
|
151
|
-
* + `drain_ack` + `channel_payload`).
|
|
152
|
-
* on
|
|
86
|
+
* `ggui_emit` fanout + `props_update` + `render` + `data`
|
|
87
|
+
* + `drain_ack` + `channel_payload`). For an MCP-Apps embed, user
|
|
88
|
+
* actions belong on the host relay: per spec §401 the iframe relays
|
|
153
89
|
* `tools/call:ggui_runtime_submit_action` through the host (the
|
|
154
90
|
* `_meta.ui.visibility:['app']` channel), and the server's MCP
|
|
155
91
|
* handler appends to `pendingEventConsumer` so `ggui_consume`
|
|
156
|
-
* wakes the agent.
|
|
92
|
+
* wakes the agent. A cross-host iframe cannot assume a reachable
|
|
93
|
+
* WS endpoint at all, so production MCP-Apps callers MUST supply
|
|
94
|
+
* this option.
|
|
157
95
|
*
|
|
158
|
-
* The default
|
|
159
|
-
*
|
|
160
|
-
*
|
|
161
|
-
*
|
|
96
|
+
* The default WS-frame send reaches the agent only on servers whose
|
|
97
|
+
* live channel bridges WS `data:submit` actions onto the same
|
|
98
|
+
* pending-events pipe (first-party `createGguiServer` does — see
|
|
99
|
+
* `GguiSessionChannelOptions.pendingEventConsumer`); it remains the
|
|
100
|
+
* seam tests and direct-WS callers exercise.
|
|
162
101
|
*
|
|
163
102
|
* Called AFTER outbound validation passes. Receives the validated
|
|
164
103
|
* {@link ActionEnvelope}; caller extracts payload.action +
|
|
165
104
|
* payload.data + slice meta to route via the host's tools/call
|
|
166
105
|
* relay (see iframe-runtime's `routeDispatch`).
|
|
167
|
-
*
|
|
168
|
-
* Tests omit this opt to exercise the legacy WS-frame path.
|
|
169
106
|
*/
|
|
170
107
|
readonly onDispatchEnvelope?: (envelope: ActionEnvelope) => void;
|
|
171
108
|
}
|
|
172
109
|
/**
|
|
173
110
|
* Build the per-render `WireConfig` for the iframe runtime.
|
|
174
111
|
* Bootstraps the renderer's outbound emission + inbound subscription
|
|
175
|
-
* seams. Returns a `WireConfig` keyed by the bootstrap's `
|
|
112
|
+
* seams. Returns a `WireConfig` keyed by the bootstrap's `sessionId`;
|
|
176
113
|
* the active render's `actionSpec` is resolved through the
|
|
177
|
-
* {@link BuildRootWireConfigOptions.
|
|
114
|
+
* {@link BuildRootWireConfigOptions.getCurrentGguiSession} thunk on every
|
|
178
115
|
* dispatch so props_update patches don't require rebuilding the
|
|
179
116
|
* config.
|
|
180
117
|
*
|
package/dist/wire-config.js
CHANGED
|
@@ -1,133 +1,16 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { buildWireConfig, StreamBus, } from '@ggui-ai/wire';
|
|
2
2
|
import { validateOutboundActionEnvelope } from './validation.js';
|
|
3
3
|
import { defaultProtocolErrorEmitter, fromClientContractViolation, } from './protocol-error.js';
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* Reserved-channel namespace prefix (mirror of
|
|
10
|
-
* `@ggui-ai/protocol::RESERVED_CHANNEL_PREFIX`). Inlined to avoid
|
|
11
|
-
* widening the protocol import surface here — the constant is
|
|
12
|
-
* load-bearing for the late-subscriber replay rule below; equality
|
|
13
|
-
* with the protocol export is locked by the StreamBus tests.
|
|
14
|
-
*/
|
|
15
|
-
const RESERVED_CHANNEL_PREFIX = '_ggui:';
|
|
16
|
-
/**
|
|
17
|
-
* Per-channel ring cap for late-subscriber replay on reserved channels.
|
|
18
|
-
* Mirrors the server-side `DEFAULT_SESSION_STREAM_BUFFER_MAX` posture:
|
|
19
|
-
* bounded so a long-lived iframe can't grow memory unbounded if a
|
|
20
|
-
* reserved channel emits in a hot loop.
|
|
21
|
-
*
|
|
22
|
-
* 256 is enough headroom for the deterministic A2UI emitter (one frame
|
|
23
|
-
* per fragment, ~5–15 frames per push) plus several refresh cycles, and
|
|
24
|
-
* still fits comfortably in memory.
|
|
25
|
-
*/
|
|
26
|
-
const RESERVED_CHANNEL_REPLAY_MAX = 256;
|
|
27
|
-
/**
|
|
28
|
-
* Tiny fan-out bus keyed by channel name. `subscribe.ts`'s inbound-
|
|
29
|
-
* message handler pushes validated `data` envelopes here; wire's
|
|
30
|
-
* `useStream(channel)` subscribes per-component via the config's
|
|
31
|
-
* `subscribe()` method.
|
|
32
|
-
*
|
|
33
|
-
* Late-subscriber replay for reserved channels:
|
|
34
|
-
*
|
|
35
|
-
* The renderer iframe boots in a strict ordering:
|
|
36
|
-
* `subscribe()` → first `ack` → render stack items → mount
|
|
37
|
-
* `mountProvisional` → it then calls `streamBus.subscribe('_ggui:
|
|
38
|
-
* preview', …)`. Server-side replay frames for `_ggui:preview` (the
|
|
39
|
-
* provisional A2UI preamble fired during the agent's `ggui_render`
|
|
40
|
-
* BEFORE the user's browser navigated to the viewer) arrive over the
|
|
41
|
-
* WS in the window between ack-resolve and the provisional mount —
|
|
42
|
-
* they hit `emit()` before any listener has subscribed. Without a
|
|
43
|
-
* bounded reserved-channel ring, those frames vanish and the preview
|
|
44
|
-
* surface stays stuck on the spinner.
|
|
45
|
-
*
|
|
46
|
-
* Server-owned `_ggui:*` channels are buffered in a per-channel ring
|
|
47
|
-
* capped at {@link RESERVED_CHANNEL_REPLAY_MAX}. New subscribers
|
|
48
|
-
* receive the buffered envelopes synchronously before the unsubscribe
|
|
49
|
-
* handle returns — same mental model as the server-side
|
|
50
|
-
* `SessionStreamBuffer.replay(...)` walk over reserved channels at
|
|
51
|
-
* ack time, but mirrored at the inner bus boundary so the host
|
|
52
|
-
* transport stays portable (Claude Desktop / ChatGPT / Cursor /
|
|
53
|
-
* `<McpAppIframe>` all behave the same).
|
|
54
|
-
*
|
|
55
|
-
* Agent-declared (non-reserved) channels are NOT buffered here. Their
|
|
56
|
-
* replay is the server's job — the renderer's `subscribe()` handshake
|
|
57
|
-
* pulls history per `streamSpec.replay` policy when reconnecting with
|
|
58
|
-
* `fromSeq`. Buffering them at this layer would double-count and
|
|
59
|
-
* change the semantics of agent contracts.
|
|
60
|
-
*
|
|
61
|
-
* No observable mutation outside the renderer boundary — the bus is
|
|
62
|
-
* internal plumbing.
|
|
63
|
-
*/
|
|
64
|
-
export class StreamBus {
|
|
65
|
-
listeners = new Map();
|
|
66
|
-
/**
|
|
67
|
-
* Per-channel bounded ring of envelopes for reserved (`_ggui:*`)
|
|
68
|
-
* channels only. FIFO eviction at `RESERVED_CHANNEL_REPLAY_MAX`.
|
|
69
|
-
* Replayed to every new subscriber for that channel.
|
|
70
|
-
*/
|
|
71
|
-
reservedReplay = new Map();
|
|
72
|
-
subscribe(channel, listener) {
|
|
73
|
-
let bucket = this.listeners.get(channel);
|
|
74
|
-
if (bucket === undefined) {
|
|
75
|
-
bucket = new Set();
|
|
76
|
-
this.listeners.set(channel, bucket);
|
|
77
|
-
}
|
|
78
|
-
bucket.add(listener);
|
|
79
|
-
// Replay buffered reserved-channel envelopes BEFORE returning the
|
|
80
|
-
// unsubscribe handle so the new listener is fully caught up by the
|
|
81
|
-
// time `subscribe()` returns. Order matches arrival order at
|
|
82
|
-
// `emit()` (FIFO ring).
|
|
83
|
-
if (channel.startsWith(RESERVED_CHANNEL_PREFIX)) {
|
|
84
|
-
const ring = this.reservedReplay.get(channel);
|
|
85
|
-
if (ring !== undefined) {
|
|
86
|
-
for (const env of ring) {
|
|
87
|
-
listener(env);
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
return () => {
|
|
92
|
-
const current = this.listeners.get(channel);
|
|
93
|
-
if (current === undefined)
|
|
94
|
-
return;
|
|
95
|
-
current.delete(listener);
|
|
96
|
-
if (current.size === 0)
|
|
97
|
-
this.listeners.delete(channel);
|
|
98
|
-
};
|
|
99
|
-
}
|
|
100
|
-
emit(envelope) {
|
|
101
|
-
if (envelope.channel.startsWith(RESERVED_CHANNEL_PREFIX)) {
|
|
102
|
-
let ring = this.reservedReplay.get(envelope.channel);
|
|
103
|
-
if (ring === undefined) {
|
|
104
|
-
ring = [];
|
|
105
|
-
this.reservedReplay.set(envelope.channel, ring);
|
|
106
|
-
}
|
|
107
|
-
ring.push(envelope);
|
|
108
|
-
if (ring.length > RESERVED_CHANNEL_REPLAY_MAX) {
|
|
109
|
-
// FIFO drop — keeps the most recent
|
|
110
|
-
// `RESERVED_CHANNEL_REPLAY_MAX` frames. Late subscribers may
|
|
111
|
-
// miss the createSurface fragment if the ring overflows (would
|
|
112
|
-
// render as Spinner-stuck), but that's an upper-bound contract
|
|
113
|
-
// on a bounded buffer, not a silent failure mode in practice.
|
|
114
|
-
ring.shift();
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
const bucket = this.listeners.get(envelope.channel);
|
|
118
|
-
if (bucket === undefined)
|
|
119
|
-
return;
|
|
120
|
-
for (const listener of bucket) {
|
|
121
|
-
listener(envelope);
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
}
|
|
4
|
+
// `StreamBus` (and its bounded reserved-channel replay ring) was
|
|
5
|
+
// hoisted into `@ggui-ai/wire` so `<GguiRender>` and this runtime share
|
|
6
|
+
// one implementation. Re-exported for the runtime's internal modules.
|
|
7
|
+
export { StreamBus };
|
|
125
8
|
/**
|
|
126
9
|
* Build the per-render `WireConfig` for the iframe runtime.
|
|
127
10
|
* Bootstraps the renderer's outbound emission + inbound subscription
|
|
128
|
-
* seams. Returns a `WireConfig` keyed by the bootstrap's `
|
|
11
|
+
* seams. Returns a `WireConfig` keyed by the bootstrap's `sessionId`;
|
|
129
12
|
* the active render's `actionSpec` is resolved through the
|
|
130
|
-
* {@link BuildRootWireConfigOptions.
|
|
13
|
+
* {@link BuildRootWireConfigOptions.getCurrentGguiSession} thunk on every
|
|
131
14
|
* dispatch so props_update patches don't require rebuilding the
|
|
132
15
|
* config.
|
|
133
16
|
*
|
|
@@ -137,13 +20,7 @@ export class StreamBus {
|
|
|
137
20
|
* indirection collapsed to a single `WireConfig`.
|
|
138
21
|
*/
|
|
139
22
|
export function buildRootWireConfig(opts) {
|
|
140
|
-
const clientSeqBox = { current: 0 };
|
|
141
|
-
function nextSeq() {
|
|
142
|
-
clientSeqBox.current += 1;
|
|
143
|
-
return clientSeqBox.current;
|
|
144
|
-
}
|
|
145
23
|
const emitProtocolError = opts.onProtocolError ?? defaultProtocolErrorEmitter;
|
|
146
|
-
const emitObserve = opts.onObserve ?? (() => { });
|
|
147
24
|
function surfaceViolation(err) {
|
|
148
25
|
// Dual emission — the narrow `onContractViolation` sink stays
|
|
149
26
|
// for in-renderer tests that assert the raw class shape; the
|
|
@@ -156,7 +33,7 @@ export function buildRootWireConfig(opts) {
|
|
|
156
33
|
else if (opts.onProtocolError === undefined) {
|
|
157
34
|
// No caller-supplied sink at all → operator-visible fallback so
|
|
158
35
|
// dev consoles see the violation. Matches
|
|
159
|
-
// `
|
|
36
|
+
// `GguiRender.surfaceContractViolation`'s console.warn posture.
|
|
160
37
|
// eslint-disable-next-line no-console
|
|
161
38
|
console.warn('[ggui:contract] ' + err.message, {
|
|
162
39
|
direction: err.direction,
|
|
@@ -165,93 +42,50 @@ export function buildRootWireConfig(opts) {
|
|
|
165
42
|
}
|
|
166
43
|
emitProtocolError(fromClientContractViolation(err));
|
|
167
44
|
}
|
|
168
|
-
return {
|
|
45
|
+
return buildWireConfig({
|
|
169
46
|
app: { appId: opts.appId, appName: opts.appId },
|
|
170
|
-
render: {
|
|
47
|
+
render: { sessionId: opts.sessionId, isConnected: true },
|
|
171
48
|
auth: { isAuthenticated: false },
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
const currentRender = opts.
|
|
178
|
-
|
|
49
|
+
// Resolve the active render's actionSpec on every dispatch.
|
|
50
|
+
// Per-render lifecycle: props_update patches replace the render
|
|
51
|
+
// reference; reading through the thunk keeps the outbound
|
|
52
|
+
// validator coherent without rebuilding the config.
|
|
53
|
+
getActiveActionSpec: () => {
|
|
54
|
+
const currentRender = opts.getCurrentGguiSession();
|
|
55
|
+
return currentRender !== null &&
|
|
179
56
|
currentRender.type !== 'mcpApps' &&
|
|
180
57
|
currentRender.type !== 'system'
|
|
181
58
|
? currentRender.actionSpec
|
|
182
59
|
: undefined;
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
action: actionName,
|
|
190
|
-
data: data,
|
|
191
|
-
...(tool ? { tool } : {}),
|
|
192
|
-
},
|
|
193
|
-
clientSeq: nextSeq(),
|
|
194
|
-
});
|
|
195
|
-
const result = validateOutboundActionEnvelope(activeActionSpec, envelope);
|
|
196
|
-
if (!result.valid) {
|
|
197
|
-
surfaceViolation(new ClientContractViolationError('outbound-action', result.violations));
|
|
198
|
-
return;
|
|
199
|
-
}
|
|
60
|
+
},
|
|
61
|
+
// The iframe's precompiled-validator variant — the dispatch never
|
|
62
|
+
// trips the iframe's no-`unsafe-eval` CSP.
|
|
63
|
+
validateEnvelope: validateOutboundActionEnvelope,
|
|
64
|
+
onViolation: surfaceViolation,
|
|
65
|
+
emitEnvelope: (envelope) => {
|
|
200
66
|
if (opts.onDispatchEnvelope !== undefined) {
|
|
201
67
|
// Spec-canonical path — the iframe-runtime's LIVE-mode boot
|
|
202
68
|
// wires this to `routeDispatch`, which postMessages
|
|
203
69
|
// `tools/call:ggui_runtime_submit_action` through the MCP-Apps
|
|
204
70
|
// host relay. The default WS-frame send below is retained for
|
|
205
|
-
// tests +
|
|
71
|
+
// tests + direct-WS callers that don't relay tools/call.
|
|
206
72
|
opts.onDispatchEnvelope(envelope);
|
|
207
73
|
}
|
|
208
74
|
else {
|
|
209
75
|
sendActionEnvelope(opts.manager, envelope);
|
|
210
76
|
}
|
|
211
|
-
// Observability: a wired-tool dispatch is a `data:submit` whose
|
|
212
|
-
// resolved action has a `tool` binding. Plain agent-routed actions
|
|
213
|
-
// (no `tool` on the actionSpec) fire a `dispatch` activity row in
|
|
214
|
-
// the console via the session-side ring buffer; only wired tools
|
|
215
|
-
// get this observability event. The server separately emits the
|
|
216
|
-
// operational telemetry variant on its TelemetrySink.
|
|
217
|
-
if (tool !== undefined) {
|
|
218
|
-
emitObserve({
|
|
219
|
-
kind: 'wired-tool-invoked',
|
|
220
|
-
toolName: tool,
|
|
221
|
-
actionName,
|
|
222
|
-
dispatchedAt: new Date().toISOString(),
|
|
223
|
-
});
|
|
224
|
-
}
|
|
225
|
-
},
|
|
226
|
-
subscribe: (channelName, handler) => {
|
|
227
|
-
return opts.streamBus.subscribe(channelName, (env) => {
|
|
228
|
-
// `WireConfig<DataContract>.subscribe`'s handler expects a
|
|
229
|
-
// `StreamDelivery<unknown>`; the bus ships the raw envelope
|
|
230
|
-
// payload unmodified. The type-level `unknown` narrowing
|
|
231
|
-
// lives on the caller's generic boundary (useStream /
|
|
232
|
-
// useContract).
|
|
233
|
-
handler({
|
|
234
|
-
payload: env.payload,
|
|
235
|
-
mode: env.mode,
|
|
236
|
-
...(env.complete !== undefined ? { complete: env.complete } : {}),
|
|
237
|
-
});
|
|
238
|
-
});
|
|
239
77
|
},
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
// from inside the renderer. Cross-refs surface via
|
|
243
|
-
// `actionSpec[*].nextStep` (event metadata) and
|
|
244
|
-
// `streamSpec[*].source.tool` (channel data source).
|
|
245
|
-
};
|
|
78
|
+
streamBus: opts.streamBus,
|
|
79
|
+
});
|
|
246
80
|
}
|
|
247
81
|
// =============================================================================
|
|
248
82
|
// Outbound envelope emission
|
|
249
83
|
// =============================================================================
|
|
250
84
|
/**
|
|
251
85
|
* Send a validated action envelope over the live channel. The WS frame shape
|
|
252
|
-
* is `{type:'action', payload: envelope}` — matches
|
|
253
|
-
*
|
|
254
|
-
*
|
|
86
|
+
* is `{type:'action', payload: envelope}` — matches `useWebSocket.sendAction`
|
|
87
|
+
* exactly, so wire-emitted dispatches from the renderer are
|
|
88
|
+
* byte-equivalent to `GguiRender`-emitted ones.
|
|
255
89
|
*/
|
|
256
90
|
function sendActionEnvelope(manager, envelope) {
|
|
257
91
|
manager.send({ type: 'action', payload: envelope });
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ggui-ai/iframe-runtime",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0-rc.0",
|
|
4
4
|
"description": "The iframe-local ggui runtime: a self-contained ESM bundle that boots inside an MCP Apps host iframe, runs the protocol-version handshake, opens the live-channel WebSocket, and mounts the rendered UI. Bundles React, ReactDOM, and the ggui design system so generated component code has a single seam to read from.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ggui",
|
|
@@ -28,9 +28,6 @@
|
|
|
28
28
|
"types": "./dist/server.d.ts",
|
|
29
29
|
"import": "./dist/server.js",
|
|
30
30
|
"default": "./dist/server.js"
|
|
31
|
-
},
|
|
32
|
-
"./runtime.js": {
|
|
33
|
-
"default": "./dist/iframe-runtime.js"
|
|
34
31
|
}
|
|
35
32
|
},
|
|
36
33
|
"dependencies": {
|
|
@@ -38,14 +35,18 @@
|
|
|
38
35
|
"qrcode-generator": "^2.0.4",
|
|
39
36
|
"react": "^19.0.0",
|
|
40
37
|
"react-dom": "^19.0.0",
|
|
41
|
-
"@ggui-ai/live-channel": "0.
|
|
42
|
-
"@ggui-ai/
|
|
43
|
-
"@ggui-ai/design": "0.
|
|
44
|
-
"@ggui-ai/
|
|
45
|
-
"@ggui-ai/protocol": "0.
|
|
46
|
-
"@ggui-ai/wire": "0.
|
|
38
|
+
"@ggui-ai/live-channel": "0.3.0-rc.0",
|
|
39
|
+
"@ggui-ai/preview-a2ui": "0.3.0-rc.0",
|
|
40
|
+
"@ggui-ai/design": "0.3.0-rc.0",
|
|
41
|
+
"@ggui-ai/gadgets": "0.3.0-rc.0",
|
|
42
|
+
"@ggui-ai/protocol": "0.3.0-rc.0",
|
|
43
|
+
"@ggui-ai/wire": "0.3.0-rc.0"
|
|
44
|
+
},
|
|
45
|
+
"peerDependencies": {
|
|
46
|
+
"@modelcontextprotocol/sdk": "^1.27.0"
|
|
47
47
|
},
|
|
48
48
|
"devDependencies": {
|
|
49
|
+
"@modelcontextprotocol/sdk": "^1.27.0",
|
|
49
50
|
"@testing-library/react": "^16.0.0",
|
|
50
51
|
"@types/node": "^24.0.0",
|
|
51
52
|
"@types/react": "^19.0.0",
|
|
@@ -73,7 +74,7 @@
|
|
|
73
74
|
},
|
|
74
75
|
"author": "ggui contributors <hello@ggui.ai>",
|
|
75
76
|
"scripts": {
|
|
76
|
-
"build": "node esbuild.config.mjs && tsc -p tsconfig.types.json && pnpm run check:bundle-size",
|
|
77
|
+
"build": "rm -rf dist && node esbuild.config.mjs && tsc -p tsconfig.types.json && pnpm run check:bundle-size",
|
|
77
78
|
"check:bundle-size": "tsx scripts/check-bundle-size.ts",
|
|
78
79
|
"typecheck": "tsc --noEmit",
|
|
79
80
|
"test": "vitest run",
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* `feedback` channel handler — no-op stub.
|
|
3
|
-
*
|
|
4
|
-
* Legacy `feedback` frame: older servers emitted this as the round-trip
|
|
5
|
-
* half of the retired `tool:<name>` client-tool RPC path. Silently
|
|
6
|
-
* ignored under the `clientCapabilities` reframe; kept as a recognized
|
|
7
|
-
* type until the WS message union drops `'feedback'`. Registering a
|
|
8
|
-
* no-op handler is what keeps the registry's dispatch from logging
|
|
9
|
-
* "no handler for type 'feedback'" warnings — the silence is
|
|
10
|
-
* intentional, the handler documents that.
|
|
11
|
-
*/
|
|
12
|
-
import type { ChannelHandler } from '@ggui-ai/live-channel';
|
|
13
|
-
import type { JsonObject } from '@ggui-ai/protocol';
|
|
14
|
-
export declare function createFeedbackHandler(): ChannelHandler<JsonObject>;
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* `feedback` channel handler — no-op stub.
|
|
3
|
-
*
|
|
4
|
-
* Legacy `feedback` frame: older servers emitted this as the round-trip
|
|
5
|
-
* half of the retired `tool:<name>` client-tool RPC path. Silently
|
|
6
|
-
* ignored under the `clientCapabilities` reframe; kept as a recognized
|
|
7
|
-
* type until the WS message union drops `'feedback'`. Registering a
|
|
8
|
-
* no-op handler is what keeps the registry's dispatch from logging
|
|
9
|
-
* "no handler for type 'feedback'" warnings — the silence is
|
|
10
|
-
* intentional, the handler documents that.
|
|
11
|
-
*/
|
|
12
|
-
export function createFeedbackHandler() {
|
|
13
|
-
return {
|
|
14
|
-
type: 'feedback',
|
|
15
|
-
onMessage: () => {
|
|
16
|
-
/* no-op — legacy stub */
|
|
17
|
-
},
|
|
18
|
-
};
|
|
19
|
-
}
|