@ggui-ai/mcp-server-handlers 0.7.0 → 0.9.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 (50) hide show
  1. package/dist/blueprints/index.d.ts +2 -2
  2. package/dist/blueprints/index.d.ts.map +1 -1
  3. package/dist/blueprints/index.js +1 -1
  4. package/dist/blueprints/search-blueprints.d.ts +64 -12
  5. package/dist/blueprints/search-blueprints.d.ts.map +1 -1
  6. package/dist/blueprints/search-blueprints.js +166 -16
  7. package/dist/renders/amend.d.ts +57 -0
  8. package/dist/renders/amend.d.ts.map +1 -0
  9. package/dist/renders/amend.js +64 -0
  10. package/dist/renders/apply-ggui-session-patch.d.ts +4 -0
  11. package/dist/renders/apply-ggui-session-patch.d.ts.map +1 -1
  12. package/dist/renders/apply-ggui-session-patch.js +1 -1
  13. package/dist/renders/assert-props-contract.d.ts +5 -4
  14. package/dist/renders/assert-props-contract.d.ts.map +1 -1
  15. package/dist/renders/assert-props-contract.js +6 -5
  16. package/dist/renders/code-delivery-events.d.ts +45 -29
  17. package/dist/renders/code-delivery-events.d.ts.map +1 -1
  18. package/dist/renders/code-delivery-events.js +42 -25
  19. package/dist/renders/consume.js +1 -1
  20. package/dist/renders/decide-handshake.d.ts +15 -0
  21. package/dist/renders/decide-handshake.d.ts.map +1 -1
  22. package/dist/renders/decide-handshake.js +18 -0
  23. package/dist/renders/declare-tool-catalog.js +1 -1
  24. package/dist/renders/handshake.d.ts.map +1 -1
  25. package/dist/renders/handshake.js +58 -12
  26. package/dist/renders/index.d.ts +5 -1
  27. package/dist/renders/index.d.ts.map +1 -1
  28. package/dist/renders/index.js +5 -1
  29. package/dist/renders/props-mutation-core.d.ts +60 -0
  30. package/dist/renders/props-mutation-core.d.ts.map +1 -0
  31. package/dist/renders/props-mutation-core.js +339 -0
  32. package/dist/renders/render.d.ts +2 -0
  33. package/dist/renders/render.d.ts.map +1 -1
  34. package/dist/renders/render.js +61 -32
  35. package/dist/renders/runtime-pull.d.ts +122 -0
  36. package/dist/renders/runtime-pull.d.ts.map +1 -0
  37. package/dist/renders/runtime-pull.js +178 -0
  38. package/dist/renders/runtime-telemetry.d.ts +60 -0
  39. package/dist/renders/runtime-telemetry.d.ts.map +1 -0
  40. package/dist/renders/runtime-telemetry.js +76 -0
  41. package/dist/renders/slice-meta-derivation.d.ts +146 -6
  42. package/dist/renders/slice-meta-derivation.d.ts.map +1 -1
  43. package/dist/renders/slice-meta-derivation.js +190 -8
  44. package/dist/renders/submit-action.d.ts +9 -0
  45. package/dist/renders/submit-action.d.ts.map +1 -1
  46. package/dist/renders/submit-action.js +70 -1
  47. package/dist/renders/update.d.ts +33 -42
  48. package/dist/renders/update.d.ts.map +1 -1
  49. package/dist/renders/update.js +99 -230
  50. package/package.json +7 -7
@@ -0,0 +1,122 @@
1
+ /**
2
+ * `ggui_runtime_pull` — terminal bridge-pull rung of the live-channel
3
+ * failover ladder (WS → SSE → HTTP polling → bridge-pull).
4
+ *
5
+ * In a CSP-jailed MCP Apps host (claude.ai) the iframe can reach NO
6
+ * network origin — no WS, no SSE, no HTTP polling. The one channel it
7
+ * always has is the host's `tools/call` postMessage bridge. This tool
8
+ * serves the GguiSessionEvent ledger over that bridge: the
9
+ * `@ggui-ai/iframe-runtime` bridge rung pulls it on a flat interval,
10
+ * reusing the SAME cursor-walk algorithm (and the same parse core) as
11
+ * its HTTP `/events` polling rung, with `tools/call` as the carrier.
12
+ *
13
+ * **Wire shape** (what iframe-runtime postMessages via `tools/call`
14
+ * and the host relays to the MCP server):
15
+ *
16
+ * ```jsonc
17
+ * {
18
+ * "method": "tools/call",
19
+ * "params": {
20
+ * "name": "ggui_runtime_pull",
21
+ * "arguments": {
22
+ * "sessionId": "rnd_…", // bootstrap.sessionId
23
+ * "sinceSequence": 12, // replay cursor; omit = 0
24
+ * "limit": 100 // page size; clamped to 100
25
+ * }
26
+ * }
27
+ * }
28
+ * ```
29
+ *
30
+ * **Output = EXACT `EventsResponse` parity** with
31
+ * `GET /api/sessions/:sessionId/events` (`@ggui-ai/mcp-server`'s
32
+ * `api-renders-routes.ts`) — both read the ledger through the same
33
+ * `GguiSessionStore.listEventsSince` and answer with the same two
34
+ * shapes, so the client reuses one parse core across carriers:
35
+ *
36
+ * - Normal page: `{events, lastSequence, hasMore}` — events strictly
37
+ * ascending by seq with `seq > sinceSequence`, `lastSequence` = the
38
+ * render's high-water mark (advances the cursor even on an empty
39
+ * page), `hasMore` = the page was truncated by `limit` (client
40
+ * SHOULD immediately re-pull with the advanced cursor).
41
+ * - Horizon: `{reason: 'REPLAY_HORIZON_PASSED', currentSequence}` — a
42
+ * NORMAL result arm, not an error (the bridge rung is terminal;
43
+ * this is a re-sync instruction, mirroring the route's 410 body).
44
+ * Returned when the cursor fell out of the replayable window on
45
+ * EITHER side: `sinceSequence > lastSequence` (cursor from a
46
+ * different deployment / reset render) or
47
+ * `sinceSequence < horizonSeq` (events evicted from the bounded
48
+ * retention window). Client recovery: re-mount from a fresh
49
+ * snapshot and reset the cursor to `currentSequence`.
50
+ *
51
+ * **Failure modes:**
52
+ * - Unknown sessionId, cross-tenant sessionId, and a render deleted
53
+ * mid-read all surface uniformly as
54
+ * {@link GguiSessionNotFoundError} — no cross-tenant existence
55
+ * leak (tenancy gate: `renderStore.get` + `ctx.appId`).
56
+ * - Malformed input (empty sessionId, negative cursor, `limit < 1`)
57
+ * rejects at the zod boundary.
58
+ *
59
+ * **Visibility.** Registered with `_meta.ui.visibility: ['app']` so
60
+ * MCP Apps hosts route iframe-issued `tools/call` to this handler per
61
+ * spec §401 — REQUIRED: without it hosts reject view-issued calls, and
62
+ * the terminal rung goes dark exactly where it is the only channel
63
+ * left. Outer agents don't see the tool — ledger replay is a runtime
64
+ * concern, not an agent gesture.
65
+ *
66
+ * Deliberate divergences from the `/events` route, per the bridge-pull
67
+ * ruling: `sinceSequence` is optional (`?? 0` — the bridge rung owns
68
+ * its cursor), and `limit` above {@link RUNTIME_PULL_MAX_LIMIT} is
69
+ * CLAMPED, not rejected.
70
+ */
71
+ import { z } from 'zod';
72
+ import { type GguiRuntimePullOutput } from '@ggui-ai/protocol';
73
+ import type { GguiSessionStore } from '@ggui-ai/mcp-server-core';
74
+ import type { SharedHandler } from '../types.js';
75
+ declare const inputSchema: {
76
+ readonly sessionId: z.ZodString;
77
+ readonly sinceSequence: z.ZodOptional<z.ZodNumber>;
78
+ readonly limit: z.ZodOptional<z.ZodNumber>;
79
+ readonly wait: z.ZodOptional<z.ZodNumber>;
80
+ };
81
+ declare const outputSchema: {
82
+ /** Normal page only — ledger rows with `seq > sinceSequence`, ascending. */
83
+ readonly events: z.ZodOptional<z.ZodArray<z.ZodObject<{
84
+ seq: z.ZodNumber;
85
+ type: z.ZodString;
86
+ timestamp: z.ZodString;
87
+ data: z.ZodUnknown;
88
+ }, z.core.$strip>>>;
89
+ /** Normal page only — the render's current high-water mark (cursor floor on empty pages). */
90
+ readonly lastSequence: z.ZodOptional<z.ZodNumber>;
91
+ /** Normal page only — `true` when `limit` truncated the page; re-pull immediately. */
92
+ readonly hasMore: z.ZodOptional<z.ZodBoolean>;
93
+ /** Horizon arm only — the cursor fell out of the replayable window (normal result, not an error). */
94
+ readonly reason: z.ZodOptional<z.ZodLiteral<"REPLAY_HORIZON_PASSED">>;
95
+ /** Horizon arm only — reset the cursor here after re-mounting from a snapshot. */
96
+ readonly currentSequence: z.ZodOptional<z.ZodNumber>;
97
+ };
98
+ export interface GguiRuntimePullHandlerDeps {
99
+ /**
100
+ * The same store `ggui_render` commits to and the `/events` route
101
+ * reads from — one ledger, two carriers. The handler needs both
102
+ * `get` (tenancy gate) and `listEventsSince` (cursor read).
103
+ */
104
+ readonly renderStore: GguiSessionStore;
105
+ /**
106
+ * Subscription-mode hold probe cadence, in ms. During a `wait` hold
107
+ * the handler re-reads the ledger at this interval — deliberately a
108
+ * store poll rather than an in-process wake seam, because the
109
+ * update that ends the hold may land on a DIFFERENT pod (the store
110
+ * is the only cross-pod truth this package may assume). Defaults to
111
+ * 1000. Test hook: inject a small value for deterministic hold
112
+ * tests without fake timers.
113
+ */
114
+ readonly waitProbeIntervalMs?: number;
115
+ }
116
+ /**
117
+ * Build the `ggui_runtime_pull` handler. Registers as app-visible so
118
+ * MCP Apps hosts route iframe-issued `tools/call` to it per spec §401.
119
+ */
120
+ export declare function createGguiRuntimePullHandler(deps: GguiRuntimePullHandlerDeps): SharedHandler<typeof inputSchema, typeof outputSchema, GguiRuntimePullOutput>;
121
+ export {};
122
+ //# sourceMappingURL=runtime-pull.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"runtime-pull.d.ts","sourceRoot":"","sources":["../../src/renders/runtime-pull.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqEG;AACH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EAKL,KAAK,qBAAqB,EAC3B,MAAM,mBAAmB,CAAC;AAC3B,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,0BAA0B,CAAC;AACjE,OAAO,KAAK,EAAkB,aAAa,EAAE,MAAM,aAAa,CAAC;AAKjE,QAAA,MAAM,WAAW;;;;;CAAwB,CAAC;AAQ1C,QAAA,MAAM,YAAY;IAChB,4EAA4E;;;;;;;IAE5E,6FAA6F;;IAE7F,sFAAsF;;IAEtF,qGAAqG;;IAErG,kFAAkF;;CAE1E,CAAC;AAEX,MAAM,WAAW,0BAA0B;IACzC;;;;OAIG;IACH,QAAQ,CAAC,WAAW,EAAE,gBAAgB,CAAC;IACvC;;;;;;;;OAQG;IACH,QAAQ,CAAC,mBAAmB,CAAC,EAAE,MAAM,CAAC;CACvC;AAED;;;GAGG;AACH,wBAAgB,4BAA4B,CAC1C,IAAI,EAAE,0BAA0B,GAC/B,aAAa,CAAC,OAAO,WAAW,EAAE,OAAO,YAAY,EAAE,qBAAqB,CAAC,CAkG/E"}
@@ -0,0 +1,178 @@
1
+ /**
2
+ * `ggui_runtime_pull` — terminal bridge-pull rung of the live-channel
3
+ * failover ladder (WS → SSE → HTTP polling → bridge-pull).
4
+ *
5
+ * In a CSP-jailed MCP Apps host (claude.ai) the iframe can reach NO
6
+ * network origin — no WS, no SSE, no HTTP polling. The one channel it
7
+ * always has is the host's `tools/call` postMessage bridge. This tool
8
+ * serves the GguiSessionEvent ledger over that bridge: the
9
+ * `@ggui-ai/iframe-runtime` bridge rung pulls it on a flat interval,
10
+ * reusing the SAME cursor-walk algorithm (and the same parse core) as
11
+ * its HTTP `/events` polling rung, with `tools/call` as the carrier.
12
+ *
13
+ * **Wire shape** (what iframe-runtime postMessages via `tools/call`
14
+ * and the host relays to the MCP server):
15
+ *
16
+ * ```jsonc
17
+ * {
18
+ * "method": "tools/call",
19
+ * "params": {
20
+ * "name": "ggui_runtime_pull",
21
+ * "arguments": {
22
+ * "sessionId": "rnd_…", // bootstrap.sessionId
23
+ * "sinceSequence": 12, // replay cursor; omit = 0
24
+ * "limit": 100 // page size; clamped to 100
25
+ * }
26
+ * }
27
+ * }
28
+ * ```
29
+ *
30
+ * **Output = EXACT `EventsResponse` parity** with
31
+ * `GET /api/sessions/:sessionId/events` (`@ggui-ai/mcp-server`'s
32
+ * `api-renders-routes.ts`) — both read the ledger through the same
33
+ * `GguiSessionStore.listEventsSince` and answer with the same two
34
+ * shapes, so the client reuses one parse core across carriers:
35
+ *
36
+ * - Normal page: `{events, lastSequence, hasMore}` — events strictly
37
+ * ascending by seq with `seq > sinceSequence`, `lastSequence` = the
38
+ * render's high-water mark (advances the cursor even on an empty
39
+ * page), `hasMore` = the page was truncated by `limit` (client
40
+ * SHOULD immediately re-pull with the advanced cursor).
41
+ * - Horizon: `{reason: 'REPLAY_HORIZON_PASSED', currentSequence}` — a
42
+ * NORMAL result arm, not an error (the bridge rung is terminal;
43
+ * this is a re-sync instruction, mirroring the route's 410 body).
44
+ * Returned when the cursor fell out of the replayable window on
45
+ * EITHER side: `sinceSequence > lastSequence` (cursor from a
46
+ * different deployment / reset render) or
47
+ * `sinceSequence < horizonSeq` (events evicted from the bounded
48
+ * retention window). Client recovery: re-mount from a fresh
49
+ * snapshot and reset the cursor to `currentSequence`.
50
+ *
51
+ * **Failure modes:**
52
+ * - Unknown sessionId, cross-tenant sessionId, and a render deleted
53
+ * mid-read all surface uniformly as
54
+ * {@link GguiSessionNotFoundError} — no cross-tenant existence
55
+ * leak (tenancy gate: `renderStore.get` + `ctx.appId`).
56
+ * - Malformed input (empty sessionId, negative cursor, `limit < 1`)
57
+ * rejects at the zod boundary.
58
+ *
59
+ * **Visibility.** Registered with `_meta.ui.visibility: ['app']` so
60
+ * MCP Apps hosts route iframe-issued `tools/call` to this handler per
61
+ * spec §401 — REQUIRED: without it hosts reject view-issued calls, and
62
+ * the terminal rung goes dark exactly where it is the only channel
63
+ * left. Outer agents don't see the tool — ledger replay is a runtime
64
+ * concern, not an agent gesture.
65
+ *
66
+ * Deliberate divergences from the `/events` route, per the bridge-pull
67
+ * ruling: `sinceSequence` is optional (`?? 0` — the bridge rung owns
68
+ * its cursor), and `limit` above {@link RUNTIME_PULL_MAX_LIMIT} is
69
+ * CLAMPED, not rejected.
70
+ */
71
+ import { z } from 'zod';
72
+ import { RUNTIME_PULL_MAX_LIMIT, RUNTIME_PULL_MAX_WAIT_SECONDS, gguiSessionEventSchema, runtimePullInputShape, } from '@ggui-ai/protocol';
73
+ import { GguiSessionNotFoundError } from './errors.js';
74
+ // Canonical SSoT shape — authored once in `@ggui-ai/protocol`
75
+ // (`schemas/mcp.ts`), same wiring as `ggui_get_session`.
76
+ const inputSchema = runtimePullInputShape;
77
+ // Flat raw shape for MCP tool registration — a `ZodRawShape` cannot
78
+ // express the top-level two-arm union, so every field of both arms is
79
+ // optional here and the canonical strict contract stays
80
+ // `runtimePullOutputSchema` (`@ggui-ai/protocol`). The alignment tests
81
+ // in `runtime-pull.test.ts` pin this shape to the union key-for-key —
82
+ // same posture as `ggui_update`'s input.
83
+ const outputSchema = {
84
+ /** Normal page only — ledger rows with `seq > sinceSequence`, ascending. */
85
+ events: z.array(gguiSessionEventSchema).optional(),
86
+ /** Normal page only — the render's current high-water mark (cursor floor on empty pages). */
87
+ lastSequence: z.number().int().min(0).optional(),
88
+ /** Normal page only — `true` when `limit` truncated the page; re-pull immediately. */
89
+ hasMore: z.boolean().optional(),
90
+ /** Horizon arm only — the cursor fell out of the replayable window (normal result, not an error). */
91
+ reason: z.literal('REPLAY_HORIZON_PASSED').optional(),
92
+ /** Horizon arm only — reset the cursor here after re-mounting from a snapshot. */
93
+ currentSequence: z.number().int().min(0).optional(),
94
+ };
95
+ /**
96
+ * Build the `ggui_runtime_pull` handler. Registers as app-visible so
97
+ * MCP Apps hosts route iframe-issued `tools/call` to it per spec §401.
98
+ */
99
+ export function createGguiRuntimePullHandler(deps) {
100
+ return {
101
+ name: 'ggui_runtime_pull',
102
+ title: '[runtime] Pull Events',
103
+ audience: ['runtime'],
104
+ description: 'Serves the GguiSessionEvent ledger over the host\'s tools/call postMessage bridge — the terminal failover rung for iframes whose CSP blocks every network channel. Cursor-replay semantics identical to the HTTP events endpoint: returns {events, lastSequence, hasMore} for a live cursor, or {reason: "REPLAY_HORIZON_PASSED", currentSequence} as a NORMAL result when the cursor fell out of the replayable window (client re-mounts from a snapshot and resets its cursor). Never invoked by the model directly — `_meta.ui.visibility: [\'app\']` restricts callers to MCP Apps views per spec §401; the iframe holds no auth credential so the host is the relay party.',
105
+ inputSchema,
106
+ outputSchema,
107
+ _meta: {
108
+ ui: {
109
+ // Spec §401: only an MCP Apps view (iframe) can call.
110
+ // Outer agent does NOT see this tool.
111
+ visibility: ['app'],
112
+ },
113
+ },
114
+ async handler(rawInput, ctx) {
115
+ const parsed = z.object(inputSchema).parse(rawInput);
116
+ const { sessionId } = parsed;
117
+ const sinceSequence = parsed.sinceSequence ?? 0;
118
+ const limit = Math.min(parsed.limit ?? RUNTIME_PULL_MAX_LIMIT, RUNTIME_PULL_MAX_LIMIT);
119
+ // Subscription-mode hold (transport-ladder ruling 20). Clamped
120
+ // like `limit` — a tolerant reader, and the ceiling is a hard
121
+ // safety line: the hold MUST resolve before host-side tools/call
122
+ // timeouts (ggui_consume's 25s holds prove ~20s is safe).
123
+ const waitMs = Math.min(parsed.wait ?? 0, RUNTIME_PULL_MAX_WAIT_SECONDS) * 1000;
124
+ const probeIntervalMs = deps.waitProbeIntervalMs ?? 1000;
125
+ // Tenancy gate — cross-tenant + missing surface uniformly so
126
+ // cross-tenant existence is not leaked (same posture as the
127
+ // /events route's wsToken appId check, with ctx.appId as the
128
+ // proved identity on this carrier).
129
+ const stored = await deps.renderStore.get(sessionId);
130
+ if (!stored || stored.appId !== ctx.appId) {
131
+ throw new GguiSessionNotFoundError(sessionId);
132
+ }
133
+ const deadline = Date.now() + waitMs;
134
+ // Hold loop: one immediate read, then — empty page + time left —
135
+ // probe the store at `probeIntervalMs` until an event lands or
136
+ // the hold elapses. The probe is a STORE read on purpose: the
137
+ // event that ends this hold may be committed by another pod, and
138
+ // the store is the only cross-pod truth available here. Horizon
139
+ // results and non-empty pages return immediately regardless of
140
+ // remaining hold budget.
141
+ for (;;) {
142
+ const result = await deps.renderStore.listEventsSince(sessionId, sinceSequence, limit);
143
+ if (result === null) {
144
+ // Render deleted between reads — same uniform not-found as
145
+ // never-existed (also ends a hold on a mid-hold delete).
146
+ throw new GguiSessionNotFoundError(sessionId);
147
+ }
148
+ // Replay-horizon gate — mirrors the /events route verbatim.
149
+ // Two cases collapse to REPLAY_HORIZON_PASSED:
150
+ // (a) sinceSequence > lastSequence — cursor from a different
151
+ // deployment / reset render; the server cannot safely
152
+ // advance it.
153
+ // (b) sinceSequence < horizonSeq — events evicted from the
154
+ // bounded retention window (cloud TTL; in-mem never).
155
+ if (sinceSequence > result.lastSequence ||
156
+ sinceSequence < result.horizonSeq) {
157
+ return {
158
+ reason: 'REPLAY_HORIZON_PASSED',
159
+ currentSequence: result.lastSequence,
160
+ };
161
+ }
162
+ if (result.events.length > 0 || Date.now() >= deadline) {
163
+ // Events to deliver, or the hold elapsed — an empty page
164
+ // after a full hold is a NORMAL result (the client stays
165
+ // subscribed by immediately re-pulling).
166
+ return {
167
+ events: result.events,
168
+ lastSequence: result.lastSequence,
169
+ hasMore: result.hasMore,
170
+ };
171
+ }
172
+ await new Promise((resolve) => {
173
+ setTimeout(resolve, probeIntervalMs);
174
+ });
175
+ }
176
+ },
177
+ };
178
+ }
@@ -0,0 +1,60 @@
1
+ /**
2
+ * `ggui_runtime_telemetry` — the iframe runtime's transport
3
+ * self-report, and the ONLY window an operator has into the delivery
4
+ * ladder's behavior on sandboxed MCP-Apps hosts.
5
+ *
6
+ * Why this tool exists (2026-08-12, transport-ladder debugging): on
7
+ * claude.ai the view runs in a `claudemcpcontent.com` sandbox frame
8
+ * whose console is unreadable from outside and whose CSP blocks every
9
+ * network channel — when the WS → SSE → polling → bridge-pull ladder
10
+ * misbehaves there, NOTHING reaches server logs and the failure is
11
+ * invisible by construction. The runtime therefore batches its own
12
+ * ladder events (boot-path decision, per-rung status transitions, the
13
+ * live-channel logger's `channel_*` diagnostics, doorbell rings) and
14
+ * flushes them over the host's `tools/call` postMessage bridge — the
15
+ * one carrier a CSP jail cannot block.
16
+ *
17
+ * Posture: fire-and-forget diagnostics, NEVER a data plane.
18
+ * - Stores nothing; emits ONE structured log line per batch.
19
+ * - `sessionId` is client-claimed — a log tag, never a read key, so
20
+ * no tenancy gate and no store dependency (nothing to leak).
21
+ * - Bounded by the protocol schema (≤ 40 events/batch, kind ≤ 64
22
+ * chars, detail ≤ 512) against log flooding; `ctx.appId` (the
23
+ * PROVED identity on this carrier) tags every line so abuse is
24
+ * attributable.
25
+ */
26
+ import { z } from 'zod';
27
+ import { type GguiRuntimeTelemetryOutput } from '@ggui-ai/protocol';
28
+ import type { SharedHandler } from '../types.js';
29
+ declare const inputSchema: {
30
+ readonly sessionId: z.ZodString;
31
+ readonly events: z.ZodArray<z.ZodObject<{
32
+ at: z.ZodNumber;
33
+ kind: z.ZodString;
34
+ detail: z.ZodOptional<z.ZodString>;
35
+ }, z.core.$strip>>;
36
+ };
37
+ declare const outputSchema: {
38
+ readonly ok: z.ZodLiteral<true>;
39
+ };
40
+ /**
41
+ * Structured-log sink. Matches the narrow slice of the pod/server
42
+ * logger this handler needs; OSS default falls back to a single-line
43
+ * `console.log` in the same shape the log pipeline already parses.
44
+ */
45
+ export interface RuntimeTelemetryLogger {
46
+ info(event: string, fields: Record<string, unknown>): void;
47
+ }
48
+ export interface GguiRuntimeTelemetryHandlerDeps {
49
+ /** Optional structured logger; absent → single-line console JSON. */
50
+ readonly logger?: RuntimeTelemetryLogger;
51
+ }
52
+ /**
53
+ * Build the `ggui_runtime_telemetry` handler. Registers as
54
+ * app-visible (`_meta.ui.visibility: ['app']`) so MCP Apps hosts route
55
+ * iframe-issued `tools/call` to it per spec §401 — the same channel
56
+ * `ggui_runtime_submit_action` and `ggui_runtime_pull` ride.
57
+ */
58
+ export declare function createGguiRuntimeTelemetryHandler(deps?: GguiRuntimeTelemetryHandlerDeps): SharedHandler<typeof inputSchema, typeof outputSchema, GguiRuntimeTelemetryOutput>;
59
+ export {};
60
+ //# sourceMappingURL=runtime-telemetry.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"runtime-telemetry.d.ts","sourceRoot":"","sources":["../../src/renders/runtime-telemetry.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EAEL,KAAK,0BAA0B,EAChC,MAAM,mBAAmB,CAAC;AAC3B,OAAO,KAAK,EAAkB,aAAa,EAAE,MAAM,aAAa,CAAC;AAEjE,QAAA,MAAM,WAAW;;;;;;;CAA6B,CAAC;AAE/C,QAAA,MAAM,YAAY;;CAER,CAAC;AAEX;;;;GAIG;AACH,MAAM,WAAW,sBAAsB;IACrC,IAAI,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;CAC5D;AAED,MAAM,WAAW,+BAA+B;IAC9C,qEAAqE;IACrE,QAAQ,CAAC,MAAM,CAAC,EAAE,sBAAsB,CAAC;CAC1C;AAED;;;;;GAKG;AACH,wBAAgB,iCAAiC,CAC/C,IAAI,GAAE,+BAAoC,GACzC,aAAa,CAAC,OAAO,WAAW,EAAE,OAAO,YAAY,EAAE,0BAA0B,CAAC,CAyCpF"}
@@ -0,0 +1,76 @@
1
+ /**
2
+ * `ggui_runtime_telemetry` — the iframe runtime's transport
3
+ * self-report, and the ONLY window an operator has into the delivery
4
+ * ladder's behavior on sandboxed MCP-Apps hosts.
5
+ *
6
+ * Why this tool exists (2026-08-12, transport-ladder debugging): on
7
+ * claude.ai the view runs in a `claudemcpcontent.com` sandbox frame
8
+ * whose console is unreadable from outside and whose CSP blocks every
9
+ * network channel — when the WS → SSE → polling → bridge-pull ladder
10
+ * misbehaves there, NOTHING reaches server logs and the failure is
11
+ * invisible by construction. The runtime therefore batches its own
12
+ * ladder events (boot-path decision, per-rung status transitions, the
13
+ * live-channel logger's `channel_*` diagnostics, doorbell rings) and
14
+ * flushes them over the host's `tools/call` postMessage bridge — the
15
+ * one carrier a CSP jail cannot block.
16
+ *
17
+ * Posture: fire-and-forget diagnostics, NEVER a data plane.
18
+ * - Stores nothing; emits ONE structured log line per batch.
19
+ * - `sessionId` is client-claimed — a log tag, never a read key, so
20
+ * no tenancy gate and no store dependency (nothing to leak).
21
+ * - Bounded by the protocol schema (≤ 40 events/batch, kind ≤ 64
22
+ * chars, detail ≤ 512) against log flooding; `ctx.appId` (the
23
+ * PROVED identity on this carrier) tags every line so abuse is
24
+ * attributable.
25
+ */
26
+ import { z } from 'zod';
27
+ import { runtimeTelemetryInputShape, } from '@ggui-ai/protocol';
28
+ const inputSchema = runtimeTelemetryInputShape;
29
+ const outputSchema = {
30
+ ok: z.literal(true),
31
+ };
32
+ /**
33
+ * Build the `ggui_runtime_telemetry` handler. Registers as
34
+ * app-visible (`_meta.ui.visibility: ['app']`) so MCP Apps hosts route
35
+ * iframe-issued `tools/call` to it per spec §401 — the same channel
36
+ * `ggui_runtime_submit_action` and `ggui_runtime_pull` ride.
37
+ */
38
+ export function createGguiRuntimeTelemetryHandler(deps = {}) {
39
+ return {
40
+ name: 'ggui_runtime_telemetry',
41
+ title: '[runtime] Report Transport Telemetry',
42
+ audience: ['runtime'],
43
+ description: "Fire-and-forget transport diagnostics from the iframe runtime — batched delivery-ladder events (boot path, WS/SSE/polling/bridge rung transitions, doorbell rings) logged server-side for operator forensics. Stores nothing, returns {ok: true}. Never invoked by the model — `_meta.ui.visibility: ['app']` restricts callers to MCP Apps views; sandboxed hosts expose no console, so this tool is the only way ladder behavior there reaches an operator.",
44
+ inputSchema,
45
+ outputSchema,
46
+ _meta: {
47
+ ui: {
48
+ // Spec §401: only an MCP Apps view (iframe) can call. The
49
+ // outer agent does NOT see this tool.
50
+ visibility: ['app'],
51
+ },
52
+ },
53
+ // eslint-disable-next-line @typescript-eslint/require-await -- SharedHandler's handler contract is async; this one has no awaits (log-and-return)
54
+ async handler(rawInput, ctx) {
55
+ const parsed = z.object(inputSchema).parse(rawInput);
56
+ const line = {
57
+ sessionId: parsed.sessionId,
58
+ appId: ctx.appId,
59
+ eventCount: parsed.events.length,
60
+ events: parsed.events.map((e) => ({
61
+ at: e.at,
62
+ kind: e.kind,
63
+ ...(e.detail !== undefined ? { detail: e.detail } : {}),
64
+ })),
65
+ };
66
+ if (deps.logger) {
67
+ deps.logger.info('runtime_telemetry', line);
68
+ }
69
+ else {
70
+ // eslint-disable-next-line no-console -- structured single-line event for log-pipeline pickup (same posture as render-identity's emitters)
71
+ console.log(JSON.stringify({ msg: 'runtime_telemetry', ...line }));
72
+ }
73
+ return { ok: true };
74
+ },
75
+ };
76
+ }
@@ -48,9 +48,13 @@ import { type CompiledContractValidators, type McpAppAiGguiRenderMeta } from '@g
48
48
  * ships via a non-registry CDN (esm.sh, jsdelivr, …).
49
49
  * 2. `entry.bundleHost` + `entry.package` + `entry.version` —
50
50
  * hostname resolution: server assembles
51
- * `https://<bundleHost>/bundles/<scope>/<name>/<version>/bundle.js`
52
- * (matches `@ggui-ai/registry-core`'s `bundleStorage.bundleUrl(…)`
53
- * path layout — see `packages/registry-core/src/impls/memory-bundle-storage.ts`).
51
+ * `https://<bundleHost>/bundles/public/<scope>/<name>/<version>/bundle.js`
52
+ * — the PUBLIC prefix of `@ggui-ai/registry-core`'s
53
+ * visibility-split `BundleStorage` layout (H1). Host composition
54
+ * serves the anonymous render-time fetch, which only the public
55
+ * prefix answers; private artifacts always arrive as explicit
56
+ * `bundleUrl` values and are presign-rewritten at render-meta
57
+ * time ({@link rewritePrivateBundleUrls}).
54
58
  * 3. Spec default `registry.ggui.ai` — applied when `entry.bundleHost`
55
59
  * is absent but `package` + `version` are present (first-party
56
60
  * hosted-registry default).
@@ -90,6 +94,48 @@ export declare function resolveGadgetUrls(entry: Partial<Pick<GadgetDescriptor,
90
94
  readonly bundleUrl?: string;
91
95
  readonly styleUrl?: string;
92
96
  };
97
+ /**
98
+ * URL-path marker of the registry's PRIVATE bundle prefix (H1
99
+ * visibility split) — the substring {@link rewritePrivateBundleUrls}
100
+ * dispatches on.
101
+ */
102
+ export declare const PRIVATE_BUNDLE_PREFIX = "/bundles/private/";
103
+ /**
104
+ * H1 ruling 1b — rewrite a render item's PRIVATE gadget bundle URLs
105
+ * into fetchable equivalents at render-meta time.
106
+ *
107
+ * A private artifact's explicit `bundleUrl` / `styleUrl` / `typesUrl`
108
+ * point at the registry's `bundles/private/` prefix, which serves no
109
+ * anonymous request — the sandboxed iframe (which sends no
110
+ * credentials) could never load it. When a `presign` function is
111
+ * supplied, every descriptor URL containing
112
+ * {@link PRIVATE_BUNDLE_PREFIX} is replaced by `await presign(url)` —
113
+ * the hosted deployment presigns a short-lived object-storage URL via
114
+ * its pod task role. Everything else is untouched: public explicit
115
+ * URLs, bundleHost-composed URLs (which are public-prefix by
116
+ * construction, see {@link resolveGadgetUrls}), and descriptors with
117
+ * no URLs at all.
118
+ *
119
+ * **OSS default: passthrough.** Without `presign` the item is returned
120
+ * unchanged — the self-hosted story is same-origin: a self-hoster's
121
+ * iframe and registry share an origin, so its `/bundles/private/…`
122
+ * route can authenticate the fetch itself (cookie/session) or the
123
+ * operator simply publishes render-consumed gadgets public. No
124
+ * presigning infrastructure is required to run the OSS stack.
125
+ *
126
+ * MUST run BEFORE {@link deriveRenderMeta}: both the CSP origin
127
+ * allowlist (`deriveBundleOrigins`) and the iframe registrations
128
+ * (`deriveGadgetRegistrations`) read the descriptor URLs, so the
129
+ * rewritten (presigned) origin lands in `script-src`/`style-src`
130
+ * automatically and the iframe fetches exactly what the CSP allows.
131
+ * Zero iframe changes — the client sees an ordinary absolute URL.
132
+ *
133
+ * Purity note: `deriveRenderMeta` stays pure; this pre-pass is the
134
+ * ONLY projection stage that may perform I/O, and it returns a new
135
+ * item (never mutates) so the WeakMap-memoized URL resolution keys
136
+ * stay correct.
137
+ */
138
+ export declare function rewritePrivateBundleUrls(item: GguiSession, presign?: (url: string) => Promise<string>): Promise<GguiSession>;
93
139
  /**
94
140
  * Per-slot contextSpec data for an active render, derived to a
95
141
  * wire-friendly array. Returns `undefined` when no slots are declared.
@@ -276,7 +322,22 @@ export declare function derivePropsJson(item: GguiSession): string | undefined;
276
322
  */
277
323
  export interface RenderMetaView {
278
324
  readonly kind?: string;
325
+ /** History head epoch (#483) — the mount's own epoch for the freeze latch. */
326
+ readonly epoch?: number;
279
327
  readonly propsJson?: string;
328
+ /**
329
+ * Base64-encoded compiled component source — the fetch-free static
330
+ * delivery channel for hosts whose iframe CSP forbids cross-origin
331
+ * fetches (no `connect-src`/`script-src` to this server). Distinct
332
+ * from the 2026-05-13-retired raw-ESM `componentCode` field: this is
333
+ * base64 (inline-embedding-safe), consumed by the iframe-runtime's
334
+ * seed builder which PREFERS it over `codeUrl` and never fetches.
335
+ * Omitted when the source exceeds {@link CODE_B64_MAX_CHARS}
336
+ * (over-cap renders keep the codeUrl / live channels) and for
337
+ * system-card renders. Each transport spreads it explicitly, like
338
+ * every other view field.
339
+ */
340
+ readonly codeB64?: string;
280
341
  readonly contextSlots?: ReadonlyArray<{
281
342
  name: string;
282
343
  contextName: string;
@@ -432,17 +493,44 @@ export declare function deriveContractBundle(item: GguiSession): Promise<{
432
493
  readonly bundleSource: string;
433
494
  readonly validators: CompiledContractValidators;
434
495
  } | undefined>;
496
+ /**
497
+ * Ceiling on the base64 length {@link deriveRenderMeta} will inline as
498
+ * `codeB64` (256 KiB of base64 ≈ 192 KiB of source). Generated
499
+ * components run 5–50 KB; the cap exists so a pathological render
500
+ * cannot balloon every tool result and resource read it rides on.
501
+ * Over-cap renders keep their `codeUrl` / live-channel delivery.
502
+ */
503
+ export declare const CODE_B64_MAX_CHARS = 262144;
435
504
  /**
436
505
  * Build the {@link RenderMetaView} for a render — the
437
506
  * single-entry-point projection function every bootstrap transport
438
507
  * SHOULD call. Composing transports take the view, spread it into
439
508
  * their own envelope alongside render/auth/runtime concerns. The
440
- * static-component code body itself is delivered via the render
441
- * handler's `codeUrl` channel (composed from `codeStore` + `codeBaseUrl`);
442
- * this projection only carries the wire-shape metadata.
509
+ * static-component code body is delivered via the render handler's
510
+ * `codeUrl` channel (composed from `codeStore` + `codeBaseUrl`) AND —
511
+ * for fetch-blocked hosts — inline via the size-capped `codeB64`
512
+ * field this projection carries.
443
513
  *
444
514
  * Pure. Same input → identical output, byte-for-byte.
445
515
  */
516
+ /**
517
+ * Spread a {@link RenderMetaView}'s state + policy fields onto an
518
+ * `ai.ggui/render` slice literal — the ONE place the view→slice field
519
+ * list lives (#483 follow-up). Every slice emitter (ggui_render /
520
+ * ggui_update resultMeta, the resources/read shell, the /state route)
521
+ * MUST compose its envelope as `{ ...siteFields, ...spreadRenderMetaViewOntoSlice(view) }`
522
+ * instead of re-listing view fields: four emitters once re-spread the
523
+ * list by hand, and a field missing from one hand-copy is silently
524
+ * deleted for that transport (the `epoch` freeze-latch signal shipped
525
+ * exactly that way — probe 18). Mode discriminators (`kind`,
526
+ * `codeB64`/`codeUrl`) stay site-local on purpose: their mutual-
527
+ * exclusion logic differs per transport.
528
+ *
529
+ * Array fields are emitted only when non-empty (absent and `[]` are
530
+ * indistinguishable to every consumer's projection) and re-spread into
531
+ * fresh arrays so no emitter aliases the view's internals.
532
+ */
533
+ export declare function spreadRenderMetaViewOntoSlice(view: RenderMetaView | undefined): Pick<McpAppAiGguiRenderMeta, 'epoch' | 'propsJson' | 'contextSlots' | 'permissionsPolicy' | 'theme' | 'gadgets'>;
446
534
  export declare function deriveRenderMeta(item: GguiSession): RenderMetaView;
447
535
  /**
448
536
  * Deps fields powering the `ai.ggui/render` slice envelope, shared by
@@ -520,7 +608,51 @@ export interface RenderSliceMetaDeps {
520
608
  readonly id?: string;
521
609
  readonly mode?: 'light' | 'dark';
522
610
  } | undefined;
611
+ /**
612
+ * H1 ruling 1b — presigner for PRIVATE gadget bundle URLs, applied
613
+ * via {@link rewritePrivateBundleUrls} before the render projection
614
+ * so a private artifact's `bundles/private/…` URLs become
615
+ * short-lived fetchable equivalents (and their origin lands in the
616
+ * CSP allowlist). Injected at the deployment boundary: the hosted
617
+ * pod supplies an object-storage presigner running on its task
618
+ * role; leaving it unset (the OSS default) passes URLs through
619
+ * unchanged — the self-hosted same-origin story needs no presigning.
620
+ *
621
+ * Consumed by the full-envelope projection (`ggui_render`); the
622
+ * `ggui_update` slice subset carries no gadget URLs or CSP, so it
623
+ * never presigns.
624
+ */
625
+ readonly presignPrivateBundleUrl?: (url: string) => Promise<string>;
626
+ /**
627
+ * Absolute public origin the wsToken-gated session API
628
+ * (`/api/sessions/:sessionId/{events,stream}`) is served on. Feeds
629
+ * `composeSessionApiUrls` so the `ai.ggui/render` slice stamps
630
+ * `pollingUrl` + `sseUrl` alongside the minted live trio.
631
+ *
632
+ * Function form (request-aware): mirrors {@link runtimeUrl}'s
633
+ * rationale — behind a tunnel or reverse proxy a static value can't
634
+ * know the public host, so the server passes a getter resolved
635
+ * lazily inside the request scope (X-Forwarded-Host honored for
636
+ * loopback peers). When unset (or the getter returns `undefined`),
637
+ * the assembly falls back to the ws→http origin flip of the minted
638
+ * trio's `wsUrl` — correct whenever the session API is served on
639
+ * the WS origin (OSS defaults, the cloud pod's single ingress).
640
+ * Operators splitting WS and HTTP across hosts MUST set this (or
641
+ * `publicBaseUrl` upstream), or both URLs will point at the
642
+ * WS-only host.
643
+ */
644
+ readonly sessionApiBaseUrl?: string | (() => string | undefined);
523
645
  }
646
+ /**
647
+ * Flip a WebSocket URL's origin to its HTTP twin —
648
+ * `wss://host/ws` → `https://host`, `ws://host/ws` → `http://host`.
649
+ * The fallback base for {@link composeSessionApiUrls} when no
650
+ * explicit `sessionApiBaseUrl` is configured (same derivation the
651
+ * cloud pod uses for its oauth issuerUrl). Total: a malformed
652
+ * `wsUrl` returns `undefined` so slice assembly degrades to
653
+ * no-channel-URLs instead of throwing mid-resultMeta.
654
+ */
655
+ export declare function wsOriginToHttpOrigin(wsUrl: string): string | undefined;
524
656
  /**
525
657
  * Envelope-base fields assembled from {@link RenderSliceMetaDeps} —
526
658
  * the per-call slice plumbing every emitting handler shares.
@@ -533,6 +665,14 @@ export interface RenderSliceBase {
533
665
  * the minter's legacy `token`). Empty object when no minter wired.
534
666
  */
535
667
  readonly authFields: Partial<Pick<McpAppAiGguiRenderMeta, 'wsUrl' | 'wsToken' | 'expiresAt'>>;
668
+ /**
669
+ * Token-bearing HTTP fallback URLs (`pollingUrl` + `sseUrl`),
670
+ * composed via `composeSessionApiUrls`. Stamped ONLY when the
671
+ * minter ran (both URLs embed the token) AND a session-API base
672
+ * resolved; empty object otherwise — the slice then honestly
673
+ * advertises no HTTP fallback.
674
+ */
675
+ readonly channelUrls: Partial<Pick<McpAppAiGguiRenderMeta, 'pollingUrl' | 'sseUrl'>>;
536
676
  /** Resolved theme preset id, when any layer produced one. */
537
677
  readonly themeId?: string;
538
678
  /** Resolved theme mode, when any layer produced one. */