@ggui-ai/mcp-server 0.1.0-rc.3 → 0.2.0-alpha.1
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/README.md +1 -1
- package/dist/build-mcp.d.ts +8 -8
- package/dist/build-mcp.d.ts.map +1 -1
- package/dist/build-mcp.js +53 -12
- package/dist/console-auth.d.ts +7 -7
- package/dist/console-auth.d.ts.map +1 -1
- package/dist/console-auth.js +4 -4
- package/dist/console-cache.d.ts +2 -2
- package/dist/console-cache.d.ts.map +1 -1
- package/dist/console-cache.js +10 -10
- package/dist/console-headers.d.ts +2 -2
- package/dist/console-headers.js +2 -2
- package/dist/console-payloads.d.ts +3 -3
- package/dist/console-payloads.d.ts.map +1 -1
- package/dist/console-payloads.js +10 -10
- package/dist/console-timeline.d.ts +12 -30
- package/dist/console-timeline.d.ts.map +1 -1
- package/dist/console-timeline.js +44 -45
- package/dist/index.d.ts +7 -7
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -5
- package/dist/instructions-presets.d.ts +1 -1
- package/dist/instructions-presets.d.ts.map +1 -1
- package/dist/instructions-presets.js +40 -22
- package/dist/llm-backed-negotiator.d.ts +6 -6
- package/dist/llm-backed-negotiator.d.ts.map +1 -1
- package/dist/llm-backed-negotiator.js +92 -91
- package/dist/mcp-apps-inbound.d.ts +3 -3
- package/dist/mcp-apps-inbound.d.ts.map +1 -1
- package/dist/mcp-apps-inbound.js +28 -23
- package/dist/mcp-apps-outbound.d.ts +136 -121
- package/dist/mcp-apps-outbound.d.ts.map +1 -1
- package/dist/mcp-apps-outbound.js +384 -414
- package/dist/mcp-mounts.d.ts +21 -20
- package/dist/mcp-mounts.d.ts.map +1 -1
- package/dist/mcp-mounts.js +25 -29
- package/dist/{session-channel.d.ts → render-channel.d.ts} +145 -102
- package/dist/render-channel.d.ts.map +1 -0
- package/dist/{session-channel.js → render-channel.js} +481 -462
- package/dist/schema-compat.d.ts +11 -11
- package/dist/schema-compat.d.ts.map +1 -1
- package/dist/schema-compat.js +6 -6
- package/dist/server.d.ts +224 -219
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +1498 -1963
- package/dist/storage.d.ts +7 -7
- package/dist/storage.d.ts.map +1 -1
- package/dist/storage.js +12 -12
- package/package.json +12 -11
- package/dist/render-gate.d.ts +0 -87
- package/dist/render-gate.d.ts.map +0 -1
- package/dist/render-gate.js +0 -77
- package/dist/render-rate-limit.d.ts +0 -59
- package/dist/render-rate-limit.d.ts.map +0 -1
- package/dist/render-rate-limit.js +0 -73
- package/dist/render-signing.d.ts +0 -98
- package/dist/render-signing.d.ts.map +0 -1
- package/dist/render-signing.js +0 -113
- package/dist/session-channel.d.ts.map +0 -1
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* The live channel is where the typed-channel contract is enforced on
|
|
5
5
|
* live traffic between the server and the user. It co-hosts on the
|
|
6
6
|
* same Express server as `/mcp` and reuses the same
|
|
7
|
-
* `@ggui-ai/mcp-server-handlers/
|
|
7
|
+
* `@ggui-ai/mcp-server-handlers/renders` helpers that the
|
|
8
8
|
* closed hosted server consumes.
|
|
9
9
|
*
|
|
10
10
|
* Scope:
|
|
@@ -14,15 +14,15 @@
|
|
|
14
14
|
* - `action` → inbound user action carried as an {@link ActionEnvelope}.
|
|
15
15
|
* Gated through `assertEventAllowed` (allowlist) +
|
|
16
16
|
* `assertActionContract` (payload, for data:submit). Persisted to
|
|
17
|
-
*
|
|
17
|
+
* RenderStore as a typed session event.
|
|
18
18
|
* - `ping`/`pong` → heartbeat parity with hosted.
|
|
19
19
|
* - `close`/socket-close → clean subscriber teardown.
|
|
20
|
-
* - `sendToSession(
|
|
20
|
+
* - `sendToSession(renderId, data)` → outbound fan-out API for
|
|
21
21
|
* mutation handlers (ggui_emit / connector `ctx.send`). Validated
|
|
22
22
|
* through `assertStreamContract` before delivery.
|
|
23
23
|
*
|
|
24
24
|
* `props_update`: mount handlers dispatched through the wired-action
|
|
25
|
-
* router can call `ctx.sendPropsUpdate(
|
|
25
|
+
* router can call `ctx.sendPropsUpdate(renderId, props)` to fan a
|
|
26
26
|
* `{type:'props_update'}` frame to live subscribers without going
|
|
27
27
|
* through a refresh-stream path. Reaches the renderer's existing
|
|
28
28
|
* `props_update` branch in `iframe-runtime` and applies new props
|
|
@@ -32,26 +32,27 @@
|
|
|
32
32
|
* Not handled here:
|
|
33
33
|
*
|
|
34
34
|
* - Pattern-B daemon-agent notification stream (GET /mcp SSE).
|
|
35
|
-
* - Short-lived session-token mint/consume — that's
|
|
35
|
+
* - Short-lived session-token mint/consume — that's ggui_render-gated.
|
|
36
36
|
* Dev-mode auth (any bearer via existing AuthAdapter) matches the
|
|
37
37
|
* `/mcp` endpoint's shape and is operator-replaceable.
|
|
38
38
|
*/
|
|
39
|
-
import type {
|
|
40
|
-
import type {
|
|
41
|
-
import type {
|
|
42
|
-
import type {
|
|
43
|
-
import type {
|
|
39
|
+
import type { AuthAdapter, RenderStore, SessionStreamBuffer, StreamEnvelopeInput, StreamFanout, TelemetrySink } from "@ggui-ai/mcp-server-core";
|
|
40
|
+
import type { JsonObject, Render, ReservedChannelValidator, SanitizeCausedBy } from "@ggui-ai/protocol";
|
|
41
|
+
import type { WebSocketMessage } from "@ggui-ai/protocol/transport/websocket";
|
|
42
|
+
import type { IncomingMessage } from "node:http";
|
|
43
|
+
import type { Duplex } from "node:stream";
|
|
44
|
+
import type { Logger } from "./logger.js";
|
|
44
45
|
/** Default URL path for the channel endpoint. Operators can override. */
|
|
45
|
-
export declare const
|
|
46
|
+
export declare const DEFAULT_RENDER_CHANNEL_PATH = "/ws";
|
|
46
47
|
/**
|
|
47
48
|
* Opt-in plumbing for the `channel_subscribe` polling loop. When this
|
|
48
|
-
* field is set on {@link
|
|
49
|
+
* field is set on {@link RenderChannelOptions}, channel subscribes
|
|
49
50
|
* whose `source.tool` is in {@link allowlist} are accepted and the
|
|
50
51
|
* server begins polling. When absent, every `channel_subscribe`
|
|
51
52
|
* returns `CHANNEL_NOT_LOCAL` so the iframe falls back to direct
|
|
52
53
|
* polling via the MCP host proxy.
|
|
53
54
|
*/
|
|
54
|
-
export interface
|
|
55
|
+
export interface RenderChannelLocalToolsOptions {
|
|
55
56
|
/**
|
|
56
57
|
* Whitelist of `source.tool` names this channel can poll. Must mirror
|
|
57
58
|
* the value the host advertises on
|
|
@@ -92,13 +93,13 @@ export interface SessionChannelLocalToolsOptions {
|
|
|
92
93
|
* message (`SubscribePayload.bootstrap`). When present:
|
|
93
94
|
*
|
|
94
95
|
* 1. `verify(token)` is called. Must return the bound
|
|
95
|
-
* `{
|
|
96
|
+
* `{renderId, appId}` on success, or `null` on any failure
|
|
96
97
|
* (invalid sig, expired, wrong kind, replayed, etc.).
|
|
97
|
-
* 2. The bound `
|
|
98
|
+
* 2. The bound `renderId` MUST match the one on the subscribe
|
|
98
99
|
* payload. Mismatches are rejected with a clean error.
|
|
99
100
|
* 3. On success, the server mints a reconnect credential via
|
|
100
|
-
* `issueSessionToken(
|
|
101
|
-
* `AckPayload.
|
|
101
|
+
* `issueSessionToken(renderId, appId)` and returns it in
|
|
102
|
+
* `AckPayload.renderToken`. The iframe stores this for WS
|
|
102
103
|
* reconnects via the normal bearer path.
|
|
103
104
|
*
|
|
104
105
|
* Bootstrap auth is MUTUALLY EXCLUSIVE with the upstream `AuthAdapter`
|
|
@@ -118,16 +119,16 @@ export interface SessionChannelLocalToolsOptions {
|
|
|
118
119
|
* handshake. Past expiry, the iframe MAY refresh via the
|
|
119
120
|
* {@link refresh} surface; past the refresh window, fresh handshake.
|
|
120
121
|
*/
|
|
121
|
-
export type
|
|
122
|
+
export type RenderChannelBootstrapVerifyResult = {
|
|
122
123
|
readonly ok: true;
|
|
123
|
-
readonly
|
|
124
|
+
readonly renderId: string;
|
|
124
125
|
readonly appId: string;
|
|
125
126
|
} | {
|
|
126
127
|
readonly ok: false;
|
|
127
|
-
readonly reason:
|
|
128
|
+
readonly reason: "expired" | "invalid";
|
|
128
129
|
};
|
|
129
130
|
/**
|
|
130
|
-
* Result of {@link
|
|
131
|
+
* Result of {@link RenderChannelBootstrap.refresh}.
|
|
131
132
|
*
|
|
132
133
|
* - `ok: true`: caller swaps the old envelope for `token` and resumes.
|
|
133
134
|
* - `ok: false`: caller MUST re-handshake (refresh window closed,
|
|
@@ -139,9 +140,9 @@ export type SessionChannelBootstrapRefreshResult = {
|
|
|
139
140
|
readonly expiresAt: string;
|
|
140
141
|
} | {
|
|
141
142
|
readonly ok: false;
|
|
142
|
-
readonly reason:
|
|
143
|
+
readonly reason: "window_closed" | "invalid";
|
|
143
144
|
};
|
|
144
|
-
export interface
|
|
145
|
+
export interface RenderChannelBootstrap {
|
|
145
146
|
/**
|
|
146
147
|
* Verify a `SubscribePayload.bootstrap` token.
|
|
147
148
|
*
|
|
@@ -151,23 +152,23 @@ export interface SessionChannelBootstrap {
|
|
|
151
152
|
* `BOOTSTRAP_INVALID` for tamper / format / kind failures (no
|
|
152
153
|
* refresh on those).
|
|
153
154
|
*/
|
|
154
|
-
verify(token: string):
|
|
155
|
+
verify(token: string): RenderChannelBootstrapVerifyResult;
|
|
155
156
|
/**
|
|
156
157
|
* Mint a longer-lived reconnect credential to return in
|
|
157
|
-
* `AckPayload.
|
|
158
|
+
* `AckPayload.renderToken`. Called only after a successful
|
|
158
159
|
* `verify()` on a bootstrap subscribe.
|
|
159
160
|
*/
|
|
160
|
-
issueSessionToken(
|
|
161
|
+
issueSessionToken(renderId: string, appId: string): string;
|
|
161
162
|
/**
|
|
162
163
|
* Refresh a (possibly-expired-but-signature-valid) bootstrap envelope
|
|
163
164
|
* into a new envelope with a fresh TTL. Used by the
|
|
164
165
|
* `ggui_runtime_refresh_bootstrap` MCP tool — iframes that see their
|
|
165
166
|
* bootstrap drift out of the TTL window swap in the refreshed
|
|
166
|
-
* envelope without going back through `
|
|
167
|
+
* envelope without going back through `ggui_render`.
|
|
167
168
|
*
|
|
168
169
|
* Stateless: verifies HMAC against the same secret used at mint,
|
|
169
170
|
* checks the refresh window against the ORIGINAL `iat`, and mints
|
|
170
|
-
* a fresh bootstrap envelope bound to the SAME `(
|
|
171
|
+
* a fresh bootstrap envelope bound to the SAME `(renderId, appId)`.
|
|
171
172
|
* Past the refresh window the result is `{ok:false, reason:
|
|
172
173
|
* 'window_closed'}`; tampered envelopes are `{ok:false, reason:
|
|
173
174
|
* 'invalid'}`.
|
|
@@ -176,7 +177,7 @@ export interface SessionChannelBootstrap {
|
|
|
176
177
|
}
|
|
177
178
|
/**
|
|
178
179
|
* Default timeout for a single wired-tool invocation, in ms. Operators
|
|
179
|
-
* override via {@link
|
|
180
|
+
* override via {@link RenderChannelOptions.wiredActionTimeoutMs}; the
|
|
180
181
|
* 30 s ceiling is a honest non-promise: long-running tools MUST design
|
|
181
182
|
* their own completion path (streaming, polling).
|
|
182
183
|
*/
|
|
@@ -221,7 +222,7 @@ export declare const DEFAULT_WIRED_TOOL_TIMEOUT_MS = 30000;
|
|
|
221
222
|
* shape every shared handler — ggui-native AND mounted — accepts. It
|
|
222
223
|
* stays narrow on purpose (`appId`, `requestId`, optional `apiKeyHash`)
|
|
223
224
|
* so the surface a host implements is stable. Wired-action runtime
|
|
224
|
-
* fields (`
|
|
225
|
+
* fields (`renderId`, `renderId`, `sendPropsUpdate`) are dispatcher-
|
|
225
226
|
* specific — only mount tools invoked through the wired-action router
|
|
226
227
|
* see them, only at dispatch time. Passing them as a third arg to
|
|
227
228
|
* `invoke` keeps the canonical handler shape untouched and makes
|
|
@@ -230,32 +231,24 @@ export declare const DEFAULT_WIRED_TOOL_TIMEOUT_MS = 30000;
|
|
|
230
231
|
* The composer in `mcp-mounts.ts::composeWiredActionRouterFromMounts`
|
|
231
232
|
* synthesizes a runtime ctx for the mount handler that satisfies
|
|
232
233
|
* `HandlerContext` AND structurally carries these wired fields, so a
|
|
233
|
-
* mount fixture can read `ctx.sendPropsUpdate` / `ctx.
|
|
234
|
+
* mount fixture can read `ctx.sendPropsUpdate` / `ctx.renderId` from the
|
|
234
235
|
* same `ctx` argument the canonical `HandlerContext` sig types — no
|
|
235
236
|
* cast, no widening of the static type.
|
|
236
237
|
*/
|
|
237
238
|
export interface WiredActionContext {
|
|
238
|
-
/** The
|
|
239
|
-
* subscriber + the action envelope's spoof-guarded `
|
|
240
|
-
readonly
|
|
241
|
-
/**
|
|
242
|
-
*
|
|
243
|
-
*
|
|
244
|
-
*
|
|
245
|
-
|
|
246
|
-
/**
|
|
247
|
-
* Push a `{type:'props_update', payload:{stackItemId, props}}` frame to
|
|
248
|
-
* every live subscriber bound to this dispatcher's `sessionId`. The
|
|
249
|
-
* call closes over the `SessionChannelServer.sendPropsUpdate` method,
|
|
250
|
-
* scoped to the active session for safety — even if a buggy mount
|
|
251
|
-
* picks a `stackItemId` that doesn't exist on the session, the channel
|
|
252
|
-
* server's own validation no-ops the fan-out (same posture as
|
|
253
|
-
* `notifyStackPush` orphan handling).
|
|
239
|
+
/** The render this dispatch is bound to. Sourced from the live
|
|
240
|
+
* subscriber + the action envelope's spoof-guarded `renderId`. */
|
|
241
|
+
readonly renderId: string;
|
|
242
|
+
/**
|
|
243
|
+
* Push a `{type:'props_update', payload:{renderId, props}}` frame to
|
|
244
|
+
* every live subscriber bound to this dispatcher's `renderId`. The
|
|
245
|
+
* call closes over the `RenderChannelServer.sendPropsUpdate` method,
|
|
246
|
+
* scoped to the active render for safety.
|
|
254
247
|
*
|
|
255
248
|
* Best-effort: per-subscriber send failures are swallowed; a closed
|
|
256
249
|
* socket is a no-op.
|
|
257
250
|
*/
|
|
258
|
-
sendPropsUpdate(
|
|
251
|
+
sendPropsUpdate(props: JsonObject): void;
|
|
259
252
|
}
|
|
260
253
|
export interface WiredActionRouter {
|
|
261
254
|
/** Returns `true` when the named tool has a registered handler. Used
|
|
@@ -278,9 +271,9 @@ export interface WiredActionRouter {
|
|
|
278
271
|
*/
|
|
279
272
|
invoke(toolName: string, input: Record<string, unknown>, ctx: WiredActionContext): Promise<unknown>;
|
|
280
273
|
}
|
|
281
|
-
export interface
|
|
282
|
-
/** Required — the
|
|
283
|
-
readonly
|
|
274
|
+
export interface RenderChannelOptions {
|
|
275
|
+
/** Required — the render backing store (typically `InMemoryRenderStore`). */
|
|
276
|
+
readonly renderStore: RenderStore;
|
|
284
277
|
/**
|
|
285
278
|
* Required — the same `AuthAdapter` the `/mcp` endpoint uses. Any
|
|
286
279
|
* failure during `subscribe` rejects the upgrade with HTTP 401.
|
|
@@ -319,16 +312,16 @@ export interface SessionChannelOptions {
|
|
|
319
312
|
/**
|
|
320
313
|
* Optional bootstrap-auth plumbing. When present, the channel
|
|
321
314
|
* accepts `SubscribePayload.bootstrap` and issues reconnect
|
|
322
|
-
* credentials in `AckPayload.
|
|
315
|
+
* credentials in `AckPayload.renderToken`. When absent, bootstrap
|
|
323
316
|
* tokens are rejected with `BOOTSTRAP_NOT_SUPPORTED`.
|
|
324
317
|
*/
|
|
325
|
-
readonly bootstrap?:
|
|
318
|
+
readonly bootstrap?: RenderChannelBootstrap;
|
|
326
319
|
/**
|
|
327
320
|
* Optional console cookie-auth plumbing. When present, the
|
|
328
321
|
* channel upgrade looks for the configured cookie on the incoming
|
|
329
322
|
* request. A valid cookie binds the identity as a `builder` and
|
|
330
|
-
* scopes the subscriber to the cookie's `
|
|
331
|
-
* `subscribe.
|
|
323
|
+
* scopes the subscriber to the cookie's `renderId` — any
|
|
324
|
+
* `subscribe.renderId` mismatch is rejected with
|
|
332
325
|
* `DEVTOOL_COOKIE_SESSION_MISMATCH`.
|
|
333
326
|
*
|
|
334
327
|
* Absent = cookie auth disabled on this channel. Cookies are never
|
|
@@ -339,7 +332,7 @@ export interface SessionChannelOptions {
|
|
|
339
332
|
* bootstrap path (via `?bootstrap=` query) wins; cookie is only
|
|
340
333
|
* consulted for standard upgrades.
|
|
341
334
|
*/
|
|
342
|
-
readonly cookieAuth?:
|
|
335
|
+
readonly cookieAuth?: RenderChannelCookieAuth;
|
|
343
336
|
/**
|
|
344
337
|
* Opt-in wired-action dispatch router. When present, validated
|
|
345
338
|
* `data:submit` envelopes whose declared `actionSpec[name]
|
|
@@ -373,7 +366,7 @@ export interface SessionChannelOptions {
|
|
|
373
366
|
*
|
|
374
367
|
* The contract-error envelope flows on `_ggui:contract-error` with
|
|
375
368
|
* `replay: 'all'`, so anything that lands in `causedBy` persists in
|
|
376
|
-
* the session ring buffer and surfaces in
|
|
369
|
+
* the session ring buffer and surfaces in RenderInspector. Accepting
|
|
377
370
|
* raw `err.stack` verbatim is a credential-leak footgun; the default
|
|
378
371
|
* sanitizer is load-bearing.
|
|
379
372
|
*/
|
|
@@ -422,7 +415,7 @@ export interface SessionChannelOptions {
|
|
|
422
415
|
* `handshake.serverCapabilities.streamWebSocketLocalTools` so iframe
|
|
423
416
|
* + server agree on which channels use the WS fan-out path.
|
|
424
417
|
*/
|
|
425
|
-
readonly streamWebSocketLocalTools?:
|
|
418
|
+
readonly streamWebSocketLocalTools?: RenderChannelLocalToolsOptions;
|
|
426
419
|
/**
|
|
427
420
|
* Protocol-version handshake policy. Governs server behavior when a
|
|
428
421
|
* subscribe declares a `supportedVersions` list that does NOT
|
|
@@ -448,31 +441,63 @@ export interface SessionChannelOptions {
|
|
|
448
441
|
* not a schema change — the wire fields and error code ship
|
|
449
442
|
* identically in both modes.
|
|
450
443
|
*/
|
|
451
|
-
readonly versionPolicy?:
|
|
444
|
+
readonly versionPolicy?: "advisory" | "reject";
|
|
445
|
+
/**
|
|
446
|
+
* Optional hook fired synchronously when the local subscriber count
|
|
447
|
+
* for `renderId` transitions 0 → 1 (the first subscriber for that
|
|
448
|
+
* session connects to this server instance).
|
|
449
|
+
*
|
|
450
|
+
* Hosted deployments use this to lazily SUBSCRIBE to the per-session
|
|
451
|
+
* cross-pod broadcast channel (e.g. Redis pub/sub); OSS has no use
|
|
452
|
+
* for it (in-process broadcasts already route via
|
|
453
|
+
* {@link RenderChannelServer.sendPropsUpdate}). Bounding pubsub
|
|
454
|
+
* fan-in to only sessions a pod actually holds connections for is a
|
|
455
|
+
* correctness requirement, not an optimization — without it every
|
|
456
|
+
* pod receives every other pod's broadcast for every active session.
|
|
457
|
+
*
|
|
458
|
+
* Best-effort: a thrown callback is logged and swallowed.
|
|
459
|
+
* `register()` MUST NOT fail because of a hook error or the
|
|
460
|
+
* `wsSubscribers` set would drift out of sync with the real socket
|
|
461
|
+
* lifecycle.
|
|
462
|
+
*
|
|
463
|
+
* Concurrent register/unregister for the same renderId are serialized
|
|
464
|
+
* by the channel's single-threaded WS event loop; hook implementations
|
|
465
|
+
* do not need their own mutex for the 0↔1 transition.
|
|
466
|
+
*/
|
|
467
|
+
readonly onFirstSubscriber?: (renderId: string) => void;
|
|
468
|
+
/**
|
|
469
|
+
* Optional hook fired synchronously when the local subscriber count
|
|
470
|
+
* for `renderId` transitions 1 → 0 (the last subscriber for that
|
|
471
|
+
* session disconnects).
|
|
472
|
+
*
|
|
473
|
+
* Symmetric with {@link onFirstSubscriber}; same best-effort posture
|
|
474
|
+
* and single-threaded serialization guarantee.
|
|
475
|
+
*/
|
|
476
|
+
readonly onLastSubscriberGone?: (renderId: string) => void;
|
|
452
477
|
}
|
|
453
478
|
/**
|
|
454
479
|
* Cookie-based authentication for the live-channel upgrade. Used
|
|
455
480
|
* exclusively by the same-origin console viewer; see
|
|
456
481
|
* `console-auth.ts` for the single consumer today.
|
|
457
482
|
*/
|
|
458
|
-
export interface
|
|
483
|
+
export interface RenderChannelCookieAuth {
|
|
459
484
|
/**
|
|
460
485
|
* Read the raw cookie value for THIS server's console cookie
|
|
461
486
|
* from the incoming request headers. Returns `null` when the
|
|
462
487
|
* cookie is absent or malformed.
|
|
463
488
|
*/
|
|
464
|
-
readCookie(headers: import(
|
|
489
|
+
readCookie(headers: import("node:http").IncomingHttpHeaders): string | null;
|
|
465
490
|
/**
|
|
466
491
|
* Verify a cookie value and return the bound session/app. Returns
|
|
467
492
|
* `null` on any failure (signature, expiry, wrong kind). Never
|
|
468
493
|
* throws.
|
|
469
494
|
*/
|
|
470
495
|
verify(cookieValue: string): {
|
|
471
|
-
|
|
496
|
+
renderId: string;
|
|
472
497
|
appId: string;
|
|
473
498
|
} | null;
|
|
474
499
|
}
|
|
475
|
-
export interface
|
|
500
|
+
export interface RenderChannelServer {
|
|
476
501
|
/** The URL path the channel accepts upgrade requests on. */
|
|
477
502
|
readonly path: string;
|
|
478
503
|
/**
|
|
@@ -482,7 +507,7 @@ export interface SessionChannelServer {
|
|
|
482
507
|
*/
|
|
483
508
|
handleUpgrade(req: IncomingMessage, socket: Duplex, head: Buffer): void;
|
|
484
509
|
/**
|
|
485
|
-
* Deliver a stream envelope to every subscriber of `delivery.
|
|
510
|
+
* Deliver a stream envelope to every subscriber of `delivery.renderId`.
|
|
486
511
|
*
|
|
487
512
|
* Outbound fan-out enforcement point — the delivery's `payload` is
|
|
488
513
|
* validated against the active stack item's streamSpec via
|
|
@@ -513,44 +538,44 @@ export interface SessionChannelServer {
|
|
|
513
538
|
seq: number;
|
|
514
539
|
}>;
|
|
515
540
|
/**
|
|
516
|
-
* Fan a `{type:'
|
|
517
|
-
* to every subscriber currently bound to `
|
|
518
|
-
* notify already-subscribed clients about a
|
|
519
|
-
* happened AFTER they subscribed — the initial `ack.
|
|
541
|
+
* Fan a `{type:'render', payload:{render, matchType?}}` wire frame
|
|
542
|
+
* to every subscriber currently bound to `renderId`. Use this to
|
|
543
|
+
* notify already-subscribed clients about a render-commit that
|
|
544
|
+
* happened AFTER they subscribed — the initial `ack.render` snapshot
|
|
520
545
|
* covers state at subscribe time only, so without an explicit notify
|
|
521
|
-
* a second-turn `
|
|
546
|
+
* a second-turn `commit` is invisible to the live client.
|
|
522
547
|
*
|
|
523
548
|
* The `B1` regression context (2026-04-22 QA pass): the chat surface
|
|
524
|
-
* in `/chat` reuses one
|
|
525
|
-
* subscribed AFTER `
|
|
526
|
-
* entry. The second turn's
|
|
549
|
+
* in `/chat` reuses one render across turns. The first turn's render
|
|
550
|
+
* subscribed AFTER `commit` ran, so the ack carried the
|
|
551
|
+
* entry. The second turn's commit landed on the live render — the
|
|
527
552
|
* subscriber never heard about the new entry, the inline UI slot
|
|
528
|
-
* stayed in "Waiting for
|
|
529
|
-
* `
|
|
553
|
+
* stayed in "Waiting for render channel replay…" indefinitely.
|
|
554
|
+
* `notifyRenderPush` closes that gap. Best-effort: per-subscriber
|
|
530
555
|
* send failures are swallowed (same posture as `sendToSession`).
|
|
531
556
|
*
|
|
532
557
|
* NOT durable. Frames are not stamped through the replay buffer —
|
|
533
|
-
* fresh subscribers still get the current
|
|
534
|
-
* subscribe. A new tab opening mid-
|
|
558
|
+
* fresh subscribers still get the current render via `ack.render` on
|
|
559
|
+
* subscribe. A new tab opening mid-render reads the latest render
|
|
535
560
|
* from the snapshot; live tabs read the delta from this notify.
|
|
536
561
|
*
|
|
537
562
|
* Subscribers received via `register()` are tracked in
|
|
538
563
|
* `subscribersBySession`; this helper iterates the bound set and
|
|
539
564
|
* skips closed sockets via the `send()` helper's existing guard.
|
|
540
565
|
* Callers ARE responsible for ordering — call after the underlying
|
|
541
|
-
* `
|
|
566
|
+
* `renderStore.commit` resolves so the snapshot a
|
|
542
567
|
* concurrent fresh subscriber observes still includes the entry.
|
|
543
568
|
*/
|
|
544
|
-
|
|
569
|
+
notifyRenderPush(renderId: string, render: Render, matchType?: string): void;
|
|
545
570
|
/**
|
|
546
|
-
* Prime every declared streamSpec channel on `
|
|
571
|
+
* Prime every declared streamSpec channel on `render` that carries a
|
|
547
572
|
* `tool` refresh hint. Invokes each refresh tool via the bound
|
|
548
573
|
* wiredActionRouter with the empty refresh input, validates the result
|
|
549
574
|
* against the channel's schema, and fans it out so subscribers see an
|
|
550
575
|
* initial value instead of `latest = undefined`.
|
|
551
576
|
*
|
|
552
|
-
* Intended for seed-a-
|
|
553
|
-
* initiated bootstraps that
|
|
577
|
+
* Intended for seed-a-render mount paths (console try-live, agent-
|
|
578
|
+
* initiated bootstraps that mint a render without a driving
|
|
554
579
|
* action). Without this, blueprints with `streamSpec[channel].tool`
|
|
555
580
|
* render their empty-state branch because the channel has no live
|
|
556
581
|
* delivery — operators see "waiting for data" UI even when the
|
|
@@ -559,41 +584,41 @@ export interface SessionChannelServer {
|
|
|
559
584
|
* Same isolation posture as the action-driven refresh pass: one
|
|
560
585
|
* broken refresh MUST NOT block others; per-channel failures log a
|
|
561
586
|
* warning but don't throw. No-op when no wiredActionRouter is
|
|
562
|
-
* configured, when `
|
|
587
|
+
* configured, when `render.streamSpec` is absent, or when every
|
|
563
588
|
* channel lacks a `.tool` hint.
|
|
564
589
|
*
|
|
565
|
-
* Ordering: callers should invoke AFTER the
|
|
566
|
-
* so `sendToSession`'s active-
|
|
590
|
+
* Ordering: callers should invoke AFTER the render is persisted
|
|
591
|
+
* so `sendToSession`'s active-render lookup resolves. The
|
|
567
592
|
* `try-live` endpoint awaits this before returning the shortCode so
|
|
568
593
|
* the viewer SPA subscribes with the initial envelope already
|
|
569
|
-
* buffered on the
|
|
594
|
+
* buffered on the render's stream-buffer replay state.
|
|
570
595
|
*/
|
|
571
|
-
primeStreams(
|
|
596
|
+
primeStreams(renderId: string, render: Render): Promise<void>;
|
|
572
597
|
/**
|
|
573
|
-
* Fan a `{type:'props_update', payload:{
|
|
574
|
-
* to every subscriber currently bound to `
|
|
598
|
+
* Fan a `{type:'props_update', payload:{renderId, props}}` wire frame
|
|
599
|
+
* to every subscriber currently bound to `renderId`. Mount tools
|
|
575
600
|
* dispatched through {@link WiredActionRouter} call this via
|
|
576
601
|
* `WiredActionContext.sendPropsUpdate` so a wired action that
|
|
577
602
|
* mutates server-side state can replace renderer props in-place
|
|
578
603
|
* without going through a refresh-stream tool.
|
|
579
604
|
*
|
|
580
|
-
* Validation posture (mirrors `
|
|
605
|
+
* Validation posture (mirrors `notifyRenderPush`'s "best-effort orphan
|
|
581
606
|
* no-op"):
|
|
582
|
-
* 1. Look up the session via `
|
|
607
|
+
* 1. Look up the session via `renderStore.get`. Absent → log
|
|
583
608
|
* `session_channel_props_update_orphan` and return — the wire
|
|
584
609
|
* validator on the renderer side would reject a frame for an
|
|
585
610
|
* unknown session anyway.
|
|
586
|
-
* 2. Look up the target stack entry by `
|
|
611
|
+
* 2. Look up the target stack entry by `renderId` in the loaded
|
|
587
612
|
* session's stack. Absent → log
|
|
588
613
|
* `session_channel_props_update_pageid_unknown` and return.
|
|
589
614
|
* 3. Iterate the flat WS-subscriber set, filter to subscribers
|
|
590
|
-
* whose `
|
|
615
|
+
* whose `renderId` matches, and `send()` the frame. Closed
|
|
591
616
|
* sockets are skipped silently by `send()`.
|
|
592
617
|
*
|
|
593
618
|
* NOT routed through StreamFanout — `type: 'props_update'` is a
|
|
594
619
|
* distinct WebSocket message type, not a stream envelope. Stream
|
|
595
620
|
* envelopes flow on `data` frames and have a `seq` cursor; props
|
|
596
|
-
* updates are ephemeral and follow `
|
|
621
|
+
* updates are ephemeral and follow `notifyRenderPush`'s pattern
|
|
597
622
|
* (live-only, no replay-buffer stamping). A new subscriber that
|
|
598
623
|
* connects mid-session reads current `props` from the stack
|
|
599
624
|
* snapshot delivered in `ack.stack`.
|
|
@@ -601,17 +626,17 @@ export interface SessionChannelServer {
|
|
|
601
626
|
* Schema validation against `propsSpec`: NOT enforced server-side
|
|
602
627
|
* here. The renderer validates inbound props via
|
|
603
628
|
* `validateInboundPropsPayload` against the cached
|
|
604
|
-
* `
|
|
629
|
+
* `render.propsSpec` before applying — defense-in-depth at the
|
|
605
630
|
* receiving boundary. Server-side enforcement is reserved for the
|
|
606
631
|
* future agent-driven `ggui_update` path; the mount-tool seam is
|
|
607
632
|
* trusted-runtime today (mounts execute in-process, same trust
|
|
608
633
|
* boundary as ggui-native handlers).
|
|
609
634
|
*/
|
|
610
|
-
sendPropsUpdate(
|
|
635
|
+
sendPropsUpdate(renderId: string, props: JsonObject): Promise<void>;
|
|
611
636
|
/**
|
|
612
|
-
* Fan a `{type:'drain_ack', payload:{
|
|
637
|
+
* Fan a `{type:'drain_ack', payload:{renderId, appId, renderId,
|
|
613
638
|
* eventId, drainedAt}}` wire frame to every subscriber currently
|
|
614
|
-
* bound to `
|
|
639
|
+
* bound to `renderId`.
|
|
615
640
|
*
|
|
616
641
|
* Fired by `createGguiConsumeHandler` once per drained
|
|
617
642
|
* `PendingEvent` so the iframe-runtime can cancel the matching
|
|
@@ -619,19 +644,37 @@ export interface SessionChannelServer {
|
|
|
619
644
|
* Implements the `DrainAckNotifier` contract from
|
|
620
645
|
* `@ggui-ai/mcp-server-handlers`.
|
|
621
646
|
*
|
|
622
|
-
* Same posture as `
|
|
647
|
+
* Same posture as `notifyRenderPush` / `sendPropsUpdate` — live-only,
|
|
623
648
|
* no replay-buffer stamping. Subscribers that connect AFTER the
|
|
624
649
|
* drain see the next consume's snapshot rather than the missed
|
|
625
650
|
* frame; the iframe's claim timer + atomic-pop primitive backstop
|
|
626
651
|
* any frame loss.
|
|
627
652
|
*/
|
|
628
653
|
sendDrainAck(args: {
|
|
629
|
-
readonly
|
|
654
|
+
readonly renderId: string;
|
|
630
655
|
readonly appId: string;
|
|
631
|
-
readonly stackItemId: string;
|
|
632
656
|
readonly eventId: string;
|
|
633
657
|
readonly drainedAt: string;
|
|
634
658
|
}): void;
|
|
659
|
+
/**
|
|
660
|
+
* Fan a server-frame to every local WS subscriber bound to
|
|
661
|
+
* `renderId`. Skips replay-buffer stamping, RenderStore lookups,
|
|
662
|
+
* and contract validation — the caller is the one that originally
|
|
663
|
+
* validated + persisted the underlying mutation. This surface is the
|
|
664
|
+
* cloud adapter's path for delivering already-validated frames that
|
|
665
|
+
* arrived via an external pubsub layer (Redis from another pod).
|
|
666
|
+
*
|
|
667
|
+
* Internal adapter use only. NOT part of the published ggui
|
|
668
|
+
* protocol, NOT stable across versions, NOT exposed to MCP / wire
|
|
669
|
+
* callers. The publisher is responsible for ensuring `frame` is
|
|
670
|
+
* wire-valid; this method does not re-validate.
|
|
671
|
+
*
|
|
672
|
+
* No-op when no local subscriber is bound to `renderId`. Closed
|
|
673
|
+
* sockets are skipped silently by the underlying `send()` helper —
|
|
674
|
+
* same posture as `sendPropsUpdate` / `notifyRenderPush`. Per-
|
|
675
|
+
* subscriber send failures are logged but never propagated.
|
|
676
|
+
*/
|
|
677
|
+
externalBroadcast(renderId: string, frame: WebSocketMessage): void;
|
|
635
678
|
/** Number of live subscribers. Useful for health / debug introspection. */
|
|
636
679
|
readonly subscriberCount: number;
|
|
637
680
|
/** Number of distinct sessions with at least one subscriber. */
|
|
@@ -645,7 +688,7 @@ export interface SessionChannelServer {
|
|
|
645
688
|
* Build an OSS live-channel server. The returned object is designed to be
|
|
646
689
|
* composed into `createGguiServer` — see `server.ts` for the wire-up.
|
|
647
690
|
*/
|
|
648
|
-
export declare function
|
|
691
|
+
export declare function createRenderChannelServer(opts: RenderChannelOptions): RenderChannelServer;
|
|
649
692
|
/** Fabricate a request id for live-channel ops so logs correlate. */
|
|
650
693
|
export declare function newRequestId(): string;
|
|
651
|
-
//# sourceMappingURL=
|
|
694
|
+
//# sourceMappingURL=render-channel.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"render-channel.d.ts","sourceRoot":"","sources":["../src/render-channel.ts"],"names":[],"mappings":"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"}
|