@ggui-ai/mcp-server 0.8.0 → 0.10.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 (67) hide show
  1. package/dist/api-renders-routes.d.ts +21 -0
  2. package/dist/api-renders-routes.d.ts.map +1 -1
  3. package/dist/api-renders-routes.js +54 -28
  4. package/dist/api-renders-stream-route.d.ts +80 -0
  5. package/dist/api-renders-stream-route.d.ts.map +1 -0
  6. package/dist/api-renders-stream-route.js +311 -0
  7. package/dist/build-mcp.d.ts +48 -7
  8. package/dist/build-mcp.d.ts.map +1 -1
  9. package/dist/build-mcp.js +87 -6
  10. package/dist/code-module-variant.d.ts +150 -0
  11. package/dist/code-module-variant.d.ts.map +1 -0
  12. package/dist/code-module-variant.js +243 -0
  13. package/dist/code-routes.d.ts +12 -2
  14. package/dist/code-routes.d.ts.map +1 -1
  15. package/dist/code-routes.js +12 -2
  16. package/dist/console-session-routes.d.ts.map +1 -1
  17. package/dist/console-session-routes.js +11 -0
  18. package/dist/control-service.d.ts +29 -3
  19. package/dist/control-service.d.ts.map +1 -1
  20. package/dist/control-service.js +26 -2
  21. package/dist/ggui-session-channel/action-ingress.d.ts +2 -2
  22. package/dist/ggui-session-channel/action-ingress.d.ts.map +1 -1
  23. package/dist/ggui-session-channel/channel-subscriptions.d.ts +4 -4
  24. package/dist/ggui-session-channel/channel-subscriptions.d.ts.map +1 -1
  25. package/dist/ggui-session-channel/internal-types.d.ts +63 -9
  26. package/dist/ggui-session-channel/internal-types.d.ts.map +1 -1
  27. package/dist/ggui-session-channel/outbound.d.ts +15 -5
  28. package/dist/ggui-session-channel/outbound.d.ts.map +1 -1
  29. package/dist/ggui-session-channel/outbound.js +64 -24
  30. package/dist/ggui-session-channel/socket-router.d.ts +8 -3
  31. package/dist/ggui-session-channel/socket-router.d.ts.map +1 -1
  32. package/dist/ggui-session-channel/socket-router.js +6 -1
  33. package/dist/ggui-session-channel/subscribe.d.ts +48 -3
  34. package/dist/ggui-session-channel/subscribe.d.ts.map +1 -1
  35. package/dist/ggui-session-channel/subscribe.js +97 -36
  36. package/dist/ggui-session-channel/subscriber-lifecycle.d.ts +23 -13
  37. package/dist/ggui-session-channel/subscriber-lifecycle.d.ts.map +1 -1
  38. package/dist/ggui-session-channel/subscriber-lifecycle.js +24 -11
  39. package/dist/ggui-session-channel.d.ts +58 -11
  40. package/dist/ggui-session-channel.d.ts.map +1 -1
  41. package/dist/ggui-session-channel.js +55 -19
  42. package/dist/health-routes.d.ts +19 -3
  43. package/dist/health-routes.d.ts.map +1 -1
  44. package/dist/health-routes.js +26 -18
  45. package/dist/index.d.ts +6 -3
  46. package/dist/index.d.ts.map +1 -1
  47. package/dist/index.js +13 -1
  48. package/dist/instructions-presets.js +10 -10
  49. package/dist/mcp-apps-outbound.d.ts +88 -11
  50. package/dist/mcp-apps-outbound.d.ts.map +1 -1
  51. package/dist/mcp-apps-outbound.js +470 -77
  52. package/dist/mcp-endpoint-routes.d.ts +23 -5
  53. package/dist/mcp-endpoint-routes.d.ts.map +1 -1
  54. package/dist/mcp-endpoint-routes.js +69 -1
  55. package/dist/oauth-as-routes.d.ts +11 -0
  56. package/dist/oauth-as-routes.d.ts.map +1 -1
  57. package/dist/oauth-as-routes.js +45 -1
  58. package/dist/oauth.d.ts.map +1 -1
  59. package/dist/oauth.js +8 -1
  60. package/dist/runtime-bundle-hash.d.ts +55 -0
  61. package/dist/runtime-bundle-hash.d.ts.map +1 -0
  62. package/dist/runtime-bundle-hash.js +85 -0
  63. package/dist/runtime-bundle-route.js +1 -1
  64. package/dist/server.d.ts +239 -61
  65. package/dist/server.d.ts.map +1 -1
  66. package/dist/server.js +355 -143
  67. package/package.json +13 -12
package/dist/server.js CHANGED
@@ -38,20 +38,20 @@
38
38
  * once at boot so operators see the shape they're running.
39
39
  */
40
40
  import { CONSOLE_DIST_DIR } from "@ggui-ai/console/server";
41
- import { RUNTIME_BUNDLE_FILE, RUNTIME_BUNDLE_URL_PATH } from "@ggui-ai/iframe-runtime/server";
41
+ import { RUNTIME_BUNDLE_FILE, RUNTIME_BUNDLE_URL_PATH, RUNTIME_SHIMS_DIR, RUNTIME_SHIMS_URL_PREFIX, } from "@ggui-ai/iframe-runtime/server";
42
42
  import { createDeterministicBlueprintSelector, isTokenRegisteringAuthAdapter, mintSessionToken, mintWsToken, refreshWsToken, verifyToken, } from "@ggui-ai/mcp-server-core";
43
43
  import { createInMemoryBlueprintSearch, createInMemoryGeneratorRegistry, FixedWindowRateLimiter, InMemoryActiveConsumerRegistry, InMemoryAppMetadataStore, InMemoryAuthAdapter, InMemoryBlueprintIndex, InMemoryBlueprintStore, InMemoryKeyValueStore, InMemoryPairingService, InMemoryPendingEventConsumer, InMemoryQuotaStore, InMemoryGguiSessionStore, InMemoryGguiSessionStreamBuffer, InMemoryVectorStore, MockEmbeddingProvider, NoopAuditSink, NoopRateLimiter, NoopTelemetrySink, } from "@ggui-ai/mcp-server-core/in-memory";
44
44
  import { createGguiListGadgetsHandler, createGguiListThemesHandler, } from "@ggui-ai/mcp-server-handlers/app-discovery";
45
45
  import { createDescribeBlueprintFormatHandler, createDescribeDataContractFormatHandler, createGetBlueprintBoilerplateHandler, createGetExampleBlueprintsHandler, createListAvailablePrimitivesHandler, createListFeaturedBlueprintsHandler, createRenderBlueprintHandler, createSearchBlueprintsHandler, createValidateBlueprintHandler, } from "@ggui-ai/mcp-server-handlers/blueprints";
46
46
  import { createGguiOpsDeleteBlueprintHandler, createGguiOpsGenerateBlueprintHandler, createGguiOpsListBlueprintsHandler, createGguiOpsRegisterBlueprintHandler, createGguiOpsUpdateBlueprintHandler, } from "@ggui-ai/mcp-server-handlers/ops-blueprint";
47
- import { setCacheTraceSink, setPayloadTraceSink } from "@ggui-ai/mcp-server-handlers/renders";
47
+ import { setCacheTraceSink, setPayloadTraceSink, wsOriginToHttpOrigin, } from "@ggui-ai/mcp-server-handlers/renders";
48
48
  import { loadTheme } from "@ggui-ai/project-config/node";
49
49
  import { LIFECYCLE_CHANNEL } from "@ggui-ai/protocol";
50
50
  import { setLlmTraceSink } from "@ggui-ai/ui-gen/harness/llm-trace-sink";
51
51
  import { setValidatorTraceSink } from "@ggui-ai/ui-gen/harness/validator-trace-sink";
52
52
  import express from "express";
53
53
  import { AsyncLocalStorage } from "node:async_hooks";
54
- import { createHash, randomBytes } from "node:crypto";
54
+ import { randomBytes } from "node:crypto";
55
55
  import fs from "node:fs";
56
56
  import path from "node:path";
57
57
  import { BoundedCacheTraceSink, mountConsoleCacheRoutes } from "./console-cache.js";
@@ -71,12 +71,13 @@ import { createCreateAppHandler, createDeleteAppHandler, createListAppsHandler,
71
71
  import { createIssueConnectorKeyHandler, createListConnectorKeysHandler, createRevokeConnectorKeyHandler, } from "@ggui-ai/mcp-server-handlers/ops-connector-keys";
72
72
  import { createRedeemCouponHandler, } from "@ggui-ai/mcp-server-handlers/ops-coupon";
73
73
  import { createCreateOrgHandler, createGetOrgBalanceHandler, createInviteToOrgHandler, createListOrgsHandler, createRemoveOrgMemberHandler, createRenameOrgHandler, createRevokeInviteHandler, } from "@ggui-ai/mcp-server-handlers/ops-orgs";
74
- import { createGguiConsumeHandler, createGguiDeclareToolCatalogHandler, createGguiEmitHandler, createGguiGetSessionHandler, createGguiHandshakeHandler, createGguiListSessionsHandler, createGguiRefreshWsTokenHandler, createGguiRenderHandler, createGguiSubmitActionHandler, createGguiSyncContextHandler, createGguiUpdateHandler, InMemoryToolIdentityCatalogStore, createInMemoryProvisionalPreviewRegistry, } from "@ggui-ai/mcp-server-handlers/renders";
74
+ import { createGguiConsumeHandler, createGguiDeclareToolCatalogHandler, createGguiEmitHandler, createGguiGetRenderSourceHandler, createGguiGetSessionHandler, createGguiHandshakeHandler, createGguiListSessionsHandler, createGguiRefreshWsTokenHandler, createGguiRenderHandler, createGguiRuntimePullHandler, createGguiRuntimeTelemetryHandler, createGguiSubmitActionHandler, createGguiSyncContextHandler, createGguiUpdateHandler, createGguiAmendHandler, InMemoryToolIdentityCatalogStore, createInMemoryProvisionalPreviewRegistry, } from "@ggui-ai/mcp-server-handlers/renders";
75
75
  import { DEFAULT_ADMIN_BLUEPRINTS_PATH, mountAdminBlueprintsTransport, } from "./admin-blueprints-transport.js";
76
76
  import { mountAdminOAuthProvidersTransport } from "./admin-oauth-providers-transport.js";
77
77
  import { DEFAULT_BUILDER_APP_ID, defaultAppIdFromIdentity } from "./auth.js";
78
78
  import { mountMcpEndpoints } from "./mcp-endpoint-routes.js";
79
79
  import { mountApiRendersRoutes } from "./api-renders-routes.js";
80
+ import { mountApiRendersStreamRoute } from "./api-renders-stream-route.js";
80
81
  import { mountConsoleBlueprintRoutes } from "./console-blueprint-routes.js";
81
82
  import { mountConsoleChatRoutes } from "./console-chat-routes.js";
82
83
  import { mountConsoleConfigRoutes } from "./console-config-routes.js";
@@ -89,10 +90,12 @@ import { mountConsoleSessionRoutes } from "./console-session-routes.js";
89
90
  import { mountConsoleStaticRoutes } from "./console-static-routes.js";
90
91
  import { mountConsoleSessionsRoutes } from "./console-sessions-routes.js";
91
92
  import { mountCodeRoutes } from "./code-routes.js";
93
+ import { captureShimSources, createCodeModuleUrlMinter, mountShimRoutes, } from "./code-module-variant.js";
92
94
  import { mountHealthRoutes } from "./health-routes.js";
93
95
  import { mountOAuthAuthorizationServerRoutes } from "./oauth-as-routes.js";
94
96
  import { mountOAuthClientsRoutes } from "./oauth-clients-routes.js";
95
97
  import { mountRuntimeBundleRoute } from "./runtime-bundle-route.js";
98
+ import { computeRuntimeBundleHash, insertRuntimeBundleHash, } from "./runtime-bundle-hash.js";
96
99
  import { createCsrfMiddleware, mountCsrfTokenRoute } from "./csrf-middleware.js";
97
100
  import { mountEmailLoginRoutes } from "./email-login.js";
98
101
  import { resolveMcpInstructions } from "./instructions-presets.js";
@@ -110,7 +113,7 @@ import { InMemoryOAuthStorage } from "./oauth.js";
110
113
  import { DEFAULT_PAIRING_ADMIN_INIT_PATH, DEFAULT_PAIRING_PATH, mountPairingTransport, } from "./pairing-transport.js";
111
114
  import { createPairLoginRateLimitMiddleware } from "./rate-limit-middleware.js";
112
115
  import { createGguiSessionChannelServer, } from "./ggui-session-channel.js";
113
- import { buildRequestContextMiddleware, resolveRuntimeUrl } from "./request-context.js";
116
+ import { buildRequestContextMiddleware, resolvePublicBaseUrl, resolveRuntimeUrl, } from "./request-context.js";
114
117
  import { composePreviewReservedValidator, mergeReservedValidators } from "./reserved-validators.js";
115
118
  import { checkRenderSchemaCompat, DEFAULT_SCHEMA_COMPAT_MODE, } from "./schema-compat.js";
116
119
  import { createSecurityHeadersMiddleware } from "./security-headers-middleware.js";
@@ -124,30 +127,6 @@ const DEFAULT_INFO = {
124
127
  version: "0.0.1",
125
128
  description: "Open self-hosted MCP server for the ggui protocol. Powered by @ggui-ai/mcp-server-handlers.",
126
129
  };
127
- /**
128
- * Canonical default handler set. Every `@ggui-ai/mcp-server-handlers`
129
- * family the OSS server ships with lands here, bound to the caller-
130
- * supplied deps. Use this when you want to EXTEND the defaults rather
131
- * than replace them wholesale:
132
- *
133
- * ```ts
134
- * const server = createGguiServer({
135
- * vectors, embedding,
136
- * handlers: [
137
- * ...defaultHandlers({ vectors, embedding }),
138
- * myCustomHandler,
139
- * ],
140
- * });
141
- * ```
142
- *
143
- * Without this helper, `handlers:` replaces the full list — callers
144
- * lose the defaults unless they copy-paste them. Keeping `defaultHandlers`
145
- * named means the default set stays discoverable + testable in one place.
146
- *
147
- * `render` is opt-in via `deps.render` — it's only useful when the server
148
- * was booted with `mcpApps: true` (so `ui://ggui/render` is served)
149
- * and pairs a real GguiSessionStore. Callers get the choice explicitly.
150
- */
151
130
  /**
152
131
  * Assemble the `opsBlueprint` dep bundle for `defaultHandlers`.
153
132
  *
@@ -177,9 +156,82 @@ function buildOpsBlueprintDeps(input) {
177
156
  ...(input.resolveLlm ? { resolveLlm: input.resolveLlm } : {}),
178
157
  ...(input.blueprints ? { blueprints: input.blueprints } : {}),
179
158
  ...(input.cacheRegistry ? { cacheRegistry: input.cacheRegistry } : {}),
159
+ authorizeAppAccess: input.authorizeAppAccess ?? (async () => ({ allowed: true })),
180
160
  },
181
161
  };
182
162
  }
163
+ /**
164
+ * Materialize the operator-class blueprint family from a bound
165
+ * {@link OpsBlueprintBundle}. Registered on the control plane via
166
+ * `audience: ['ops']`.
167
+ *
168
+ * Four of the five land whenever the bundle is present; the `generate`
169
+ * tool additionally requires `resolveLlm` + `blueprints` (the same deps
170
+ * the render generation path reads) because without them it has no
171
+ * dispatch target.
172
+ *
173
+ * Lives here — not inline in {@link defaultHandlers} — so BOTH
174
+ * composition paths build the family from ONE body:
175
+ * `defaultHandlers` for the default handler set, and
176
+ * {@link buildOpsBundleHandlers} for deployments that supply their own
177
+ * `CreateGguiServerOptions.handlers` array. The construction used to sit
178
+ * only inside `defaultHandlers`, so an explicit handler list silently
179
+ * dropped all five tools even with the bundle bound.
180
+ */
181
+ export function buildOpsBlueprintHandlers(input) {
182
+ const { bundle } = input;
183
+ const handlers = [];
184
+ // Bundle-explicit `appMetadataStore` wins; falls back to the
185
+ // server-level one — same precedence
186
+ // `deps.handshake.appMetadataStore ?? deps.appMetadataStore` uses.
187
+ // Gates `assertGadgetsRegistered` inside both generate + register
188
+ // (see each handler's own no-op-when-unbound posture).
189
+ const opsBlueprintAppMetadataStore = bundle.appMetadataStore ?? input.appMetadataStore;
190
+ if (bundle.resolveLlm && bundle.blueprints) {
191
+ handlers.push(createGguiOpsGenerateBlueprintHandler({
192
+ registry: bundle.registry,
193
+ blueprintStore: bundle.blueprintStore,
194
+ resolveLlm: bundle.resolveLlm,
195
+ blueprints: bundle.blueprints,
196
+ ...(bundle.putCode ? { putCode: bundle.putCode } : {}),
197
+ ...(bundle.listAllForApp ? { listAllForApp: bundle.listAllForApp } : {}),
198
+ ...(opsBlueprintAppMetadataStore
199
+ ? { appMetadataStore: opsBlueprintAppMetadataStore }
200
+ : {}),
201
+ ...(bundle.cacheRegistry ? { cacheRegistry: bundle.cacheRegistry } : {}),
202
+ ...(input.telemetry ? { telemetry: input.telemetry } : {}),
203
+ authorizeAppAccess: bundle.authorizeAppAccess,
204
+ }));
205
+ }
206
+ // `ggui_ops_register_blueprint` — sibling of `_generate_*` that
207
+ // accepts pre-built componentCode bytes. No LLM dispatch, so it
208
+ // registers whenever the ops dep bundle is bound (no resolveLlm
209
+ // / blueprints gate). Operator UX entry point for fixture
210
+ // seeding + export/reimport round-trips.
211
+ handlers.push(createGguiOpsRegisterBlueprintHandler({
212
+ blueprintStore: bundle.blueprintStore,
213
+ ...(bundle.putCode ? { putCode: bundle.putCode } : {}),
214
+ ...(bundle.listAllForApp ? { listAllForApp: bundle.listAllForApp } : {}),
215
+ ...(opsBlueprintAppMetadataStore ? { appMetadataStore: opsBlueprintAppMetadataStore } : {}),
216
+ ...(bundle.cacheRegistry ? { cacheRegistry: bundle.cacheRegistry } : {}),
217
+ ...(input.telemetry ? { telemetry: input.telemetry } : {}),
218
+ authorizeAppAccess: bundle.authorizeAppAccess,
219
+ }));
220
+ handlers.push(createGguiOpsListBlueprintsHandler({
221
+ blueprintStore: bundle.blueprintStore,
222
+ blueprintSearch: bundle.blueprintSearch,
223
+ authorizeAppAccess: bundle.authorizeAppAccess,
224
+ }));
225
+ handlers.push(createGguiOpsUpdateBlueprintHandler({
226
+ blueprintStore: bundle.blueprintStore,
227
+ authorizeAppAccess: bundle.authorizeAppAccess,
228
+ }));
229
+ handlers.push(createGguiOpsDeleteBlueprintHandler({
230
+ blueprintStore: bundle.blueprintStore,
231
+ authorizeAppAccess: bundle.authorizeAppAccess,
232
+ }));
233
+ return handlers;
234
+ }
183
235
  export function defaultHandlers(deps) {
184
236
  // Single shared pending-events pipe (Model C, sessionId-keyed).
185
237
  // render opens (`markCreated`), submit_action appends, consume drains,
@@ -284,6 +336,30 @@ export function defaultHandlers(deps) {
284
336
  ? { renderIdentityStore: deps.render.renderIdentityStore }
285
337
  : {}),
286
338
  }));
339
+ // `ggui_runtime_pull` — terminal bridge-pull rung of the live-channel
340
+ // failover ladder (WS → SSE → HTTP polling → bridge-pull). A
341
+ // CSP-jailed MCP Apps iframe pulls the GguiSessionEvent ledger over
342
+ // the host's tools/call postMessage relay — same
343
+ // `_meta.ui.visibility: ['app']` channel as ggui_runtime_submit_action,
344
+ // same ledger + horizon semantics as `GET /api/sessions/:id/events`
345
+ // (both read `renderStore.listEventsSince`). Wired only when a
346
+ // renderStore is bound (render is on) — without render there is no
347
+ // ledger to serve.
348
+ handlers.push(createGguiRuntimePullHandler({
349
+ renderStore: deps.render.renderStore,
350
+ }));
351
+ // `ggui_runtime_telemetry` — the iframe runtime's transport
352
+ // self-report (visibility ['app'], same view-callable channel as
353
+ // submit_action/pull). Sandboxed hosts (claude.ai's
354
+ // claudemcpcontent frames) expose no readable console and no
355
+ // network, so the delivery ladder's behavior there is invisible
356
+ // WITHOUT this channel: the view batches its rung transitions +
357
+ // doorbell events and this handler logs them for operators.
358
+ // Fire-and-forget, store-less — gated on deps.render only because
359
+ // the ladder it reports on exists only when renders do.
360
+ handlers.push(createGguiRuntimeTelemetryHandler({
361
+ ...(deps.logger ? { logger: deps.logger } : {}),
362
+ }));
287
363
  // `ggui_runtime_refresh_ws_token` — G14 (2026-05-23) signed-
288
364
  // envelope refresh tool. Registered only when a refresh seam is
289
365
  // wired (typically `channelBootstrap.refresh` from the
@@ -351,6 +427,11 @@ export function defaultHandlers(deps) {
351
427
  ? { serverCapabilities: deps.handshake.serverCapabilities }
352
428
  : {}),
353
429
  ...(lifecycleEmitter ? { lifecycleEmitter } : {}),
430
+ // Server-level TelemetrySink → the handler's `handshake.decided`
431
+ // emission. Without this thread the composed emission never
432
+ // fired (the handler dep predates the wiring — P2 of
433
+ // docs/plans/2026-08-19-schema-precise-render.md pinned it).
434
+ ...(deps.telemetry ? { telemetrySink: deps.telemetry } : {}),
354
435
  }));
355
436
  }
356
437
  if (deps.update) {
@@ -367,11 +448,34 @@ export function defaultHandlers(deps) {
367
448
  // re-apply patched props on the live mount without a WS round-trip.
368
449
  ...(deps.update.mintBootstrap ? { mintWsToken: deps.update.mintBootstrap } : {}),
369
450
  ...(deps.update.runtimeUrl !== undefined ? { runtimeUrl: deps.update.runtimeUrl } : {}),
451
+ ...(deps.update.sessionApiBaseUrl !== undefined
452
+ ? { sessionApiBaseUrl: deps.update.sessionApiBaseUrl }
453
+ : {}),
370
454
  ...(deps.update.themeId !== undefined ? { themeId: deps.update.themeId } : {}),
371
455
  ...(deps.update.themeMode !== undefined ? { themeMode: deps.update.themeMode } : {}),
372
456
  ...(deps.update.themeProvider !== undefined
373
457
  ? { themeProvider: deps.update.themeProvider }
374
458
  : {}),
459
+ // Mutation-time `render.contract_violation` events (P2
460
+ // measurement — update-time violations are baselined with
461
+ // render-time ones, not hidden).
462
+ ...(deps.telemetry ? { telemetrySink: deps.telemetry } : {}),
463
+ }));
464
+ // ggui_amend rides the SAME deps slot (#483 tool split): one
465
+ // mutation core, one wiring. It reads only the mutation-flow deps
466
+ // (renderStore / identity / notifier) — the bootstrap-emission
467
+ // options above are meaningless to a tool that emits no result
468
+ // meta by design.
469
+ handlers.push(createGguiAmendHandler({
470
+ renderStore: deps.update.renderStore,
471
+ ...(deps.update.renderIdentityStore
472
+ ? { renderIdentityStore: deps.update.renderIdentityStore }
473
+ : {}),
474
+ ...(deps.update.propsUpdateNotifier
475
+ ? { propsUpdateNotifier: deps.update.propsUpdateNotifier }
476
+ : {}),
477
+ // Same mutation core as ggui_update — same violation events.
478
+ ...(deps.telemetry ? { telemetrySink: deps.telemetry } : {}),
375
479
  }));
376
480
  }
377
481
  // ggui_consume registers whenever render is bound (it shares the
@@ -437,6 +541,12 @@ export function defaultHandlers(deps) {
437
541
  handlers.push(createGguiGetSessionHandler({
438
542
  renderStore: deps.render.renderStore,
439
543
  }));
544
+ // ggui_get_render_source (#282 data-plane rider) — the calling
545
+ // app reads its OWN render's generated source. No heartbeat: a
546
+ // one-shot source read is not an activity signal.
547
+ handlers.push(createGguiGetRenderSourceHandler({
548
+ renderStore: deps.render.renderStore,
549
+ }));
440
550
  // ggui_list_sessions — host-scoped render enumeration for resume.
441
551
  // Folds the ws-token mint into the same call so the host doesn't
442
552
  // round-trip twice (list, then mint-per-render). Reuses the
@@ -515,12 +625,20 @@ export function defaultHandlers(deps) {
515
625
  : {}),
516
626
  ...(deps.render.mintBootstrap ? { mintWsToken: deps.render.mintBootstrap } : {}),
517
627
  ...(deps.render.runtimeUrl !== undefined ? { runtimeUrl: deps.render.runtimeUrl } : {}),
628
+ ...(deps.render.sessionApiBaseUrl !== undefined
629
+ ? { sessionApiBaseUrl: deps.render.sessionApiBaseUrl }
630
+ : {}),
518
631
  ...(deps.render.themeId !== undefined ? { themeId: deps.render.themeId } : {}),
519
632
  ...(deps.render.themeMode !== undefined ? { themeMode: deps.render.themeMode } : {}),
520
633
  ...(deps.render.themeProvider !== undefined
521
634
  ? { themeProvider: deps.render.themeProvider }
522
635
  : {}),
523
636
  ...(deps.render.rateLimiter ? { rateLimiter: deps.render.rateLimiter } : {}),
637
+ // Render measurement events (`render.attempted` /
638
+ // `render.contract_violation` / `render.committed`) — P2 of
639
+ // docs/plans/2026-08-19-schema-precise-render.md. One
640
+ // server-level sink, every consumer.
641
+ ...(deps.telemetry ? { telemetrySink: deps.telemetry } : {}),
524
642
  ...(deps.render.shortCodeIndex ? { shortCodeIndex: deps.render.shortCodeIndex } : {}),
525
643
  ...(deps.render.renderIdentityStore
526
644
  ? { renderIdentityStore: deps.render.renderIdentityStore }
@@ -553,6 +671,9 @@ export function defaultHandlers(deps) {
553
671
  ? {
554
672
  codeStore: deps.render.codeStore,
555
673
  codeBaseUrl: deps.render.codeBaseUrl,
674
+ ...(deps.render.mintCodeModuleUrl
675
+ ? { mintCodeModuleUrl: deps.render.mintCodeModuleUrl }
676
+ : {}),
556
677
  }
557
678
  : {}),
558
679
  // Share the handshake KV store between the two handlers so
@@ -565,61 +686,24 @@ export function defaultHandlers(deps) {
565
686
  ...(lifecycleEmitter ? { lifecycleEmitter } : {}),
566
687
  }));
567
688
  }
568
- // Operator-class blueprint tools. Registered
569
- // on /ops via `audience: ['ops']`. Three read-mutating tools land
570
- // whenever the blueprint store + search seam is bound; the
571
- // `generate` tool additionally requires `resolveLlm` +
572
- // `blueprints` (same deps the render generation path reads). Cloud
573
- // pods wire all four through their own composition layer.
689
+ // Operator-class blueprint tools — see `buildOpsBlueprintHandlers`
690
+ // for the family's registration rules. The server-level
691
+ // `deps.appMetadataStore` + `deps.telemetry` are offered as the
692
+ // bundle's fallbacks; render already gets the server-level store at
693
+ // :1398. Cloud pods that pass their own handler list pick the same
694
+ // family up through `buildOpsBundleHandlers`.
574
695
  if (deps.opsBlueprint) {
575
- if (deps.opsBlueprint.resolveLlm && deps.opsBlueprint.blueprints) {
576
- handlers.push(createGguiOpsGenerateBlueprintHandler({
577
- registry: deps.opsBlueprint.registry,
578
- blueprintStore: deps.opsBlueprint.blueprintStore,
579
- resolveLlm: deps.opsBlueprint.resolveLlm,
580
- blueprints: deps.opsBlueprint.blueprints,
581
- ...(deps.opsBlueprint.putCode ? { putCode: deps.opsBlueprint.putCode } : {}),
582
- ...(deps.opsBlueprint.listAllForApp
583
- ? { listAllForApp: deps.opsBlueprint.listAllForApp }
584
- : {}),
585
- ...(deps.opsBlueprint.cacheRegistry
586
- ? { cacheRegistry: deps.opsBlueprint.cacheRegistry }
587
- : {}),
588
- ...(deps.telemetry ? { telemetry: deps.telemetry } : {}),
589
- }));
590
- }
591
- // `ggui_ops_register_blueprint` — sibling of `_generate_*` that
592
- // accepts pre-built componentCode bytes. No LLM dispatch, so it
593
- // registers whenever the ops dep bundle is bound (no resolveLlm
594
- // / blueprints gate). Operator UX entry point for fixture
595
- // seeding + export/reimport round-trips.
596
- handlers.push(createGguiOpsRegisterBlueprintHandler({
597
- blueprintStore: deps.opsBlueprint.blueprintStore,
598
- ...(deps.opsBlueprint.putCode ? { putCode: deps.opsBlueprint.putCode } : {}),
599
- ...(deps.opsBlueprint.listAllForApp
600
- ? { listAllForApp: deps.opsBlueprint.listAllForApp }
601
- : {}),
602
- ...(deps.opsBlueprint.cacheRegistry
603
- ? { cacheRegistry: deps.opsBlueprint.cacheRegistry }
604
- : {}),
696
+ handlers.push(...buildOpsBlueprintHandlers({
697
+ bundle: deps.opsBlueprint,
698
+ ...(deps.appMetadataStore ? { appMetadataStore: deps.appMetadataStore } : {}),
605
699
  ...(deps.telemetry ? { telemetry: deps.telemetry } : {}),
606
700
  }));
607
- handlers.push(createGguiOpsListBlueprintsHandler({
608
- blueprintStore: deps.opsBlueprint.blueprintStore,
609
- blueprintSearch: deps.opsBlueprint.blueprintSearch,
610
- }));
611
- handlers.push(createGguiOpsUpdateBlueprintHandler({
612
- blueprintStore: deps.opsBlueprint.blueprintStore,
613
- }));
614
- handlers.push(createGguiOpsDeleteBlueprintHandler({
615
- blueprintStore: deps.opsBlueprint.blueprintStore,
616
- }));
617
701
  }
618
702
  return handlers;
619
703
  }
620
704
  /**
621
705
  * Build the operator-class handlers for the apps / orgs /
622
- * connector-keys / coupon domains. Each domain materializes
706
+ * connector-keys / coupon / blueprint domains. Each domain materializes
623
707
  * independently when its deps seam is bound; deployments that wired
624
708
  * none get an empty list.
625
709
  *
@@ -650,6 +734,15 @@ export function buildOpsBundleHandlers(deps) {
650
734
  coupons: deps.opsCoupon.coupons,
651
735
  }));
652
736
  }
737
+ if (deps.opsBlueprint) {
738
+ // No server-level `appMetadataStore` fallback on this path — the
739
+ // bundle's own field governs, since a deployment that binds its own
740
+ // bundle here binds the store with it.
741
+ handlers.push(...buildOpsBlueprintHandlers({
742
+ bundle: deps.opsBlueprint,
743
+ ...(deps.telemetry ? { telemetry: deps.telemetry } : {}),
744
+ }));
745
+ }
653
746
  return handlers;
654
747
  }
655
748
  /**
@@ -661,6 +754,9 @@ export function createGguiServer(opts = {}) {
661
754
  const info = { ...DEFAULT_INFO, ...opts.info };
662
755
  const logger = opts.logger ?? createConsoleLogger({ server: info.name });
663
756
  const bodyLimit = opts.bodyLimit ?? "4mb";
757
+ // Where the content-addressable routes are reached from the iframe:
758
+ // an explicit asset host, else the public origin (see `codeBaseUrl`).
759
+ const codeBaseUrl = opts.codeBaseUrl ?? opts.publicBaseUrl;
664
760
  // Operator mode: gates the `/devtools/*` namespace. Explicit option
665
761
  // wins; otherwise read GGUI_MODE env (`'dev'` opts in, anything else
666
762
  // including unset is `'prod'`). Surfaced via `/info` so the SPA
@@ -799,22 +895,40 @@ export function createGguiServer(opts = {}) {
799
895
  // pointing at a foreign copy is left untouched, and
800
896
  // `runtime.hashedUrl: false` opts out entirely.
801
897
  const runtimeBundleHash = runtimeBundleBytes !== undefined && runtimeConfig.hashedUrl !== false
802
- ? createHash("sha256").update(runtimeBundleBytes).digest("hex").slice(0, 12)
898
+ ? computeRuntimeBundleHash(runtimeBundleBytes)
803
899
  : undefined;
804
- const insertHash = (urlOrPath) => {
805
- if (runtimeBundleHash === undefined)
806
- return urlOrPath;
807
- const plainName = runtimePath.slice(runtimePath.lastIndexOf("/") + 1);
808
- const dot = plainName.lastIndexOf(".");
809
- const hashedName = dot === -1
810
- ? `${plainName}.${runtimeBundleHash}`
811
- : `${plainName.slice(0, dot)}.${runtimeBundleHash}${plainName.slice(dot)}`;
812
- if (!urlOrPath.endsWith(`/${plainName}`) && urlOrPath !== plainName)
813
- return urlOrPath;
814
- return `${urlOrPath.slice(0, urlOrPath.length - plainName.length)}${hashedName}`;
815
- };
900
+ const insertHash = (urlOrPath) => runtimeBundleHash === undefined
901
+ ? urlOrPath
902
+ : insertRuntimeBundleHash(urlOrPath, runtimeBundleHash, runtimePath.slice(runtimePath.lastIndexOf("/") + 1));
816
903
  const hashedRuntimePath = runtimeBundleHash !== undefined ? insertHash(runtimePath) : undefined;
817
904
  const runtimeBootstrapUrl = insertHash(runtimeConfig.url ?? runtimePath);
905
+ // Strict-CSP module-variant family (ggui#522 slice 2). Every
906
+ // ingredient must exist for any of it to mount: the shim files the
907
+ // runtime build emitted next to the bundle (the fetchable twins of
908
+ // the data-url shims), the bundle content hash (the variant KEY —
909
+ // shims ship in the bundle's dist, so one hash versions both), the
910
+ // code store (the variant is a `/code` twin), and an absolute code
911
+ // base URL (the rewrite embeds absolute shim URLs — a srcdoc frame
912
+ // has no base to resolve relative ones). Missing any ⇒ no variant
913
+ // URLs are minted and no variant routes mount; renders keep the raw
914
+ // codeUrl/codeB64 carriers and the renderer's blob ladder.
915
+ const shimSources = runtimeEnabled && runtimeBundleHash !== undefined
916
+ ? captureShimSources(runtimeConfig.distDir !== undefined
917
+ ? path.join(runtimeConfig.distDir, "shims")
918
+ : RUNTIME_SHIMS_DIR)
919
+ : undefined;
920
+ const codeModuleVariant = shimSources !== undefined &&
921
+ runtimeBundleHash !== undefined &&
922
+ opts.codeStore !== undefined &&
923
+ codeBaseUrl !== undefined
924
+ ? {
925
+ runtimeHash: runtimeBundleHash,
926
+ shimBaseUrl: `${codeBaseUrl.replace(/\/$/, "")}${RUNTIME_SHIMS_URL_PREFIX}/${runtimeBundleHash}`,
927
+ }
928
+ : undefined;
929
+ const mintCodeModuleUrl = codeModuleVariant !== undefined
930
+ ? createCodeModuleUrlMinter({ runtimeHash: codeModuleVariant.runtimeHash })
931
+ : undefined;
818
932
  // Lazy resolver: each render/update handler invocation looks up the
819
933
  // request-context-derived absolute base inside the request scope
820
934
  // (via AsyncLocalStorage). Static `publicBaseUrl` wins when set;
@@ -1315,6 +1429,14 @@ export function createGguiServer(opts = {}) {
1315
1429
  // an absolute URL instead of the relative default that
1316
1430
  // breaks under srcdoc iframes (claude.ai).
1317
1431
  runtimeUrl: resolveRuntimeUrlForResultMeta,
1432
+ // Session-API base for `pollingUrl` + `sseUrl` stamping.
1433
+ // Same lazy request-scope resolution as runtimeUrl:
1434
+ // static publicBaseUrl wins, X-Forwarded-Host honored
1435
+ // for loopback peers, `undefined` outside any request —
1436
+ // the handler then falls back to the minted trio's
1437
+ // wsUrl origin flip (OSS dev: `ws://localhost/ws` →
1438
+ // `http://localhost`).
1439
+ sessionApiBaseUrl: () => resolvePublicBaseUrl(opts.publicBaseUrl),
1318
1440
  // Forward operator-picked theme onto every
1319
1441
  // `ai.ggui/render.themeId` slice field so MCP Apps hosts
1320
1442
  // (claude.ai, Claude Desktop) that mount via the
@@ -1418,18 +1540,22 @@ export function createGguiServer(opts = {}) {
1418
1540
  //
1419
1541
  // Content-addressable code delivery. When the operator
1420
1542
  // wired `opts.codeStore`, forward it to the render
1421
- // handler along with the base
1422
- // URL the code-blob route resolves to. We prefer the
1423
- // explicit `--public-base-url` (so the URL is reachable
1424
- // from a remote host's iframe sandbox); when absent we
1425
- // fall back to "no codeUrl emission" — the iframe then
1543
+ // handler along with the base URL the code-blob route
1544
+ // resolves to: the explicit `codeBaseUrl` (an edge-cached
1545
+ // asset host, ggui#522) or, absent that, the
1546
+ // `--public-base-url` (so the URL is reachable from a
1547
+ // remote host's iframe sandbox); with neither we fall
1548
+ // back to "no codeUrl emission" — the iframe then
1426
1549
  // mounts through the live trio and the WS subscribe
1427
1550
  // carries the render body, which is the delivery path a
1428
1551
  // render-channel deployment already has.
1429
- ...(opts.codeStore && opts.publicBaseUrl
1552
+ ...(opts.codeStore && codeBaseUrl !== undefined
1430
1553
  ? {
1431
1554
  codeStore: opts.codeStore,
1432
- codeBaseUrl: opts.publicBaseUrl,
1555
+ codeBaseUrl,
1556
+ ...(mintCodeModuleUrl !== undefined
1557
+ ? { mintCodeModuleUrl }
1558
+ : {}),
1433
1559
  }
1434
1560
  : {}),
1435
1561
  // Bootstrap-side mirror of the handshake's
@@ -1503,10 +1629,10 @@ export function createGguiServer(opts = {}) {
1503
1629
  ? { renderIdentityStore: opts.renderIdentityStore }
1504
1630
  : {}),
1505
1631
  propsUpdateNotifier: {
1506
- sendPropsUpdate: async (sessionId, props) => {
1632
+ sendPropsUpdate: async (sessionId, props, epoch) => {
1507
1633
  if (!channelForHealth)
1508
1634
  return;
1509
- await channelForHealth.sendPropsUpdate(sessionId, props);
1635
+ await channelForHealth.sendPropsUpdate(sessionId, props, epoch);
1510
1636
  },
1511
1637
  },
1512
1638
  // Bootstrap-emission deps. Mirror render so MCP Apps hosts
@@ -1520,6 +1646,9 @@ export function createGguiServer(opts = {}) {
1520
1646
  // same absolute URL when the server sits behind a tunnel
1521
1647
  // / reverse proxy.
1522
1648
  runtimeUrl: resolveRuntimeUrlForResultMeta,
1649
+ // Session-API base — matches render so both tools stamp
1650
+ // identical `pollingUrl` + `sseUrl` values.
1651
+ sessionApiBaseUrl: () => resolvePublicBaseUrl(opts.publicBaseUrl),
1523
1652
  ...(opts.theme !== undefined && opts.theme.source === "preset"
1524
1653
  ? { themeId: opts.theme.preset }
1525
1654
  : {}),
@@ -1549,42 +1678,48 @@ export function createGguiServer(opts = {}) {
1549
1678
  // `appMetadataStore` to register `ggui_list_themes`; absent ⇒
1550
1679
  // tool omitted from `tools/list`.
1551
1680
  ...(opts.themes ? { themes: opts.themes } : {}),
1552
- // Operator-class blueprint tool wiring. Threads the
1553
- // resolved blueprint store + search + generator registry into
1554
- // defaultHandlers; the four `ggui_ops_*` tools land on /ops
1555
- // via their `audience: ['ops']` tag. The `resolveLlm` +
1556
- // `blueprints` deps come from the same source render reads, so
1557
- // generate dispatches through the same credential + catalog
1558
- // path as live agent traffic. listAllForApp wires only when
1559
- // the resolved store is the in-memory adapter (which exposes
1560
- // it); cloud adapters bind their own listAllForApp via the
1561
- // search seam.
1562
- // Wire only when we have a resolved generator
1563
- // registry. Without `generators`, the ops `generate` path has
1564
- // no dispatch target; the list/update/delete trio could
1565
- // technically run without it but the operator UX expects all
1566
- // four together, so we gate the whole block on the registry.
1567
- ...(generators
1568
- ? buildOpsBlueprintDeps({
1569
- registry: generators,
1570
- blueprintStore,
1571
- blueprintSearch,
1572
- ...(generationWithCache?.resolveLlm
1573
- ? { resolveLlm: generationWithCache.resolveLlm }
1574
- : {}),
1575
- ...(generationWithCache?.blueprints
1576
- ? { blueprints: generationWithCache.blueprints }
1577
- : opts.blueprintProvider
1578
- ? { blueprints: opts.blueprintProvider }
1681
+ // Operator-class blueprint tool wiring. Explicit-wins: a
1682
+ // deployment that supplies `opts.opsBlueprint` gets that bundle
1683
+ // threaded through AS-IS (its own store/search/registry/
1684
+ // authorizer — the factory does not assemble or override it).
1685
+ // Otherwise falls back to the OSS default assembly below, which
1686
+ // threads the resolved blueprint store + search + generator
1687
+ // registry into defaultHandlers; the five `ggui_ops_*` tools
1688
+ // land on the control plane via their `audience: ['ops']` tag.
1689
+ // The `resolveLlm` + `blueprints` deps come from the same
1690
+ // source render reads, so generate dispatches through the same
1691
+ // credential + catalog path as live agent traffic.
1692
+ // listAllForApp wires only when the resolved store is the
1693
+ // in-memory adapter (which exposes it); cloud adapters bind
1694
+ // their own listAllForApp via the search seam.
1695
+ // The fallback is gated on `generators`: without it, the ops
1696
+ // `generate` path has no dispatch target; the
1697
+ // register/list/update/delete quartet could technically run
1698
+ // without it but the operator UX expects all five together, so
1699
+ // we gate the whole block on the registry.
1700
+ ...(opts.opsBlueprint
1701
+ ? { opsBlueprint: opts.opsBlueprint }
1702
+ : generators
1703
+ ? buildOpsBlueprintDeps({
1704
+ registry: generators,
1705
+ blueprintStore,
1706
+ blueprintSearch,
1707
+ ...(generationWithCache?.resolveLlm
1708
+ ? { resolveLlm: generationWithCache.resolveLlm }
1579
1709
  : {}),
1580
- // Mirror operator-authored blueprints into the cache
1581
- // vectorStore so the agent-facing matchBlueprint exact-
1582
- // key probe (handshake + render) finds them. Same bundle
1583
- // the render handler + handshake negotiator already
1584
- // consume.
1585
- ...(generationWithCache?.cache ? { cacheRegistry: generationWithCache.cache } : {}),
1586
- })
1587
- : {}),
1710
+ ...(generationWithCache?.blueprints
1711
+ ? { blueprints: generationWithCache.blueprints }
1712
+ : opts.blueprintProvider
1713
+ ? { blueprints: opts.blueprintProvider }
1714
+ : {}),
1715
+ // Mirror operator-authored blueprints into the cache
1716
+ // vectorStore so the agent-facing matchBlueprint exact-
1717
+ // key probe (handshake + render) finds them. Same bundle
1718
+ // the render handler + handshake negotiator already
1719
+ // consume.
1720
+ ...(generationWithCache?.cache ? { cacheRegistry: generationWithCache.cache } : {}),
1721
+ })
1722
+ : {}),
1588
1723
  // ggui_emit resolves the channel via
1589
1724
  // a lazy getter so the handler captures whatever
1590
1725
  // `channelForHealth` ends up pointing at after
@@ -1600,11 +1735,13 @@ export function createGguiServer(opts = {}) {
1600
1735
  logger,
1601
1736
  });
1602
1737
  // Operator-class domain handlers (apps / orgs / connector-keys /
1603
- // coupon). Built on EVERY path — they hang off their own explicit
1604
- // options, so a deployment that supplies a custom base handler list
1605
- // still gets the domains it wired. A name already claimed by the base
1606
- // list wins: that's how the cloud pod ships its own
1607
- // `ggui_ops_create_app` while still picking up the rest of the family.
1738
+ // coupon / blueprints). Built on EVERY path — they hang off their own
1739
+ // explicit options, so a deployment that supplies a custom base
1740
+ // handler list still gets the domains it wired. A name already claimed
1741
+ // by the base list wins: that's how the cloud pod ships its own
1742
+ // `ggui_ops_create_app` while still picking up the rest of the family,
1743
+ // and it is what keeps the blueprint family single-registered when the
1744
+ // default handler set already built it from the same bundle.
1608
1745
  const baseHandlerNames = new Set(baseHandlers.map((h) => h.name));
1609
1746
  const opsBundleHandlers = buildOpsBundleHandlers(opts).filter((h) => !baseHandlerNames.has(h.name));
1610
1747
  const handlers = composeHandlersWithMounts(opsBundleHandlers.length > 0 ? [...baseHandlers, ...opsBundleHandlers] : baseHandlers, opts.mcpMounts);
@@ -1772,6 +1909,7 @@ export function createGguiServer(opts = {}) {
1772
1909
  buildResourceValidator({
1773
1910
  universalMcpPath: opts.universalMcpPath ?? "/mcp",
1774
1911
  perAppRouting: opts.perAppRouting,
1912
+ controlPath: CONTROL_PATH,
1775
1913
  }),
1776
1914
  };
1777
1915
  const oauthStorage = oauthConfig.storage ?? new InMemoryOAuthStorage();
@@ -1785,6 +1923,7 @@ export function createGguiServer(opts = {}) {
1785
1923
  oauthConfig,
1786
1924
  oauthStorage,
1787
1925
  universalMcpPath: opts.universalMcpPath ?? "/mcp",
1926
+ controlPath: CONTROL_PATH,
1788
1927
  ...(opts.perAppRouting !== undefined
1789
1928
  ? {
1790
1929
  perAppRouting: {
@@ -1811,6 +1950,7 @@ export function createGguiServer(opts = {}) {
1811
1950
  info,
1812
1951
  toolCount: handlers.length,
1813
1952
  readinessChecks: opts.readinessChecks ?? [],
1953
+ advisoryChecks: opts.advisoryChecks ?? [],
1814
1954
  getChannel: () => channelForHealth,
1815
1955
  ...(opts.threads !== undefined
1816
1956
  ? {
@@ -1868,6 +2008,18 @@ export function createGguiServer(opts = {}) {
1868
2008
  // (`connect-src 'none'`) blocks the iframe from fetching the
1869
2009
  // runtime bundle and opening the WebSocket.
1870
2010
  ...(opts.publicBaseUrl !== undefined ? { publicBaseUrl: opts.publicBaseUrl } : {}),
2011
+ // Live-channel origins for the static shell's CSP declaration:
2012
+ // the WS endpoint + its http-origin flip (the session-API base the
2013
+ // sseUrl/pollingUrl stamps derive from when `publicBaseUrl` is
2014
+ // unset — the cloud pod's posture, see compose.ts's Origin/Host
2015
+ // ruling). Cross-origin hosts (claude.ai) build the frame CSP from
2016
+ // the STATIC shell resource they mount; per-render declarations
2017
+ // never reach it, so without these entries every network rung of
2018
+ // the failover ladder (WS, SSE, HTTP polling) is `connect-src`-
2019
+ // blocked in the frame — observed live in #471 round 11.
2020
+ ...(mcpAppsEnabled
2021
+ ? { extraConnectUrls: [wsUrl, wsOriginToHttpOrigin(wsUrl)] }
2022
+ : {}),
1871
2023
  // Per-render self-contained shell registration. Only wired
1872
2024
  // when MCP Apps is on AND the render store is resolved —
1873
2025
  // both preconditions for `ggui_render.resultMeta` stamping a
@@ -1892,6 +2044,13 @@ export function createGguiServer(opts = {}) {
1892
2044
  ...(opts.theme !== undefined && opts.theme.source !== "default"
1893
2045
  ? { themeMode: opts.theme.mode }
1894
2046
  : {}),
2047
+ // The live pick reaches the read door too (ggui#539) — same
2048
+ // getter the render handler's deps carry, so the FIRST
2049
+ // resolution layer is identical across the tool-result
2050
+ // slice and the served shell.
2051
+ ...(opts.themeProvider !== undefined
2052
+ ? { themeProvider: opts.themeProvider }
2053
+ : {}),
1895
2054
  // Resume contract — registry-only fallback. Wired
1896
2055
  // when the blueprint vector store is available so the
1897
2056
  // resource handler can rehydrate a render-evicted
@@ -1930,10 +2089,13 @@ export function createGguiServer(opts = {}) {
1930
2089
  // shell that would never paint. Wiring both (the shape
1931
2090
  // this factory produces) also means a fault on one
1932
2091
  // degrades to the other instead of failing the read.
1933
- ...(opts.codeStore && opts.publicBaseUrl
2092
+ ...(opts.codeStore && codeBaseUrl !== undefined
1934
2093
  ? {
1935
2094
  codeStore: opts.codeStore,
1936
- codeBaseUrl: opts.publicBaseUrl,
2095
+ codeBaseUrl,
2096
+ ...(mintCodeModuleUrl !== undefined
2097
+ ? { mintCodeModuleUrl }
2098
+ : {}),
1937
2099
  }
1938
2100
  : {}),
1939
2101
  // Bind the app-metadata store so the resource
@@ -2000,6 +2162,7 @@ export function createGguiServer(opts = {}) {
2000
2162
  ...(opts.allowedKinds !== undefined ? { allowedKinds: opts.allowedKinds } : {}),
2001
2163
  ...(resolvedInstructions !== undefined ? { instructions: resolvedInstructions } : {}),
2002
2164
  ...(opts.extraResources !== undefined ? { extraResources: opts.extraResources } : {}),
2165
+ ...(opts.withholdResultMeta === true ? { withholdResultMeta: true } : {}),
2003
2166
  };
2004
2167
  // MCP wire endpoints (data plane: universal / per-app; control
2005
2168
  // plane: /control; plus any isolated services) — see
@@ -2014,6 +2177,7 @@ export function createGguiServer(opts = {}) {
2014
2177
  info,
2015
2178
  handlers,
2016
2179
  controlHandlers: controlService.handlers,
2180
+ controlOpsToolNames: controlService.opsToolNames,
2017
2181
  mcpServices,
2018
2182
  als,
2019
2183
  appIdFromIdentity,
@@ -2043,6 +2207,20 @@ export function createGguiServer(opts = {}) {
2043
2207
  ? { hashed: { path: hashedRuntimePath, source: runtimeBundleBytes } }
2044
2208
  : {}),
2045
2209
  });
2210
+ // Static shim assets (ggui#522 slice 2) — the fetchable twins of
2211
+ // the data-url import shims, served immutable under the runtime
2212
+ // bundle's content hash. Mounted whenever the build shipped them
2213
+ // (independent of the code store: the `/code` variant route is the
2214
+ // usual consumer, but a foreign composition may rewrite against
2215
+ // these shims itself).
2216
+ if (shimSources !== undefined && runtimeBundleHash !== undefined) {
2217
+ mountShimRoutes({
2218
+ app,
2219
+ urlPrefix: RUNTIME_SHIMS_URL_PREFIX,
2220
+ runtimeHash: runtimeBundleHash,
2221
+ shims: shimSources,
2222
+ });
2223
+ }
2046
2224
  }
2047
2225
  // R6 /state snapshot + R7 /events cursor-replay reads — see
2048
2226
  // `./api-renders-routes.ts` for the wsToken auth posture, tenancy
@@ -2063,16 +2241,41 @@ export function createGguiServer(opts = {}) {
2063
2241
  : {}),
2064
2242
  ...(opts.codeStore ? { codeStore: opts.codeStore } : {}),
2065
2243
  ...(opts.publicBaseUrl !== undefined ? { publicBaseUrl: opts.publicBaseUrl } : {}),
2244
+ // Asset host for the content-addressable URLs the /state read
2245
+ // composes (ggui#522) — session-API URLs keep the public origin.
2246
+ ...(opts.codeBaseUrl !== undefined ? { codeBaseUrl: opts.codeBaseUrl } : {}),
2247
+ ...(mintCodeModuleUrl !== undefined ? { mintCodeModuleUrl } : {}),
2066
2248
  ...(mintBootstrap ? { mintBootstrap } : {}),
2067
2249
  resolveRuntimeUrl: resolveRuntimeUrlForResultMeta,
2068
2250
  logger,
2069
2251
  });
2252
+ // SSE live stream (the ladder's middle rung) — same wsToken auth
2253
+ // posture as /state and /events; see `./api-renders-stream-route.ts`
2254
+ // for the framing contract. The channel is late-bound via the same
2255
+ // `() => channelForHealth` pattern as `stream.channelProvider`:
2256
+ // route mounting runs before `createGguiSessionChannelServer`, and
2257
+ // the mcpApps ⇒ renderChannel throw above guarantees a channel
2258
+ // exists by listen time.
2259
+ mountApiRendersStreamRoute({
2260
+ app,
2261
+ renderStore,
2262
+ secret: sharedTokenSecret,
2263
+ channelProvider: () => channelForHealth,
2264
+ logger,
2265
+ });
2070
2266
  }
2071
2267
  // Content-addressable code + contract-validator delivery — see
2072
2268
  // `./code-routes.ts` for the route contract (cache posture, CORS,
2073
2269
  // hash validation).
2074
2270
  if (opts.codeStore) {
2075
- mountCodeRoutes({ app, codeStore: opts.codeStore, logger });
2271
+ mountCodeRoutes({
2272
+ app,
2273
+ codeStore: opts.codeStore,
2274
+ logger,
2275
+ ...(codeModuleVariant !== undefined
2276
+ ? { moduleVariant: codeModuleVariant }
2277
+ : {}),
2278
+ });
2076
2279
  }
2077
2280
  // Pairing transport + auth bridge. Opt-in via `opts.pairing`. When
2078
2281
  // enabled with defaults, we mint an `InMemoryPairingService` and wire
@@ -2975,7 +3178,7 @@ export function createGguiServer(opts = {}) {
2975
3178
  * Build the {@link OAuthConfig.validateResource} callback from the
2976
3179
  * deployment shape (RFC 8707).
2977
3180
  *
2978
- * Two valid resource shapes are recognized:
3181
+ * Three valid resource shapes are recognized:
2979
3182
  * - **Universal** — exactly `${issuer}` when `universalMcpPath` is
2980
3183
  * `/`, otherwise `${issuer}${universalMcpPath}`. Cloud
2981
3184
  * `mcp.ggui.ai` collapses the bare-root case (the domain already
@@ -2983,6 +3186,11 @@ export function createGguiServer(opts = {}) {
2983
3186
  * - **Per-app** — `${issuer}${perAppRouting.pathPrefix}/<appId>`
2984
3187
  * where `<appId>` matches `perAppRouting.paramPattern`. Cloud
2985
3188
  * uses `/apps` prefix + `[A-Za-z0-9]{8}`.
3189
+ * - **Control plane** — `${issuer}${controlPath}` (ggui#505). A
3190
+ * host naming `/control` consents through the same ceremony and
3191
+ * receives a UNIVERSAL key: the consent page extracts an appId
3192
+ * only from the per-app shape, and control-plane ops are
3193
+ * account-level by design.
2986
3194
  *
2987
3195
  * Anything else returns `false` → /authorize emits `invalid_target`
2988
3196
  * per RFC 8707 §2 before showing consent. Defense-in-depth — the
@@ -2990,7 +3198,7 @@ export function createGguiServer(opts = {}) {
2990
3198
  * can trust the value because it's already been validated.
2991
3199
  */
2992
3200
  function buildResourceValidator(opts) {
2993
- const { universalMcpPath, perAppRouting } = opts;
3201
+ const { universalMcpPath, perAppRouting, controlPath } = opts;
2994
3202
  // Normalize a single trailing slash on the path-only-root form. RFC
2995
3203
  // 3986 §6.2.3 says `https://host` and `https://host/` are equivalent
2996
3204
  // when no other path segments follow. Some clients (claude.ai 2026-05)
@@ -3003,6 +3211,10 @@ function buildResourceValidator(opts) {
3003
3211
  const universalResource = universalMcpPath === "/" ? issuer : `${issuer}${universalMcpPath}`;
3004
3212
  if (stripTrailingSlash(resource) === stripTrailingSlash(universalResource))
3005
3213
  return true;
3214
+ if (controlPath !== undefined &&
3215
+ stripTrailingSlash(resource) === stripTrailingSlash(`${issuer}${controlPath}`)) {
3216
+ return true;
3217
+ }
3006
3218
  if (!perAppRouting)
3007
3219
  return false;
3008
3220
  const { paramPattern, pathPrefix = "" } = perAppRouting;