@ggui-ai/iframe-runtime 0.6.3 → 0.8.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, SubscribeFailedEvent, AuthRequiredEvent, ChannelTransportPickedEvent, ChannelTransportFallbackEvent, ChannelTransportResubscribedEvent, UiFeedbackEvent, UnknownObservabilityEvent, } from './observability.js';
40
+ export type { ObservabilityEvent, ObservabilityEmitter, ObservabilityMessage, SchemaVersionMismatchEvent, SubscribeFailedEvent, ChannelTransportPickedEvent, ChannelTransportFallbackEvent, ChannelTransportResubscribedEvent, ChannelPollDegradedEvent, ChannelPollRecoveredEvent, UiFeedbackEvent, RelayIncapabilityEvent, UnknownObservabilityEvent, } from './observability.js';
41
41
  export { postObservabilityToParent } from './observability.js';
42
42
  export type { LifecycleEmitter } from './lifecycle.js';
43
43
  export { postLifecycleToParent, makeLifecycleEvent } from './lifecycle.js';
@@ -116,7 +116,14 @@ export function mountMcpAppIframe(container, opts) {
116
116
  const result = {
117
117
  protocolVersion: requested,
118
118
  hostInfo: { name: 'ggui-iframe-runtime-embed-host', version: EMBED_HOST_VERSION },
119
- hostCapabilities: {},
119
+ // Advertise exactly what the `switch` below implements
120
+ // (ggui#440). `serverTools`: the `tools/call` case proxies
121
+ // guest tool calls to the MCP server. NOT `message`: there is
122
+ // no `ui/message` case here, so a doorbell would be dropped —
123
+ // and the runtime now surfaces that to the user rather than
124
+ // claiming the gesture reached the agent. If a `ui/message`
125
+ // case is ever added, advertise `message` in the same change.
126
+ hostCapabilities: { serverTools: {} },
120
127
  hostContext: {
121
128
  locale: opts.locale ??
122
129
  (typeof navigator !== 'undefined' ? navigator.language : 'en-US'),
@@ -11,9 +11,9 @@ import type { McpAppAiGguiMetaParseResult } from './types.js';
11
11
  * direct-call callers like `parseMetaFromGlobal` / tests; the
12
12
  * envelope-driven extractors already enforce this in the protocol-
13
13
  * level parser).
14
- * - At least one of `{live mode (wsUrl+wsToken), codeUrl, kind}` MUST
15
- * be present. Without any mode discriminator the iframe has nothing
16
- * to mount.
14
+ * - At least one of `{live mode (wsUrl+wsToken), codeUrl, codeB64,
15
+ * kind}` MUST be present. Without any mode discriminator the iframe
16
+ * has nothing to mount.
17
17
  */
18
18
  export declare function validateMeta(meta: McpAppAiGguiRenderMeta): McpAppAiGguiMetaParseResult;
19
19
  /**
@@ -40,9 +40,10 @@
40
40
  *
41
41
  * - **live** — slice has `wsUrl` + `wsToken`; runtime opens
42
42
  * live-channel WS and renders agent-driven frames.
43
- * - **static-component** — slice has `codeUrl`; runtime fetches
44
- * the content-addressable URL, dynamic-imports the React
45
- * component, mounts it, never opens WS.
43
+ * - **static-component** — slice has `codeUrl` (runtime fetches the
44
+ * content-addressable URL) and/or `codeB64` (runtime decodes the
45
+ * inline bytes, no fetch); mounts the React component, never
46
+ * opens WS.
46
47
  * - **system-card** — slice has `kind`; runtime maps to a
47
48
  * built-in component via the system-card registry.
48
49
  *
@@ -177,9 +178,9 @@ function projectPublicEnv(raw) {
177
178
  *
178
179
  * Invariants enforced here that the combiner doesn't:
179
180
  * - `expiresAt` (when present) MUST parse to a valid timestamp. If
180
- * in the past AND the slice carries static content (codeUrl / kind),
181
- * DEGRADE to static-only (drop `wsUrl` / `wsToken` / `expiresAt`);
182
- * otherwise return EXPIRED_BOOTSTRAP.
181
+ * in the past AND the slice carries static content (codeUrl /
182
+ * codeB64 / kind), DEGRADE to static-only (drop `wsUrl` /
183
+ * `wsToken` / `expiresAt`); otherwise return EXPIRED_BOOTSTRAP.
183
184
  * - `gadgets` / `publicEnv` / `permissionsPolicy` /
184
185
  * `streamWebSocketLocalTools` / `contextSlots` get defensive
185
186
  * entry-level validation; malformed → defaulted.
@@ -260,6 +261,7 @@ function projectMeta(meta, hasStaticContent) {
260
261
  : {}),
261
262
  ...(meta.codeUrl !== undefined ? { codeUrl: meta.codeUrl } : {}),
262
263
  ...(meta.codeHash !== undefined ? { codeHash: meta.codeHash } : {}),
264
+ ...(meta.codeB64 !== undefined ? { codeB64: meta.codeB64 } : {}),
263
265
  ...(meta.kind !== undefined ? { kind: meta.kind } : {}),
264
266
  };
265
267
  return { ok: true, meta: projected };
@@ -275,9 +277,9 @@ function projectMeta(meta, hasStaticContent) {
275
277
  * direct-call callers like `parseMetaFromGlobal` / tests; the
276
278
  * envelope-driven extractors already enforce this in the protocol-
277
279
  * level parser).
278
- * - At least one of `{live mode (wsUrl+wsToken), codeUrl, kind}` MUST
279
- * be present. Without any mode discriminator the iframe has nothing
280
- * to mount.
280
+ * - At least one of `{live mode (wsUrl+wsToken), codeUrl, codeB64,
281
+ * kind}` MUST be present. Without any mode discriminator the iframe
282
+ * has nothing to mount.
281
283
  */
282
284
  export function validateMeta(meta) {
283
285
  // `runtimeUrl` is a hard wire-required field on every render slice
@@ -292,8 +294,9 @@ export function validateMeta(meta) {
292
294
  }
293
295
  const hasLive = isNonEmptyString(meta.wsUrl) && isNonEmptyString(meta.wsToken);
294
296
  const hasCodeUrl = isNonEmptyString(meta.codeUrl);
297
+ const hasCodeB64 = isNonEmptyString(meta.codeB64);
295
298
  const hasKind = isNonEmptyString(meta.kind);
296
- const hasStaticContent = hasCodeUrl || hasKind;
299
+ const hasStaticContent = hasCodeUrl || hasCodeB64 || hasKind;
297
300
  if (!hasLive && !hasStaticContent) {
298
301
  return { ok: false, reason: 'MALFORMED_BOOTSTRAP' };
299
302
  }
@@ -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 | SubscribeFailedEvent | AuthRequiredEvent | ChannelTransportPickedEvent | ChannelTransportFallbackEvent | ChannelTransportResubscribedEvent | UiFeedbackEvent | UnknownObservabilityEvent;
36
+ export type ObservabilityEvent = SchemaVersionMismatchEvent | SubscribeFailedEvent | ChannelTransportPickedEvent | ChannelTransportFallbackEvent | ChannelTransportResubscribedEvent | ChannelPollDegradedEvent | ChannelPollRecoveredEvent | UiFeedbackEvent | RelayIncapabilityEvent | 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
@@ -61,57 +61,6 @@ export interface SubscribeFailedEvent {
61
61
  readonly reason: string;
62
62
  readonly message?: string;
63
63
  }
64
- /**
65
- * Fired when the server emits a `system` frame with
66
- * `action: 'auth_required'` — the agent needs the end-user to authorize
67
- * an OAuth service (Google, Slack, etc.) before its tool call can
68
- * proceed. The host listens via `<McpAppIframe onObserve>` and renders
69
- * a consent overlay OUTSIDE the iframe, then redirects the user to
70
- * `authUrl` to complete the OAuth flow.
71
- *
72
- * Observability-only — the wire signal is already fully typed on
73
- * `@ggui-ai/protocol`'s `SystemPayload` (live-channel.ts). This event is a
74
- * renderer↔host projection of that payload, not a new protocol surface,
75
- * matching the vendor-neutral-separation posture of every
76
- * other `ObservabilityEvent` kind.
77
- *
78
- * Fields map 1:1 from `SystemPayload`:
79
- * - `provider` ← `serviceId` (canonical identifier the credential
80
- * store keys off — e.g., `"google"`, `"slack"`).
81
- * - `authUrl` ← `consentUrl` (URL the host opens in a popup / new tab
82
- * to initiate the OAuth consent flow).
83
- * - `displayName` / `scopes` / `message` are optional hints the host
84
- * can surface in the consent overlay; absent means "render with
85
- * generic copy".
86
- *
87
- * @public
88
- */
89
- export interface AuthRequiredEvent {
90
- readonly kind: 'auth-required';
91
- /**
92
- * Canonical identifier of the service needing authorization (e.g.
93
- * `"google"`, `"slack"`). Maps to `SystemPayload.serviceId`.
94
- */
95
- readonly provider: string;
96
- /**
97
- * URL the host opens in a popup / new tab to initiate the OAuth
98
- * consent flow. Maps to `SystemPayload.consentUrl`.
99
- */
100
- readonly authUrl: string;
101
- /**
102
- * Human-readable service name (e.g. `"Google"`, `"Slack"`). Optional
103
- * — absent when the server doesn't supply one.
104
- */
105
- readonly displayName?: string;
106
- /**
107
- * OAuth scopes the agent is requesting. Optional.
108
- */
109
- readonly scopes?: readonly string[];
110
- /**
111
- * Human-readable message explaining why access is needed. Optional.
112
- */
113
- readonly message?: string;
114
- }
115
64
  /**
116
65
  * Fired by the channel-transport router when it picks a transport
117
66
  * for a `streamSpec[ch].source.tool` channel. Hosts can inspect the
@@ -152,6 +101,50 @@ export interface ChannelTransportResubscribedEvent {
152
101
  readonly sessionId: string;
153
102
  readonly channelName: string;
154
103
  }
104
+ /**
105
+ * Fired by the channel-transport router when a polling channel's
106
+ * `tools/call` fails for a reason the router classifies as STRUCTURAL
107
+ * — the failure will repeat identically until the underlying
108
+ * condition changes, so spending a full-cadence poll on it is waste.
109
+ *
110
+ * The channel drops to a slower probe cadence; it never stops. That
111
+ * distinction is the fail-safe: recovery is a property of the running
112
+ * loop, not of an external nudge, so a host that regains the missing
113
+ * capability sees its channels come back on their own.
114
+ *
115
+ * Emitted ONCE per healthy→degraded transition, never per probe tick
116
+ * — a channel degraded for an hour emits one event, not 120.
117
+ *
118
+ * @public
119
+ */
120
+ export interface ChannelPollDegradedEvent {
121
+ readonly kind: 'channel-poll-degraded';
122
+ readonly sessionId: string;
123
+ readonly channelName: string;
124
+ /**
125
+ * What the router classified. `'relay-incapable'` = the runtime has
126
+ * confirmed this host cannot relay `tools/call` to the MCP server
127
+ * (see `RelayIncapableError`), so every poll fails before reaching
128
+ * the transport.
129
+ */
130
+ readonly reason: 'relay-incapable';
131
+ /** Cadence (ms) the channel probes at while degraded. */
132
+ readonly probeIntervalMs: number;
133
+ }
134
+ /**
135
+ * Fired by the channel-transport router when a previously-degraded
136
+ * channel's `tools/call` succeeds again. The channel returns to its
137
+ * normal cadence. Emitted once per degraded→healthy transition.
138
+ *
139
+ * @public
140
+ */
141
+ export interface ChannelPollRecoveredEvent {
142
+ readonly kind: 'channel-poll-recovered';
143
+ readonly sessionId: string;
144
+ readonly channelName: string;
145
+ /** Cadence (ms) the channel returns to. */
146
+ readonly pollIntervalMs: number;
147
+ }
155
148
  /**
156
149
  * Fired when the end user submits the runtime's in-iframe UI-feedback
157
150
  * affordance ("did this generated UI work for you?"). Field semantics
@@ -190,6 +183,37 @@ export interface UiFeedbackEvent {
190
183
  /** Tool that produced the render (e.g. `ggui_render`). */
191
184
  readonly toolName?: string;
192
185
  }
186
+ /**
187
+ * Fired at the two transition edges of the renderer's
188
+ * relay-incapability latch — the runtime's confirmed determination
189
+ * that the host cannot relay `tools/call` to the MCP server:
190
+ *
191
+ * - `'latched'` — a real user gesture just failed relay-shaped (no
192
+ * well-formed result envelope came back at all) on a host whose
193
+ * captured capability handshake never advertised `serverTools`.
194
+ * The runtime now treats relay as confirmed-unavailable: it shows
195
+ * one persistent explanation instead of a per-gesture error toast,
196
+ * and channel polls fail fast without a transport round-trip.
197
+ * - `'cleared'` — a later well-formed result envelope arrived
198
+ * (`ok:true` and `ok:false` alike — either proves the host relayed
199
+ * the call there and back), so the determination no longer holds;
200
+ * per-gesture feedback and channel transport attempts resume.
201
+ *
202
+ * Emitted once per edge, never per channel poll tick: repeated failing
203
+ * gestures while latched emit nothing further, and the router's
204
+ * fail-fast ticks emit nothing at all — the edges carry the full
205
+ * information.
206
+ *
207
+ * Always emitted via the postMessage-to-parent default — the emission
208
+ * sites live in module-level gesture-dispatch code outside the boot
209
+ * graph, so they never flow through an injected `onObserve` sink.
210
+ *
211
+ * @public
212
+ */
213
+ export interface RelayIncapabilityEvent {
214
+ readonly kind: 'relay-incapability';
215
+ readonly state: 'latched' | 'cleared';
216
+ }
193
217
  /**
194
218
  * Catch-all branch for event kinds the host's typings don't recognize.
195
219
  * Lets the union stay extensible without forcing protocol-version
@@ -32,7 +32,18 @@
32
32
  import React, { Component, createElement, Fragment, } from 'react';
33
33
  import { createRoot } from 'react-dom/client';
34
34
  import { stripMarkers, rewriteImports, getScopedThemeCss, getScopedCssTokens, getThemeCss, getCssTokens, } from '@ggui-ai/design/rendering';
35
- import { hoistImports, loadModule } from '@ggui-ai/design/module-loader';
35
+ import { hoistImports, loadModule, loadModuleInline, probeUrlModuleLoad, } from '@ggui-ai/design/module-loader';
36
+ /**
37
+ * Document-lifetime verdict: URL-scheme module loading (`blob:` main
38
+ * module + `data:` import shims) is blocked by the host CSP. Set only
39
+ * when the inline path succeeded where the URL path failed AND the
40
+ * known-good `probeUrlModuleLoad()` module ALSO fails — an arbitrary
41
+ * module's failure is not CSP evidence (an import-shim allowlist miss
42
+ * rejects blob-instantiation of that one module while URL loading is
43
+ * healthy, and latching on it would force every later evaluation in
44
+ * the document onto the inline path's narrower semantics).
45
+ */
46
+ let urlModuleLoadBlocked = false;
36
47
  // =============================================================================
37
48
  // Scope class helpers — port of RCR's `makeScopeClass` + counter.
38
49
  // =============================================================================
@@ -177,25 +188,66 @@ export async function mountReactRoot(container, opts) {
177
188
  }
178
189
  // 1. Strip metadata markers (__GGUI_META__ / __GGUI_STREAM_SPEC__).
179
190
  const cleaned = stripMarkers(resolved);
180
- // 2. Hoist imports above var declarations.
191
+ // 2. Hoist imports above var declarations. Load-bearing for BOTH
192
+ // execution paths: the inline transform places its bindings at
193
+ // the import statement's position, so a pre-import statement
194
+ // referencing an imported name needs the hoist exactly as the
195
+ // real module semantics do.
181
196
  const hoisted = hoistImports(cleaned);
182
- // 3. Rewrite bare specifiers to data-url shims reading from
183
- // `globalThis.__ggui__`. The __ggui__ registry is installed
184
- // by `runtime.ts::bootSequence` BEFORE any mountReactRoot call
185
- // — see globals.ts docstring on TOCTOU ordering.
186
- const rewritten = rewriteImports(hoisted, {
187
- mode: 'data-url',
188
- gadgetPackages: currentOpts.gadgetPackages,
189
- });
190
- // 4. Dynamically import the module.
191
- const mod = await loadModule(rewritten);
192
- // 5. Extract default export (or first function export).
193
- const Comp = (mod.default ??
194
- Object.values(mod).find((v) => typeof v === 'function'));
195
- if (Comp === undefined) {
196
- throw new Error('Module does not export a default component');
197
+ const pick = (mod) => {
198
+ const Comp = (mod.default ??
199
+ Object.values(mod).find((v) => typeof v === 'function'));
200
+ if (Comp === undefined) {
201
+ throw new Error('Module does not export a default component');
202
+ }
203
+ return Comp;
204
+ };
205
+ const evaluateInline = () => pick(loadModuleInline(hoisted, {
206
+ ...(currentOpts.gadgetPackages !== undefined
207
+ ? { gadgetPackages: currentOpts.gadgetPackages }
208
+ : {}),
209
+ }));
210
+ // Once URL-scheme module loading has been proven blocked in this
211
+ // document, go straight to inline execution — the CSP verdict
212
+ // cannot change within the document's lifetime.
213
+ if (urlModuleLoadBlocked) {
214
+ return evaluateInline();
215
+ }
216
+ try {
217
+ // 3. Rewrite bare specifiers to data-url shims reading from
218
+ // `globalThis.__ggui__`. The __ggui__ registry is installed
219
+ // by `runtime.ts::bootSequence` BEFORE any mountReactRoot call
220
+ // — see globals.ts docstring on TOCTOU ordering.
221
+ const rewritten = rewriteImports(hoisted, {
222
+ mode: 'data-url',
223
+ gadgetPackages: currentOpts.gadgetPackages,
224
+ });
225
+ // 4. Dynamically import the module.
226
+ const mod = await loadModule(rewritten);
227
+ // 5. Extract default export (or first function export).
228
+ return pick(mod);
229
+ }
230
+ catch (urlError) {
231
+ // A host CSP granting only 'unsafe-inline' rejects the blob:/
232
+ // data: imports above. Attempt the inline classic-script path;
233
+ // a buggy component fails on both paths, and the original error
234
+ // is the honest one to surface.
235
+ try {
236
+ const Comp = evaluateInline();
237
+ // Latch the inline path for the rest of the document ONLY on
238
+ // proven CSP evidence: the known-good probe module must also
239
+ // fail. A module-specific blob failure (import-shim allowlist
240
+ // drift) leaves the latch off so the next evaluation gets real
241
+ // module semantics again.
242
+ if (!(await probeUrlModuleLoad())) {
243
+ urlModuleLoadBlocked = true;
244
+ }
245
+ return Comp;
246
+ }
247
+ catch {
248
+ throw urlError;
249
+ }
197
250
  }
198
- return Comp;
199
251
  }
200
252
  function renderTree(opts) {
201
253
  const themeCss = opts.themeId
@@ -0,0 +1,64 @@
1
+ /**
2
+ * Relay-incapability failure vocabulary.
3
+ *
4
+ * Two modules need to agree on ONE fact: whether a failed
5
+ * `tools/call` failed because this host structurally cannot relay
6
+ * calls to the MCP server, or because something transient went wrong
7
+ * on an otherwise-working relay.
8
+ *
9
+ * - `runtime.ts` is the THROWER. It owns the relay-incapability
10
+ * latch (set only after a real user gesture failed relay-shaped
11
+ * on a host that never advertised `serverTools`) and throws
12
+ * {@link RelayIncapableError} from `channelToolsCall` before
13
+ * spending a transport round-trip.
14
+ * - `channel-transport.ts` is the CLASSIFIER. Its poll tick reads
15
+ * the thrown value to decide between "back off to a probe
16
+ * cadence" and "stay quiet, next tick may succeed".
17
+ *
18
+ * The vocabulary lives in its own module rather than in either party
19
+ * because `runtime.ts` already imports `channel-transport.ts` — a
20
+ * reverse import would close a cycle. Naming it for the CONCEPT
21
+ * (relay incapability) rather than for either party keeps the
22
+ * dependency direction obvious.
23
+ *
24
+ * Classification is by type, never by message string: a thrown
25
+ * `Error` whose text happens to mention relaying is NOT a structural
26
+ * failure, and re-wording a message must never silently re-classify
27
+ * a channel.
28
+ */
29
+ /**
30
+ * Canonical code carried by {@link RelayIncapableError}.
31
+ *
32
+ * Package-internal. Nothing in this module reaches a host: it is not
33
+ * re-exported from `index.ts`, so an embedding app can neither catch
34
+ * this class nor read this code. The host-facing surface for the same
35
+ * information is the `channel-poll-degraded` / `channel-poll-recovered`
36
+ * observability pair, which IS exported.
37
+ */
38
+ export declare const RELAY_INCAPABLE = "RELAY_INCAPABLE";
39
+ /**
40
+ * Thrown when the runtime has CONFIRMED (not merely suspected) that
41
+ * the surrounding host cannot relay `tools/call` to the MCP server,
42
+ * so an attempt would be spent for a guaranteed failure.
43
+ *
44
+ * "Confirmed" is load-bearing. Absence of an advertised `serverTools`
45
+ * capability is NOT confirmation — plenty of hosts under-advertise
46
+ * and relay perfectly well. Only a real gesture that failed
47
+ * relay-shaped sets the latch this error reports, and any later
48
+ * well-formed result envelope clears it.
49
+ *
50
+ * Consumers MUST classify by `instanceof` (or by `code`), never by
51
+ * parsing {@link Error.message}.
52
+ */
53
+ export declare class RelayIncapableError extends Error {
54
+ /** Canonical classification code. */
55
+ readonly code: typeof RELAY_INCAPABLE;
56
+ constructor();
57
+ }
58
+ /**
59
+ * Type guard for the structural-failure class. Thrower and classifier
60
+ * are bundled together into one iframe artifact, so `instanceof` is
61
+ * exact here — there is no module-realm boundary between them to
62
+ * defend against.
63
+ */
64
+ export declare function isRelayIncapableError(err: unknown): err is RelayIncapableError;
@@ -0,0 +1,69 @@
1
+ /**
2
+ * Relay-incapability failure vocabulary.
3
+ *
4
+ * Two modules need to agree on ONE fact: whether a failed
5
+ * `tools/call` failed because this host structurally cannot relay
6
+ * calls to the MCP server, or because something transient went wrong
7
+ * on an otherwise-working relay.
8
+ *
9
+ * - `runtime.ts` is the THROWER. It owns the relay-incapability
10
+ * latch (set only after a real user gesture failed relay-shaped
11
+ * on a host that never advertised `serverTools`) and throws
12
+ * {@link RelayIncapableError} from `channelToolsCall` before
13
+ * spending a transport round-trip.
14
+ * - `channel-transport.ts` is the CLASSIFIER. Its poll tick reads
15
+ * the thrown value to decide between "back off to a probe
16
+ * cadence" and "stay quiet, next tick may succeed".
17
+ *
18
+ * The vocabulary lives in its own module rather than in either party
19
+ * because `runtime.ts` already imports `channel-transport.ts` — a
20
+ * reverse import would close a cycle. Naming it for the CONCEPT
21
+ * (relay incapability) rather than for either party keeps the
22
+ * dependency direction obvious.
23
+ *
24
+ * Classification is by type, never by message string: a thrown
25
+ * `Error` whose text happens to mention relaying is NOT a structural
26
+ * failure, and re-wording a message must never silently re-classify
27
+ * a channel.
28
+ */
29
+ /**
30
+ * Canonical code carried by {@link RelayIncapableError}.
31
+ *
32
+ * Package-internal. Nothing in this module reaches a host: it is not
33
+ * re-exported from `index.ts`, so an embedding app can neither catch
34
+ * this class nor read this code. The host-facing surface for the same
35
+ * information is the `channel-poll-degraded` / `channel-poll-recovered`
36
+ * observability pair, which IS exported.
37
+ */
38
+ export const RELAY_INCAPABLE = 'RELAY_INCAPABLE';
39
+ /**
40
+ * Thrown when the runtime has CONFIRMED (not merely suspected) that
41
+ * the surrounding host cannot relay `tools/call` to the MCP server,
42
+ * so an attempt would be spent for a guaranteed failure.
43
+ *
44
+ * "Confirmed" is load-bearing. Absence of an advertised `serverTools`
45
+ * capability is NOT confirmation — plenty of hosts under-advertise
46
+ * and relay perfectly well. Only a real gesture that failed
47
+ * relay-shaped sets the latch this error reports, and any later
48
+ * well-formed result envelope clears it.
49
+ *
50
+ * Consumers MUST classify by `instanceof` (or by `code`), never by
51
+ * parsing {@link Error.message}.
52
+ */
53
+ export class RelayIncapableError extends Error {
54
+ /** Canonical classification code. */
55
+ code = RELAY_INCAPABLE;
56
+ constructor() {
57
+ super('tools/call unavailable: host did not advertise serverTools');
58
+ this.name = 'RelayIncapableError';
59
+ }
60
+ }
61
+ /**
62
+ * Type guard for the structural-failure class. Thrower and classifier
63
+ * are bundled together into one iframe artifact, so `instanceof` is
64
+ * exact here — there is no module-realm boundary between them to
65
+ * defend against.
66
+ */
67
+ export function isRelayIncapableError(err) {
68
+ return err instanceof RelayIncapableError;
69
+ }