@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.
- package/dist/blueprints/index.d.ts +2 -2
- package/dist/blueprints/index.d.ts.map +1 -1
- package/dist/blueprints/index.js +1 -1
- package/dist/blueprints/search-blueprints.d.ts +64 -12
- package/dist/blueprints/search-blueprints.d.ts.map +1 -1
- package/dist/blueprints/search-blueprints.js +166 -16
- package/dist/renders/amend.d.ts +57 -0
- package/dist/renders/amend.d.ts.map +1 -0
- package/dist/renders/amend.js +64 -0
- package/dist/renders/apply-ggui-session-patch.d.ts +4 -0
- package/dist/renders/apply-ggui-session-patch.d.ts.map +1 -1
- package/dist/renders/apply-ggui-session-patch.js +1 -1
- package/dist/renders/assert-props-contract.d.ts +5 -4
- package/dist/renders/assert-props-contract.d.ts.map +1 -1
- package/dist/renders/assert-props-contract.js +6 -5
- package/dist/renders/code-delivery-events.d.ts +45 -29
- package/dist/renders/code-delivery-events.d.ts.map +1 -1
- package/dist/renders/code-delivery-events.js +42 -25
- package/dist/renders/consume.js +1 -1
- package/dist/renders/decide-handshake.d.ts +15 -0
- package/dist/renders/decide-handshake.d.ts.map +1 -1
- package/dist/renders/decide-handshake.js +18 -0
- package/dist/renders/declare-tool-catalog.js +1 -1
- package/dist/renders/handshake.d.ts.map +1 -1
- package/dist/renders/handshake.js +58 -12
- package/dist/renders/index.d.ts +5 -1
- package/dist/renders/index.d.ts.map +1 -1
- package/dist/renders/index.js +5 -1
- package/dist/renders/props-mutation-core.d.ts +60 -0
- package/dist/renders/props-mutation-core.d.ts.map +1 -0
- package/dist/renders/props-mutation-core.js +339 -0
- package/dist/renders/render.d.ts +2 -0
- package/dist/renders/render.d.ts.map +1 -1
- package/dist/renders/render.js +61 -32
- package/dist/renders/runtime-pull.d.ts +122 -0
- package/dist/renders/runtime-pull.d.ts.map +1 -0
- package/dist/renders/runtime-pull.js +178 -0
- package/dist/renders/runtime-telemetry.d.ts +60 -0
- package/dist/renders/runtime-telemetry.d.ts.map +1 -0
- package/dist/renders/runtime-telemetry.js +76 -0
- package/dist/renders/slice-meta-derivation.d.ts +146 -6
- package/dist/renders/slice-meta-derivation.d.ts.map +1 -1
- package/dist/renders/slice-meta-derivation.js +190 -8
- package/dist/renders/submit-action.d.ts +9 -0
- package/dist/renders/submit-action.d.ts.map +1 -1
- package/dist/renders/submit-action.js +70 -1
- package/dist/renders/update.d.ts +33 -42
- package/dist/renders/update.d.ts.map +1 -1
- package/dist/renders/update.js +99 -230
- 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
|
-
*
|
|
53
|
-
*
|
|
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
|
|
441
|
-
*
|
|
442
|
-
*
|
|
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. */
|