@ggui-ai/iframe-runtime 0.2.0-alpha.4 → 0.4.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.
Files changed (58) hide show
  1. package/dist/channel-transport.d.ts +20 -30
  2. package/dist/channel-transport.js +24 -24
  3. package/dist/channels/data.d.ts +5 -20
  4. package/dist/channels/data.js +4 -52
  5. package/dist/channels/index.d.ts +0 -1
  6. package/dist/channels/index.js +0 -1
  7. package/dist/channels/props-update.d.ts +9 -9
  8. package/dist/channels/props-update.js +9 -9
  9. package/dist/channels/render-frame.d.ts +5 -5
  10. package/dist/channels/render-frame.js +5 -5
  11. package/dist/compiled-validators.d.ts +2 -2
  12. package/dist/compiled-validators.js +1 -1
  13. package/dist/context-observer.d.ts +4 -4
  14. package/dist/context-observer.js +3 -3
  15. package/dist/events-polling.d.ts +7 -7
  16. package/dist/events-polling.js +5 -5
  17. package/dist/globals.d.ts +1 -1
  18. package/dist/host-context-emitter.d.ts +4 -4
  19. package/dist/host-context-emitter.js +9 -9
  20. package/dist/iframe-runtime.js +68 -78
  21. package/dist/iframe-runtime.meta.json +1 -0
  22. package/dist/index.d.ts +4 -4
  23. package/dist/index.js +1 -1
  24. package/dist/lifecycle.d.ts +3 -3
  25. package/dist/lifecycle.js +29 -6
  26. package/dist/mcp-app-iframe-host.d.ts +8 -8
  27. package/dist/mcp-app-iframe-host.js +4 -4
  28. package/dist/meta-parse.d.ts +3 -4
  29. package/dist/meta-parse.js +17 -44
  30. package/dist/observability.d.ts +14 -36
  31. package/dist/observability.js +2 -1
  32. package/dist/protocol-error.d.ts +38 -48
  33. package/dist/protocol-error.js +5 -23
  34. package/dist/provisional-renderer.js +3 -3
  35. package/dist/react-renderer.d.ts +16 -6
  36. package/dist/react-renderer.js +22 -40
  37. package/dist/registry-subscribe.d.ts +7 -7
  38. package/dist/registry-subscribe.js +28 -14
  39. package/dist/render-item.d.ts +22 -15
  40. package/dist/render-item.js +7 -14
  41. package/dist/runtime.d.ts +79 -105
  42. package/dist/runtime.js +219 -269
  43. package/dist/status-dom.d.ts +12 -12
  44. package/dist/status-dom.js +11 -11
  45. package/dist/system-cards/NoCredentialsCard.d.ts +1 -1
  46. package/dist/system-cards/NoCredentialsCard.js +1 -1
  47. package/dist/system-cards/ProtocolProbeCard.d.ts +13 -17
  48. package/dist/system-cards/ProtocolProbeCard.js +21 -25
  49. package/dist/system-cards/index.d.ts +5 -5
  50. package/dist/system-cards/index.js +5 -5
  51. package/dist/types.d.ts +29 -34
  52. package/dist/validation.d.ts +4 -5
  53. package/dist/validation.js +5 -6
  54. package/dist/wire-config.d.ts +43 -106
  55. package/dist/wire-config.js +29 -195
  56. package/package.json +14 -12
  57. package/dist/channels/feedback.d.ts +0 -14
  58. package/dist/channels/feedback.js +0 -19
@@ -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 Render} per iframe
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 `renderId`, and the active render'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
- * `getCurrentRender` thunk.
9
+ * `getCurrentGguiSession` thunk.
10
10
  *
11
- * Outbound dispatch uses `buildActionEnvelope` (from
12
- * `@ggui-ai/wire`) + `validateOutboundActionEnvelope` + a direct
13
- * `manager.send({type:'action', payload})`.
14
- *
15
- * The `WireConfig` shape lives in `@ggui-ai/wire`. This module
16
- * implements against that interface.
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, Render, StreamEnvelope } from '@ggui-ai/protocol';
19
- import type { RenderSeedInput } from './types.js';
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 gadget directly — it consumes
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: ...}` or
31
- * `{type: 'feedback', payload: ...}` envelopes — wire-config never
32
- * sends transport-layer frames (`ping`, `subscribe`).
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
- import type { ObservabilityEmitter } from './observability.js';
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 renderId: string;
41
+ readonly sessionId: string;
89
42
  readonly appId: string;
90
43
  /**
91
- * Read the currently-mounted {@link Render}. The config's `dispatch`
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 `tool` + outbound validator stay coherent across props_update
94
- * patches (which replace the render reference) without rebuilding
95
- * the WireConfig.
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 getCurrentRender: () => Render | RenderSeedInput | null;
98
- /** Handle to the renderer's WS manager; used for outbound `action` + `feedback` frames. */
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
- * `GguiSession.onError` semantics on the client-contract path.
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, contract,
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; session-bound
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, server-emitted
127
- * contract-error envelopes) plumb through the same emitter without
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` + `push` + `data` + `feedback`
151
- * + `drain_ack` + `channel_payload`). Outbound user actions belong
152
- * on a different pipe — per MCP-Apps spec §401, the iframe relays
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 `manager.send({type:'action'})` path writes to the
159
- * session ledger only — it has no downstream consumer in OSS (no
160
- * `wiredActionRouter`, no bridge to `pendingEventConsumer`).
161
- * Production callers MUST supply this option to reach the agent.
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 `renderId`;
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.getCurrentRender} thunk on every
114
+ * {@link BuildRootWireConfigOptions.getCurrentGguiSession} thunk on every
178
115
  * dispatch so props_update patches don't require rebuilding the
179
116
  * config.
180
117
  *
@@ -1,133 +1,16 @@
1
- import { ClientContractViolationError, buildActionEnvelope, } from '@ggui-ai/wire';
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
- // StreamBus — the in-renderer bridge between inbound `data` frames
6
- // and per-component `useStream(channel)` subscribers.
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 `renderId`;
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.getCurrentRender} thunk on every
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
- // `GguiSession.surfaceContractViolation`'s console.warn posture.
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: { renderId: opts.renderId, isConnected: true },
47
+ render: { sessionId: opts.sessionId, isConnected: true },
171
48
  auth: { isAuthenticated: false },
172
- dispatch: (actionName, data) => {
173
- // Resolve the active render's actionSpec on every dispatch.
174
- // Per-render lifecycle: props_update patches replace the render
175
- // reference; reading through the thunk keeps the tool binding
176
- // + outbound validator coherent without rebuilding the config.
177
- const currentRender = opts.getCurrentRender();
178
- const activeActionSpec = currentRender !== null &&
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
- const entry = activeActionSpec?.[actionName];
184
- const tool = entry?.nextStep;
185
- const envelope = buildActionEnvelope({
186
- renderId: opts.renderId,
187
- type: 'data:submit',
188
- payload: {
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 + legacy callers that don't relay tools/call.
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
- // `callWiredTool` is retired — `agentTools` is now a catalog the
241
- // AGENT invokes; the component never reaches the underlying tool
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 today's
253
- * `useWebSocket.sendAction` exactly, so wire-emitted dispatches from
254
- * the renderer are byte-equivalent to `GguiSession`-emitted ones.
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.2.0-alpha.4",
3
+ "version": "0.4.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.2.0-alpha.4",
42
- "@ggui-ai/preview-a2ui": "0.2.0-alpha.4",
43
- "@ggui-ai/gadgets": "0.2.0-alpha.4",
44
- "@ggui-ai/design": "0.2.0-alpha.4",
45
- "@ggui-ai/protocol": "0.2.0-alpha.4",
46
- "@ggui-ai/wire": "0.2.0-alpha.4"
38
+ "@ggui-ai/live-channel": "0.4.0-rc.0",
39
+ "@ggui-ai/preview-a2ui": "0.4.0-rc.0",
40
+ "@ggui-ai/design": "0.4.0-rc.0",
41
+ "@ggui-ai/gadgets": "0.4.0-rc.0",
42
+ "@ggui-ai/protocol": "0.4.0-rc.0",
43
+ "@ggui-ai/wire": "0.4.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",
@@ -54,7 +55,7 @@
54
55
  "jsdom": "^24.0.0",
55
56
  "tsx": "^4.21.0",
56
57
  "typescript": "^5.0.0",
57
- "vitest": "^3.0.0"
58
+ "vitest": "^3.2.6"
58
59
  },
59
60
  "repository": {
60
61
  "type": "git",
@@ -73,8 +74,9 @@
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",
79
+ "check:bundle-size:self-test": "tsx scripts/check-bundle-size.ts --self-test",
78
80
  "typecheck": "tsc --noEmit",
79
81
  "test": "vitest run",
80
82
  "test:watch": "vitest"
@@ -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
- }