@ggui-ai/iframe-runtime 0.20.0 → 0.22.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/index.d.ts CHANGED
@@ -37,7 +37,7 @@ export { parseMetaFromGlobal, parseMetaFromToolResult, validateMeta, } from './m
37
37
  export type { RendererBootFailureReason, RendererBootFailedMessage, } from './runtime.js';
38
38
  export type { ProtocolError, ProtocolErrorEmitter, BootstrapFailureReason, } from './protocol-error.js';
39
39
  export { defaultProtocolErrorEmitter, fromBootstrapFailure, fromClientContractViolation, fromTransportFailure, fromAuthFailure, fromUpgradeRequired, fromUnknown, } from './protocol-error.js';
40
- export type { ObservabilityEvent, ObservabilityEmitter, ObservabilityMessage, SchemaVersionMismatchEvent, FontFaceBlockedEvent, AppThemeInvalidEvent, AppThemeMemberStrippedEvent, ThemeReinjectFailedEvent, SubscribeFailedEvent, ChannelTransportPickedEvent, ChannelTransportFallbackEvent, ChannelTransportResubscribedEvent, ChannelPollDegradedEvent, ChannelPollRecoveredEvent, UiFeedbackEvent, RelayDeadTapEvent, RelayIncapabilityEvent, RelayLatchTrigger, UnknownObservabilityEvent, } from './observability.js';
40
+ export type { ObservabilityEvent, ObservabilityEmitter, ObservabilityMessage, SchemaVersionMismatchEvent, FontFaceBlockedEvent, AppThemeInvalidEvent, AppThemeMemberStrippedEvent, ActionSpecInvalidEvent, ActionSpecMemberStrippedEvent, ThemeReinjectFailedEvent, SubscribeFailedEvent, ChannelTransportPickedEvent, ChannelTransportFallbackEvent, ChannelTransportResubscribedEvent, ChannelPollDegradedEvent, ChannelPollRecoveredEvent, UiFeedbackEvent, RelayDeadTapEvent, RelayIncapabilityEvent, RelayLatchTrigger, UnknownObservabilityEvent, } from './observability.js';
41
41
  export { isObservabilityMessage, isRelayDeadTapEvent, isRelayIncapabilityEvent, postObservabilityToParent, } from './observability.js';
42
42
  export type { LifecycleEmitter } from './lifecycle.js';
43
43
  export { postLifecycleToParent, makeLifecycleEvent } from './lifecycle.js';
@@ -1,4 +1,5 @@
1
1
  import type { McpAppAiGguiRenderMeta } from '@ggui-ai/protocol/integrations/mcp-apps';
2
+ import { type ParseMcpAppAiGguiRenderMetaOptions } from '@ggui-ai/protocol/integrations/mcp-apps';
2
3
  import type { McpAppAiGguiMetaParseResult } from './types.js';
3
4
  /**
4
5
  * Validate an {@link McpAppAiGguiRenderMeta} slice. Enforces mode
@@ -33,6 +34,29 @@ export declare function reportInvalidAppTheme(issues: readonly string[]): void;
33
34
  * on every `parseMcpAppAiGguiRenderMeta` call in this package.
34
35
  */
35
36
  export declare function reportStrippedThemeMembers(keys: readonly string[]): void;
37
+ /**
38
+ * The wire parser refused the slice's `actionSpec` (ggui#1178): report it
39
+ * to the embedding host as `action-spec-invalid` and mount without the
40
+ * action contract. Passed as `onInvalidActionSpec` on every
41
+ * `parseMcpAppAiGguiRenderMeta` call in this package.
42
+ */
43
+ export declare function reportInvalidActionSpec(issues: readonly string[]): void;
44
+ /**
45
+ * The read door KEPT the slice's `actionSpec` but stripped entry members
46
+ * this release does not name (ggui#1178, VERSION-POLICY §3.6): report the
47
+ * `<action>.<member>` paths so a newer writer's member is visible, not
48
+ * swallowed. Passed as `onStrippedActionSpecMembers` on every
49
+ * `parseMcpAppAiGguiRenderMeta` call in this package.
50
+ */
51
+ export declare function reportStrippedActionSpecMembers(keys: readonly string[]): void;
52
+ /**
53
+ * Every degradation the wire parser's read door can report, bound to its
54
+ * observability event — the ONE options object every
55
+ * `parseMcpAppAiGguiRenderMeta` call in this package passes, so a door
56
+ * that learns a new report is wired everywhere at once rather than at
57
+ * whichever call site someone remembered.
58
+ */
59
+ export declare const READ_DOOR_REPORTERS: ParseMcpAppAiGguiRenderMetaOptions;
36
60
  /**
37
61
  * Parse the render slice from `globalThis.__GGUI_META__` — the
38
62
  * synchronous self-contained shell delivery channel.
@@ -51,7 +51,7 @@
51
51
  * is structurally rejected by the combiner.
52
52
  */
53
53
  import { PUBLIC_ENV_APP_KEY_RE } from '@ggui-ai/protocol/wire';
54
- import { parseMcpAppAiGguiRenderMeta } from '@ggui-ai/protocol/integrations/mcp-apps';
54
+ import { parseMcpAppAiGguiRenderMeta, } from '@ggui-ai/protocol/integrations/mcp-apps';
55
55
  import { postObservabilityToParent } from './observability.js';
56
56
  /**
57
57
  * Type guard — true iff `value` is a non-null, non-array plain object.
@@ -251,6 +251,10 @@ function projectMeta(meta, hasStaticContent) {
251
251
  // Per-app theme overlay carried through verbatim. Already validated
252
252
  // via `appThemeSchema` by the wire parser, so no re-validation here.
253
253
  ...(meta.theme !== undefined ? { theme: meta.theme } : {}),
254
+ // The slice's action contract (ggui#1178), carried through whole.
255
+ // Already validated at the wire parser's tolerant read door, so no
256
+ // re-validation here.
257
+ ...(meta.actionSpec !== undefined ? { actionSpec: meta.actionSpec } : {}),
254
258
  ...(gadgets !== undefined ? { gadgets } : {}),
255
259
  ...(publicEnv !== undefined ? { publicEnv } : {}),
256
260
  ...(streamWebSocketLocalTools !== undefined
@@ -345,6 +349,38 @@ export function reportInvalidAppTheme(issues) {
345
349
  export function reportStrippedThemeMembers(keys) {
346
350
  postObservabilityToParent({ kind: 'app-theme-member-stripped', keys: [...keys] });
347
351
  }
352
+ /**
353
+ * The wire parser refused the slice's `actionSpec` (ggui#1178): report it
354
+ * to the embedding host as `action-spec-invalid` and mount without the
355
+ * action contract. Passed as `onInvalidActionSpec` on every
356
+ * `parseMcpAppAiGguiRenderMeta` call in this package.
357
+ */
358
+ export function reportInvalidActionSpec(issues) {
359
+ postObservabilityToParent({ kind: 'action-spec-invalid', issues: [...issues] });
360
+ }
361
+ /**
362
+ * The read door KEPT the slice's `actionSpec` but stripped entry members
363
+ * this release does not name (ggui#1178, VERSION-POLICY §3.6): report the
364
+ * `<action>.<member>` paths so a newer writer's member is visible, not
365
+ * swallowed. Passed as `onStrippedActionSpecMembers` on every
366
+ * `parseMcpAppAiGguiRenderMeta` call in this package.
367
+ */
368
+ export function reportStrippedActionSpecMembers(keys) {
369
+ postObservabilityToParent({ kind: 'action-spec-member-stripped', keys: [...keys] });
370
+ }
371
+ /**
372
+ * Every degradation the wire parser's read door can report, bound to its
373
+ * observability event — the ONE options object every
374
+ * `parseMcpAppAiGguiRenderMeta` call in this package passes, so a door
375
+ * that learns a new report is wired everywhere at once rather than at
376
+ * whichever call site someone remembered.
377
+ */
378
+ export const READ_DOOR_REPORTERS = {
379
+ onInvalidTheme: reportInvalidAppTheme,
380
+ onStrippedThemeMembers: reportStrippedThemeMembers,
381
+ onInvalidActionSpec: reportInvalidActionSpec,
382
+ onStrippedActionSpecMembers: reportStrippedActionSpecMembers,
383
+ };
348
384
  /**
349
385
  * Parse the render slice from `globalThis.__GGUI_META__` — the
350
386
  * synchronous self-contained shell delivery channel.
@@ -367,10 +403,7 @@ export function parseMetaFromGlobal() {
367
403
  if (!isPlainObject(raw)) {
368
404
  return { ok: false, reason: 'MALFORMED_BOOTSTRAP' };
369
405
  }
370
- const parsed = parseMcpAppAiGguiRenderMeta(raw, {
371
- onInvalidTheme: reportInvalidAppTheme,
372
- onStrippedThemeMembers: reportStrippedThemeMembers,
373
- });
406
+ const parsed = parseMcpAppAiGguiRenderMeta(raw, READ_DOOR_REPORTERS);
374
407
  if (!parsed.ok) {
375
408
  return { ok: false, reason: 'MALFORMED_BOOTSTRAP' };
376
409
  }
@@ -421,10 +454,7 @@ export function parseMetaFromToolResult(params) {
421
454
  return { ok: false, reason: 'MISSING_META_GGUI_BOOTSTRAP' };
422
455
  }
423
456
  const meta = topMeta;
424
- const parsed = parseMcpAppAiGguiRenderMeta(meta, {
425
- onInvalidTheme: reportInvalidAppTheme,
426
- onStrippedThemeMembers: reportStrippedThemeMembers,
427
- });
457
+ const parsed = parseMcpAppAiGguiRenderMeta(meta, READ_DOOR_REPORTERS);
428
458
  if (!parsed.ok) {
429
459
  return { ok: false, reason: 'MALFORMED_BOOTSTRAP' };
430
460
  }
@@ -33,7 +33,7 @@ import { MCP_APP_OBSERVE_TYPE } from '@ggui-ai/protocol/integrations/mcp-apps';
33
33
  *
34
34
  * @public
35
35
  */
36
- export type ObservabilityEvent = SchemaVersionMismatchEvent | FontFaceBlockedEvent | AppThemeInvalidEvent | AppThemeMemberStrippedEvent | ThemeReinjectFailedEvent | SubscribeFailedEvent | ChannelTransportPickedEvent | ChannelTransportFallbackEvent | ChannelTransportResubscribedEvent | ChannelPollDegradedEvent | ChannelPollRecoveredEvent | UiFeedbackEvent | RelayIncapabilityEvent | RelayDeadTapEvent | ComponentEmptyEvent | UnknownObservabilityEvent;
36
+ export type ObservabilityEvent = SchemaVersionMismatchEvent | FontFaceBlockedEvent | AppThemeInvalidEvent | AppThemeMemberStrippedEvent | ActionSpecInvalidEvent | ActionSpecMemberStrippedEvent | OneShotUnenforceableEvent | ThemeReinjectFailedEvent | SubscribeFailedEvent | ChannelTransportPickedEvent | ChannelTransportFallbackEvent | ChannelTransportResubscribedEvent | ChannelPollDegradedEvent | ChannelPollRecoveredEvent | UiFeedbackEvent | RelayIncapabilityEvent | RelayDeadTapEvent | ComponentEmptyEvent | UnknownObservabilityEvent;
37
37
  /**
38
38
  * Fired when the protocol-version handshake rejects the connection.
39
39
  * Parallel to `UpgradeRequiredError` — the host can choose to render
@@ -364,6 +364,44 @@ export interface AppThemeMemberStrippedEvent {
364
364
  readonly kind: 'app-theme-member-stripped';
365
365
  readonly keys: readonly string[];
366
366
  }
367
+ /**
368
+ * The render slice carried an `actionSpec` the wire schema refused
369
+ * (ggui#1178): the parser dropped it and the card mounted without the
370
+ * action contract. `issues` are the schema's own messages — the
371
+ * operator's pointer to the writer that projected a malformed contract.
372
+ *
373
+ * @public
374
+ */
375
+ export interface ActionSpecInvalidEvent {
376
+ readonly kind: 'action-spec-invalid';
377
+ readonly issues: readonly string[];
378
+ }
379
+ /**
380
+ * The render slice's `actionSpec` carried entry members THIS release does
381
+ * not name (ggui#1178, VERSION-POLICY §3.6): the read door kept every
382
+ * action and stripped those members, and the card says which. `keys` are
383
+ * `<action>.<member>` paths only, never their content.
384
+ *
385
+ * @public
386
+ */
387
+ export interface ActionSpecMemberStrippedEvent {
388
+ readonly kind: 'action-spec-member-stripped';
389
+ readonly keys: readonly string[];
390
+ }
391
+ /**
392
+ * A component render dispatched with no action contract on either carrier
393
+ * (ggui#1178, SPEC §5.5.1): the one-shot guard cannot know whether the
394
+ * action is `oneShot`, so the dispatch proceeds unguarded. Posted once per
395
+ * render, on its first such dispatch. `actionName` is the action that
396
+ * dispatched, never its payload.
397
+ *
398
+ * @public
399
+ */
400
+ export interface OneShotUnenforceableEvent {
401
+ readonly kind: 'one-shot-unenforceable';
402
+ readonly renderId: string;
403
+ readonly actionName: string;
404
+ }
367
405
  /**
368
406
  * The live re-injection on a host theme flip (ggui#987 §4) failed: the
369
407
  * renderer's update rejected, so the card kept its previous overlay /
package/dist/runtime.d.ts CHANGED
@@ -524,6 +524,11 @@ export declare function readSelfContainedMeta(): McpAppAiGguiRenderMeta | null;
524
524
  * surface a typed boot failure. The four server-assigned ledger fields
525
525
  * are intentionally absent — the first ack reconciles the seed to a
526
526
  * full `GguiSession` (no fabrication).
527
+ *
528
+ * A component seed carries the slice's `actionSpec` when the slice has one
529
+ * (ggui#1178): a mount with no live session frame paints from this seed, and
530
+ * the one-shot guard reads the current render's spec, so a seed without it
531
+ * could not enforce `oneShot`. A system seed never carries one.
527
532
  */
528
533
  export declare function buildGguiSessionSeedInput(meta: McpAppAiGguiRenderMeta): Promise<GguiSessionSeedInput | null>;
529
534
  /**
package/dist/runtime.js CHANGED
@@ -30,7 +30,7 @@ import './zod-jitless.js';
30
30
  import { runtimeConnectionWriter } from './connection.js';
31
31
  import { MCP_APP_BOOTSTRAP_FAILED_TYPE, MCP_APP_RENDERER_READY_TYPE, parseMcpAppAiGguiRenderMeta, readGguiShellEnvelope, } from '@ggui-ai/protocol/integrations/mcp-apps';
32
32
  import { effectiveThemeId, effectiveThemeMode, } from '@ggui-ai/protocol/integrations/theme-binding';
33
- import { extractLocatorFromToolResult, parseMetaFromGlobal, parseMetaFromToolResult, validateMeta, reportInvalidAppTheme, reportStrippedThemeMembers, } from './meta-parse.js';
33
+ import { extractLocatorFromToolResult, parseMetaFromGlobal, parseMetaFromToolResult, validateMeta, READ_DOOR_REPORTERS, } from './meta-parse.js';
34
34
  import { projectHostContext } from '@ggui-ai/protocol/wire';
35
35
  import { installDismissIntentListener } from './dismiss-intent.js';
36
36
  import { App, PostMessageTransport } from '@modelcontextprotocol/ext-apps';
@@ -1175,9 +1175,15 @@ function parseSeedProps(propsJson) {
1175
1175
  * surface a typed boot failure. The four server-assigned ledger fields
1176
1176
  * are intentionally absent — the first ack reconciles the seed to a
1177
1177
  * full `GguiSession` (no fabrication).
1178
+ *
1179
+ * A component seed carries the slice's `actionSpec` when the slice has one
1180
+ * (ggui#1178): a mount with no live session frame paints from this seed, and
1181
+ * the one-shot guard reads the current render's spec, so a seed without it
1182
+ * could not enforce `oneShot`. A system seed never carries one.
1178
1183
  */
1179
1184
  export async function buildGguiSessionSeedInput(meta) {
1180
1185
  const props = parseSeedProps(meta.propsJson);
1186
+ const actionSpec = meta.actionSpec !== undefined ? { actionSpec: meta.actionSpec } : {};
1181
1187
  // System-card mode — `kind` keyed against the built-in registry.
1182
1188
  if (meta.kind !== undefined) {
1183
1189
  return {
@@ -1195,6 +1201,7 @@ export async function buildGguiSessionSeedInput(meta) {
1195
1201
  appId: meta.appId,
1196
1202
  componentCode: decodeCodeB64(meta.codeB64),
1197
1203
  ...(props !== undefined ? { props } : {}),
1204
+ ...actionSpec,
1198
1205
  };
1199
1206
  }
1200
1207
  // Compiled-component mode — fetch the content-addressable bytes.
@@ -1210,6 +1217,7 @@ export async function buildGguiSessionSeedInput(meta) {
1210
1217
  appId: meta.appId,
1211
1218
  componentCode,
1212
1219
  ...(props !== undefined ? { props } : {}),
1220
+ ...actionSpec,
1213
1221
  };
1214
1222
  }
1215
1223
  /**
@@ -1398,10 +1406,7 @@ export async function resolveMetaViaReadDoor(app, locator) {
1398
1406
  const envelope = readGguiShellEnvelope(text);
1399
1407
  if (envelope === undefined)
1400
1408
  return null;
1401
- const parsed = parseMcpAppAiGguiRenderMeta(envelope, {
1402
- onInvalidTheme: reportInvalidAppTheme,
1403
- onStrippedThemeMembers: reportStrippedThemeMembers,
1404
- });
1409
+ const parsed = parseMcpAppAiGguiRenderMeta(envelope, READ_DOOR_REPORTERS);
1405
1410
  if (!parsed.ok || parsed.meta === undefined)
1406
1411
  return null;
1407
1412
  const validated = validateMeta(parsed.meta);
@@ -1 +1 @@
1
- const M = (globalThis.__ggui__&&globalThis.__ggui__["tokens"]||window["__GGUI_TOKENS"]||{}); export const animation = M["animation"]; export const duration = M["duration"]; export const easing = M["easing"]; export const transition = M["transition"]; export const keyframes = M["keyframes"]; export const thinkingAnimation = M["thinkingAnimation"]; export const thinkingKeyframes = M["thinkingKeyframes"]; export const thinkingPresets = M["thinkingPresets"]; export const THINKING_DEFAULT_STYLE = M["THINKING_DEFAULT_STYLE"]; export const motionSafe = M["motionSafe"]; export const reducedMotion = M["reducedMotion"]; export const reducedMotionCSS = M["reducedMotionCSS"]; export const colors = M["colors"]; export const gray = M["gray"]; export const primary = M["primary"]; export const semantic = M["semantic"]; export const chartColors = M["chartColors"]; export const success = M["success"]; export const warning = M["warning"]; export const error = M["error"]; export const info = M["info"]; export const highContrast = M["highContrast"]; export const typography = M["typography"]; export const fontFamily = M["fontFamily"]; export const fontSize = M["fontSize"]; export const fontSizeValues = M["fontSizeValues"]; export const fontWeight = M["fontWeight"]; export const lineHeight = M["lineHeight"]; export const letterSpacing = M["letterSpacing"]; export const headingStyles = M["headingStyles"]; export const spacing = M["spacing"]; export const spacingValues = M["spacingValues"]; export const radius = M["radius"]; export const shadow = M["shadow"]; export const elevation = M["elevation"]; export const maxWidth = M["maxWidth"]; export const zIndex = M["zIndex"]; export const accessibility = M["accessibility"]; export const focusRing = M["focusRing"]; export const nativeTokens = M["nativeTokens"]; export const tokens = M["tokens"]; export default M;
1
+ const M = (globalThis.__ggui__&&globalThis.__ggui__["tokens"]||window["__GGUI_TOKENS"]||{}); export const animation = M["animation"]; export const duration = M["duration"]; export const easing = M["easing"]; export const transition = M["transition"]; export const motionVar = M["motionVar"]; export const keyframes = M["keyframes"]; export const thinkingAnimation = M["thinkingAnimation"]; export const thinkingKeyframes = M["thinkingKeyframes"]; export const thinkingPresets = M["thinkingPresets"]; export const THINKING_DEFAULT_STYLE = M["THINKING_DEFAULT_STYLE"]; export const motionSafe = M["motionSafe"]; export const reducedMotion = M["reducedMotion"]; export const reducedMotionCSS = M["reducedMotionCSS"]; export const colors = M["colors"]; export const gray = M["gray"]; export const primary = M["primary"]; export const semantic = M["semantic"]; export const chartColors = M["chartColors"]; export const success = M["success"]; export const warning = M["warning"]; export const error = M["error"]; export const info = M["info"]; export const highContrast = M["highContrast"]; export const typography = M["typography"]; export const fontFamily = M["fontFamily"]; export const fontSize = M["fontSize"]; export const fontSizeValues = M["fontSizeValues"]; export const fontWeight = M["fontWeight"]; export const lineHeight = M["lineHeight"]; export const letterSpacing = M["letterSpacing"]; export const headingStyles = M["headingStyles"]; export const spacing = M["spacing"]; export const spacingValues = M["spacingValues"]; export const radius = M["radius"]; export const shadow = M["shadow"]; export const elevation = M["elevation"]; export const maxWidth = M["maxWidth"]; export const zIndex = M["zIndex"]; export const accessibility = M["accessibility"]; export const focusRing = M["focusRing"]; export const nativeTokens = M["nativeTokens"]; export const tokens = M["tokens"]; export default M;
package/dist/types.d.ts CHANGED
@@ -37,9 +37,10 @@ type GguiSessionLedgerFields = 'eventSequence' | 'createdAt' | 'lastActivityAt'
37
37
  * the seed with the authoritative `GguiSession`.
38
38
  *
39
39
  * Carries the SAME contract-spec fields a full `ComponentGguiSession` does
40
- * (`propsSpec` / `streamSpec` / `actionSpec`), as `undefined` on a
41
- * seed — so the channel handlers that read those after the
42
- * `type !== 'mcpApps' && type !== 'system'` narrowing stay type-clean.
40
+ * (`propsSpec` / `streamSpec` / `actionSpec`), so the channel handlers that
41
+ * read those after the `type !== 'mcpApps' && type !== 'system'` narrowing
42
+ * stay type-clean. On a seed, `propsSpec` and `streamSpec` are `undefined`;
43
+ * `actionSpec` is the slice's own when the slice carried one (ggui#1178).
43
44
  */
44
45
  export type GguiSessionSeedInput = Omit<ComponentGguiSession, GguiSessionLedgerFields> | Omit<SystemGguiSession, GguiSessionLedgerFields>;
45
46
  /**
@@ -1,6 +1,7 @@
1
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
+ import { postObservabilityToParent } from './observability.js';
4
5
  // `StreamBus` (and its bounded reserved-channel replay ring) lives in
5
6
  // `@ggui-ai/wire` (one implementation for every first-party renderer).
6
7
  // It was originally hoisted to share with `<GguiRender>` — that
@@ -45,6 +46,27 @@ export function buildRootWireConfig(opts) {
45
46
  }
46
47
  emitProtocolError(fromClientContractViolation(err));
47
48
  }
49
+ // ggui#1178 — a card paints and takes gestures even when no action spec
50
+ // reached it (a mount past the WS token's TTL, a host with no live channel,
51
+ // an older server that never put the spec on the render slice). The one-shot
52
+ // guard then cannot know an action is `oneShot` and fails open. The runtime's
53
+ // obligation is to NAME that, never silently: once per component render, as
54
+ // the `one-shot-unenforceable` observability event posted to the embedding
55
+ // host (the channel every other degradation in this runtime uses), and the
56
+ // dispatch proceeds. It is named on the first spec-less DISPATCH, not at
57
+ // paint: an absent spec also means "this card declares no actions", so a
58
+ // paint-time name would fire on every display-only card. System and mcpApps
59
+ // renders carry no spec by design and are never named.
60
+ let namedUnenforceableFor = null;
61
+ const nameUnenforceableOnce = (actionName) => {
62
+ const currentRender = opts.getCurrentGguiSession();
63
+ if (currentRender === null || currentRender.type === 'mcpApps' || currentRender.type === 'system')
64
+ return;
65
+ if (namedUnenforceableFor === currentRender.id)
66
+ return;
67
+ namedUnenforceableFor = currentRender.id;
68
+ postObservabilityToParent({ kind: 'one-shot-unenforceable', renderId: currentRender.id, actionName });
69
+ };
48
70
  return buildWireConfig({
49
71
  app: { appId: opts.appId, appName: opts.appId },
50
72
  // `isConnected` here is the static config field; `useRender()`
@@ -64,6 +86,7 @@ export function buildRootWireConfig(opts) {
64
86
  ? currentRender.actionSpec
65
87
  : undefined;
66
88
  },
89
+ onActionSpecAbsent: nameUnenforceableOnce,
67
90
  // The iframe's precompiled-validator variant — the dispatch never
68
91
  // trips the iframe's no-`unsafe-eval` CSP.
69
92
  validateEnvelope: validateOutboundActionEnvelope,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ggui-ai/iframe-runtime",
3
- "version": "0.20.0",
3
+ "version": "0.22.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",
@@ -41,12 +41,12 @@
41
41
  "react": "^19.0.0",
42
42
  "react-dom": "^19.0.0",
43
43
  "zod": "^4.3.6",
44
- "@ggui-ai/design": "0.20.0",
45
- "@ggui-ai/gadgets": "0.20.0",
46
- "@ggui-ai/live-channel": "0.20.0",
47
- "@ggui-ai/preview-a2ui": "0.20.0",
48
- "@ggui-ai/protocol": "0.20.0",
49
- "@ggui-ai/wire": "0.20.0"
44
+ "@ggui-ai/design": "0.22.0",
45
+ "@ggui-ai/live-channel": "0.22.0",
46
+ "@ggui-ai/preview-a2ui": "0.22.0",
47
+ "@ggui-ai/gadgets": "0.22.0",
48
+ "@ggui-ai/protocol": "0.22.0",
49
+ "@ggui-ai/wire": "0.22.0"
50
50
  },
51
51
  "peerDependencies": {
52
52
  "@modelcontextprotocol/sdk": "^1.27.0"