@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.
- package/dist/api-renders-routes.d.ts +21 -0
- package/dist/api-renders-routes.d.ts.map +1 -1
- package/dist/api-renders-routes.js +54 -28
- package/dist/api-renders-stream-route.d.ts +80 -0
- package/dist/api-renders-stream-route.d.ts.map +1 -0
- package/dist/api-renders-stream-route.js +311 -0
- package/dist/build-mcp.d.ts +48 -7
- package/dist/build-mcp.d.ts.map +1 -1
- package/dist/build-mcp.js +87 -6
- package/dist/code-module-variant.d.ts +150 -0
- package/dist/code-module-variant.d.ts.map +1 -0
- package/dist/code-module-variant.js +243 -0
- package/dist/code-routes.d.ts +12 -2
- package/dist/code-routes.d.ts.map +1 -1
- package/dist/code-routes.js +12 -2
- package/dist/console-session-routes.d.ts.map +1 -1
- package/dist/console-session-routes.js +11 -0
- package/dist/control-service.d.ts +29 -3
- package/dist/control-service.d.ts.map +1 -1
- package/dist/control-service.js +26 -2
- package/dist/ggui-session-channel/action-ingress.d.ts +2 -2
- package/dist/ggui-session-channel/action-ingress.d.ts.map +1 -1
- package/dist/ggui-session-channel/channel-subscriptions.d.ts +4 -4
- package/dist/ggui-session-channel/channel-subscriptions.d.ts.map +1 -1
- package/dist/ggui-session-channel/internal-types.d.ts +63 -9
- package/dist/ggui-session-channel/internal-types.d.ts.map +1 -1
- package/dist/ggui-session-channel/outbound.d.ts +15 -5
- package/dist/ggui-session-channel/outbound.d.ts.map +1 -1
- package/dist/ggui-session-channel/outbound.js +64 -24
- package/dist/ggui-session-channel/socket-router.d.ts +8 -3
- package/dist/ggui-session-channel/socket-router.d.ts.map +1 -1
- package/dist/ggui-session-channel/socket-router.js +6 -1
- package/dist/ggui-session-channel/subscribe.d.ts +48 -3
- package/dist/ggui-session-channel/subscribe.d.ts.map +1 -1
- package/dist/ggui-session-channel/subscribe.js +97 -36
- package/dist/ggui-session-channel/subscriber-lifecycle.d.ts +23 -13
- package/dist/ggui-session-channel/subscriber-lifecycle.d.ts.map +1 -1
- package/dist/ggui-session-channel/subscriber-lifecycle.js +24 -11
- package/dist/ggui-session-channel.d.ts +58 -11
- package/dist/ggui-session-channel.d.ts.map +1 -1
- package/dist/ggui-session-channel.js +55 -19
- package/dist/health-routes.d.ts +19 -3
- package/dist/health-routes.d.ts.map +1 -1
- package/dist/health-routes.js +26 -18
- package/dist/index.d.ts +6 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +13 -1
- package/dist/instructions-presets.js +10 -10
- package/dist/mcp-apps-outbound.d.ts +88 -11
- package/dist/mcp-apps-outbound.d.ts.map +1 -1
- package/dist/mcp-apps-outbound.js +470 -77
- package/dist/mcp-endpoint-routes.d.ts +23 -5
- package/dist/mcp-endpoint-routes.d.ts.map +1 -1
- package/dist/mcp-endpoint-routes.js +69 -1
- package/dist/oauth-as-routes.d.ts +11 -0
- package/dist/oauth-as-routes.d.ts.map +1 -1
- package/dist/oauth-as-routes.js +45 -1
- package/dist/oauth.d.ts.map +1 -1
- package/dist/oauth.js +8 -1
- package/dist/runtime-bundle-hash.d.ts +55 -0
- package/dist/runtime-bundle-hash.d.ts.map +1 -0
- package/dist/runtime-bundle-hash.js +85 -0
- package/dist/runtime-bundle-route.js +1 -1
- package/dist/server.d.ts +239 -61
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +355 -143
- 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 {
|
|
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
|
|
569
|
-
//
|
|
570
|
-
//
|
|
571
|
-
//
|
|
572
|
-
//
|
|
573
|
-
//
|
|
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
|
-
|
|
576
|
-
|
|
577
|
-
|
|
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
|
-
?
|
|
898
|
+
? computeRuntimeBundleHash(runtimeBundleBytes)
|
|
803
899
|
: undefined;
|
|
804
|
-
const insertHash = (urlOrPath) =>
|
|
805
|
-
|
|
806
|
-
|
|
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
|
-
//
|
|
1423
|
-
//
|
|
1424
|
-
//
|
|
1425
|
-
//
|
|
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 &&
|
|
1552
|
+
...(opts.codeStore && codeBaseUrl !== undefined
|
|
1430
1553
|
? {
|
|
1431
1554
|
codeStore: opts.codeStore,
|
|
1432
|
-
codeBaseUrl
|
|
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.
|
|
1553
|
-
//
|
|
1554
|
-
//
|
|
1555
|
-
//
|
|
1556
|
-
//
|
|
1557
|
-
//
|
|
1558
|
-
//
|
|
1559
|
-
// the
|
|
1560
|
-
//
|
|
1561
|
-
//
|
|
1562
|
-
//
|
|
1563
|
-
//
|
|
1564
|
-
//
|
|
1565
|
-
//
|
|
1566
|
-
//
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
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
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
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
|
|
1604
|
-
// options, so a deployment that supplies a custom base
|
|
1605
|
-
// still gets the domains it wired. A name already claimed
|
|
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 &&
|
|
2092
|
+
...(opts.codeStore && codeBaseUrl !== undefined
|
|
1934
2093
|
? {
|
|
1935
2094
|
codeStore: opts.codeStore,
|
|
1936
|
-
codeBaseUrl
|
|
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({
|
|
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
|
-
*
|
|
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;
|