@ggui-ai/iframe-runtime 0.6.3 → 0.7.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'),
@@ -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
@@ -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
+ }
package/dist/runtime.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { DrainAckPayload, GguiSession } from '@ggui-ai/protocol';
1
+ import type { DrainAckPayload, JsonValue, JsonObject, GguiSession } from '@ggui-ai/protocol';
2
2
  import type { WebSocketMessage } from '@ggui-ai/protocol/transport/websocket';
3
3
  import { type McpAppAiGguiRenderMeta, type McpAppBootstrapFailedMessage, type McpAppRendererReadyMessage } from '@ggui-ai/protocol/integrations/mcp-apps';
4
4
  import type { GguiSessionSeedInput } from './types.js';
@@ -71,6 +71,19 @@ export interface RendererBootFailedMessage extends McpAppBootstrapFailedMessage
71
71
  readonly reason: RendererBootFailureReason;
72
72
  }
73
73
  export interface BootSequenceOptions {
74
+ /**
75
+ * Document the boot path mounts into — the render root, and the
76
+ * toast announcer's live regions.
77
+ *
78
+ * MUST be the global `document` in any real deployment. The toast +
79
+ * cue surface reads the global directly, and production passes the
80
+ * global here (see `runBootProduction`), so the two are one object.
81
+ * A caller handing a DIFFERENT document pre-registers the live
82
+ * regions somewhere no announcement will ever reach; the surface then
83
+ * falls back to creating them in the global document at the moment of
84
+ * the first message, which is precisely the same-tick creation that
85
+ * loses that first announcement.
86
+ */
74
87
  readonly doc: Document;
75
88
  /**
76
89
  * Spec-canonical {@link App} instance + its {@link Transport}.
@@ -119,14 +132,17 @@ export interface BootSequenceOptions {
119
132
  * emission on UPGRADE_REQUIRED.
120
133
  * - `subscribe-failed` — forwarded from subscribe's emission on
121
134
  * transient reconnect transitions.
122
- * - `auth-required` — fired by the system channel handler when an
123
- * `auth_required` system frame arrives.
124
135
  * - `channel-transport-*` — fired by the channel-transport router
125
136
  * on per-channel transport picks / fallbacks / resubscribes.
126
137
  *
127
- * Absent = observability emission skipped entirely (matches the
128
- * ProtocolError posture; the `<McpAppIframe>` host wrapper decides
129
- * whether to bind this via its `onObserve` prop).
138
+ * Absent = observability emission for the events above is skipped
139
+ * entirely (matches the ProtocolError posture; the `<McpAppIframe>`
140
+ * host wrapper decides whether to bind this via its `onObserve`
141
+ * prop). Exception: `relay-incapability` events do NOT flow through
142
+ * this seam — they originate in module-level gesture-dispatch code
143
+ * outside the boot graph (it runs independently of any given boot
144
+ * call) and always ride the postMessage-to-parent default, whether
145
+ * or not this option is bound.
130
146
  */
131
147
  readonly onObserve?: ObservabilityEmitter;
132
148
  /**
@@ -204,9 +220,8 @@ export interface RendererHooks {
204
220
  * with `StackRenderer`.
205
221
  *
206
222
  * `onObserve` — optional observability emitter threaded down to
207
- * the channel handlers (system `auth-required`) and the
208
- * channel-transport router (`channel-transport-*`). Absent = those
209
- * sites run silent (the connect-time emission sites —
223
+ * the channel-transport router (`channel-transport-*`). Absent =
224
+ * those sites run silent (the connect-time emission sites —
210
225
  * `connectViaRegistry`'s version + subscribe events — still fire
211
226
  * via their own emitters).
212
227
  */
@@ -271,7 +286,7 @@ export interface RendererHandle {
271
286
  /**
272
287
  * Channel-client registry holding handlers for every WS frame type
273
288
  * the iframe routes (`render`, `data`, `props_update`, `drain_ack`,
274
- * `channel_payload`, `channel_error`, `system`). The
289
+ * `channel_payload`, `channel_error`). The
275
290
  * registry-bound transport is the sole dispatch surface — frames
276
291
  * arrive directly through registered handlers, no longer through a
277
292
  * separate `onMessage` callback.
@@ -391,6 +406,45 @@ export declare function emitAudit(args: {
391
406
  readonly actionId: string;
392
407
  readonly firedAt: string;
393
408
  }): void;
409
+ /**
410
+ * The announcer's two live regions, keyed by politeness (ggui#447).
411
+ *
412
+ * Two regions rather than one with a swapped `aria-live`: politeness is
413
+ * read when a region is REGISTERED, so a screen reader that already
414
+ * mapped a node as polite is not guaranteed to notice it turning
415
+ * assertive in the same mutation that delivers the text. Fixed
416
+ * politeness per node removes the question.
417
+ */
418
+ interface ToastLiveRegions {
419
+ /** `role="status"` — queued behind whatever is being read. */
420
+ readonly polite: HTMLElement;
421
+ /** `role="alert"` — interrupts. Failure + action-required classes. */
422
+ readonly assertive: HTMLElement;
423
+ }
424
+ /**
425
+ * Ensure the pair of live regions the toast surface announces through.
426
+ *
427
+ * MUST be called before the first message can land — a live region
428
+ * created in the same tick as its first content is a region the screen
429
+ * reader was never watching, and that first announcement is lost. The
430
+ * boot path calls this at the same point it mounts the render root, so
431
+ * every toast (at minimum a user gesture away) lands in a region that
432
+ * has been registered for many frames. The toast + cue paths call it
433
+ * again defensively; it is idempotent per document.
434
+ *
435
+ * Visually hidden rather than `display:none`: a region that is not
436
+ * rendered is not in the accessibility tree, and content added to it is
437
+ * never announced. The clip rectangle keeps it out of the visual layout
438
+ * while leaving it live.
439
+ *
440
+ * Returns `null` when the document has no body yet — nothing to attach
441
+ * to, and no announcement is possible.
442
+ *
443
+ * @internal — exported for unit tests.
444
+ */
445
+ export declare function ensureToastAnnouncer(doc: Document): ToastLiveRegions | null;
446
+ /** @internal — exported for unit tests to reset module state. */
447
+ export declare function __resetRelayNoticeForTest(): void;
394
448
  /**
395
449
  * Dispatch a submit-action via the empirically-validated bridge
396
450
  * chain (validated against claude.ai): a `tools/call` to
@@ -411,6 +465,65 @@ export declare function dispatchSubmitAction(args: {
411
465
  readonly sessionId: string;
412
466
  readonly appId: string;
413
467
  }): void;
468
+ /**
469
+ * Channel-transport router's `tools/call` invoker (ggui#440) —
470
+ * iframe-polling transport, `tools/call` against the parent MCP host
471
+ * via `app.callServerTool` (spec-canonical) when the App handle is
472
+ * set; falls back to raw postMessage pre-handshake or in tests.
473
+ * Direct call (no LLM consent loop). Returns the tool's
474
+ * structuredContent (or `content[0]` if that's where the payload
475
+ * landed) as a `JsonValue`. On RPC error we throw — the router
476
+ * catches and silently retries on the next tick.
477
+ *
478
+ * Once a REAL gesture has confirmed this host can't relay (the latch
479
+ * above — set only after {@link dispatchSubmitAction} actually tried
480
+ * and failed on a host with no `serverTools` advertised, with
481
+ * capabilities already captured, AND the failure was relay-shaped —
482
+ * not a well-formed `{ok:false}` result; see `isRelayShapedFailure`),
483
+ * every subsequent poll will fail identically.
484
+ *
485
+ * This guard does NOT stop the router's poll loop, and nothing here
486
+ * should ever be written so that it could (ggui#443). It does two
487
+ * things. It makes each doomed tick CHEAP — the poll throws before
488
+ * the `callServerToolSpec` round-trip. And, by throwing the typed
489
+ * {@link RelayIncapableError} rather than a bare `Error`, it tells
490
+ * the router WHICH KIND of failure this is: `channel-transport.ts`'s
491
+ * tick classifies structural rejections apart from transient ones and
492
+ * drops the channel to a slow probe cadence, emitting one
493
+ * `channel-poll-degraded` event per transition. The first poll that
494
+ * succeeds after the latch clears restores the normal cadence and
495
+ * emits `channel-poll-recovered`.
496
+ *
497
+ * The channel keeps probing throughout, which is what makes that
498
+ * recovery possible: the latch clears only when a call succeeds, and
499
+ * only an attempt can discover that. A channel stopped on structural
500
+ * failure could never come back.
501
+ *
502
+ * Keyed on the LATCH, not on `hostCanRelayToolCalls()` directly: raw
503
+ * advertisement absence never changes after boot, so gating on it
504
+ * here would throw pre-attempt for the LIFE OF THE SESSION on any
505
+ * host that under-advertises but genuinely proxies `tools/call`
506
+ * (ggui's own embed host pre-Task-2 is exactly this case) — silently
507
+ * killing working channel polls forever. That is "absence of a
508
+ * capability blocks an attempt", which the fail-safe constraint
509
+ * forbids. The latch implies `!hostCanRelayToolCalls()` (it only
510
+ * ever sets inside that guard), so testing it alone is sufficient —
511
+ * a host that never fails a real gesture never latches, and its
512
+ * channels keep working.
513
+ *
514
+ * Extracted to a named export (rather than an inline closure inside
515
+ * `bootProduction`) so unit tests can drive it directly through the
516
+ * lightweight `MockTransport` harness — `bootProduction` dynamic-
517
+ * imports the full react/design/wire module graph, which is too
518
+ * heavy to exercise from a spec file (see
519
+ * `boot-production-context.test.ts`'s docstring for precedent).
520
+ *
521
+ * @internal — exported for unit tests + production reuse.
522
+ */
523
+ export declare function channelToolsCall(args: {
524
+ readonly toolName: string;
525
+ readonly args: JsonObject;
526
+ }): Promise<JsonValue>;
414
527
  /**
415
528
  * Resolve the name of the spec-canonical submit-action receiver tool
416
529
  * the dispatch path targets with `tools/call`.