@ggui-ai/mcp-server 0.24.0 → 0.26.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/README.md +2 -2
- package/dist/api-renders-routes.d.ts +46 -5
- package/dist/api-renders-routes.d.ts.map +1 -1
- package/dist/api-renders-routes.js +98 -39
- package/dist/api-renders-stream-route.d.ts +2 -1
- package/dist/api-renders-stream-route.d.ts.map +1 -1
- package/dist/api-renders-stream-route.js +14 -2
- package/dist/build-mcp.d.ts +16 -0
- package/dist/build-mcp.d.ts.map +1 -1
- package/dist/build-mcp.js +82 -12
- package/dist/console-auth.d.ts +10 -10
- package/dist/console-auth.d.ts.map +1 -1
- package/dist/control-service.d.ts.map +1 -1
- package/dist/control-service.js +0 -3
- package/dist/first-seen.d.ts +14 -0
- package/dist/first-seen.d.ts.map +1 -0
- package/dist/first-seen.js +19 -0
- package/dist/ggui-session-channel/action-ingress.d.ts.map +1 -1
- package/dist/ggui-session-channel/action-ingress.js +96 -8
- package/dist/ggui-session-channel/channel-subscriptions.d.ts +0 -1
- package/dist/ggui-session-channel/channel-subscriptions.d.ts.map +1 -1
- package/dist/ggui-session-channel/channel-subscriptions.js +4 -1
- package/dist/ggui-session-channel/internal-types.d.ts +20 -12
- package/dist/ggui-session-channel/internal-types.d.ts.map +1 -1
- package/dist/ggui-session-channel/outbound.d.ts.map +1 -1
- package/dist/ggui-session-channel/outbound.js +18 -6
- package/dist/ggui-session-channel/stream-epoch.d.ts +61 -0
- package/dist/ggui-session-channel/stream-epoch.d.ts.map +1 -0
- package/dist/ggui-session-channel/stream-epoch.js +47 -0
- package/dist/ggui-session-channel/subscribe.d.ts +40 -53
- package/dist/ggui-session-channel/subscribe.d.ts.map +1 -1
- package/dist/ggui-session-channel/subscribe.js +288 -124
- package/dist/ggui-session-channel/subscriber-lifecycle.d.ts.map +1 -1
- package/dist/ggui-session-channel/subscriber-lifecycle.js +17 -7
- package/dist/ggui-session-channel.d.ts +43 -7
- package/dist/ggui-session-channel.d.ts.map +1 -1
- package/dist/ggui-session-channel.js +7 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/instructions-presets.d.ts +22 -21
- package/dist/instructions-presets.d.ts.map +1 -1
- package/dist/instructions-presets.js +38 -38
- package/dist/llm-backed-negotiator.d.ts +4 -37
- package/dist/llm-backed-negotiator.d.ts.map +1 -1
- package/dist/llm-backed-negotiator.js +28 -210
- package/dist/mcp-apps-outbound.d.ts +40 -91
- package/dist/mcp-apps-outbound.d.ts.map +1 -1
- package/dist/mcp-apps-outbound.js +100 -15
- package/dist/mcp-endpoint-routes.d.ts +1 -0
- package/dist/mcp-endpoint-routes.d.ts.map +1 -1
- package/dist/mcp-endpoint-routes.js +38 -4
- package/dist/runtime-bundle-hash.d.ts +7 -0
- package/dist/runtime-bundle-hash.d.ts.map +1 -1
- package/dist/runtime-bundle-hash.js +8 -2
- package/dist/runtime-bundle-route.d.ts.map +1 -1
- package/dist/runtime-bundle-route.js +15 -0
- package/dist/server.d.ts +143 -85
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +224 -110
- package/dist/storage.d.ts +9 -1
- package/dist/storage.d.ts.map +1 -1
- package/dist/storage.js +4 -3
- package/dist/view-proof-gate.d.ts +96 -0
- package/dist/view-proof-gate.d.ts.map +1 -0
- package/dist/view-proof-gate.js +167 -0
- package/package.json +14 -15
- package/dist/render-read-gate.d.ts +0 -50
- package/dist/render-read-gate.d.ts.map +0 -1
- package/dist/render-read-gate.js +0 -36
|
@@ -34,12 +34,13 @@
|
|
|
34
34
|
import { deriveBundleOrigins, deriveContractBundle, derivePublicEnvProjection, deriveRenderMeta, filterDescriptorsToContract, findBlueprintExact, resolveSliceTheme, spreadRenderMetaViewOntoSlice, wsOriginToHttpOrigin, } from "@ggui-ai/mcp-server-handlers/renders";
|
|
35
35
|
import { fontFaceRules } from "@ggui-ai/design/themes";
|
|
36
36
|
import { GGUI_WAVE_VERSION, RESOURCE_NOT_FOUND_MESSAGE, deriveContextDefault, isRecord, resolveAppGadgets, resourceReadErrorToJsonRpc, } from "@ggui-ai/protocol";
|
|
37
|
-
import { GGUI_RENDER_RESOURCE_MIME, GGUI_RENDER_RESOURCE_URI, GGUI_RENDER_SHELL_SCHEME_STYLE, GGUI_RENDER_SHELL_SURFACE, GGUI_SHELL_LOADING_INDICATOR_HTML, MCP_APPS_UI_CAPABILITY, MCP_APP_BOOTSTRAP_FAILED_TYPE, asGguiRenderBootstrap, composeSessionApiUrls, deriveContextName, escapeInlineScript, gguiShellHtml, parseEpochUri, toMcpAppEnvelope, } from "@ggui-ai/protocol/integrations/mcp-apps";
|
|
37
|
+
import { GGUI_RENDER_RESOURCE_MIME, GGUI_RENDER_RESOURCE_URI, GGUI_RENDER_SHELL_SCHEME_STYLE, GGUI_RENDER_SHELL_SURFACE, GGUI_SHELL_LOADING_INDICATOR_HTML, MCP_APPS_UI_CAPABILITY, MCP_APP_BOOTSTRAP_FAILED_TYPE, asGguiRenderBootstrap, composeSessionApiUrls, deriveContextName, escapeInlineScript, gguiShellHtml, parseEpochUri, runtimeBundleHashedNameSource, toMcpAppEnvelope, } from "@ggui-ai/protocol/integrations/mcp-apps";
|
|
38
38
|
import { ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
39
39
|
import { registerAppResource } from "@modelcontextprotocol/ext-apps/server";
|
|
40
40
|
import { createHash } from "node:crypto";
|
|
41
|
-
import {
|
|
41
|
+
import { RENDER_READ_APP_TRUST_OVER_SUBJECT, renderReadVerdict } from "@ggui-ai/mcp-server-handlers/renders";
|
|
42
42
|
import { DEFAULT_BUILDER_APP_ID } from "./auth.js";
|
|
43
|
+
import { RUNTIME_BUNDLE_PLAIN_NAME } from "./runtime-bundle-hash.js";
|
|
43
44
|
/**
|
|
44
45
|
* Thin-shell body served from `ui://ggui/render` (C8 pivot).
|
|
45
46
|
*
|
|
@@ -152,6 +153,12 @@ var rootEl=document.getElementById('ggui-root');
|
|
|
152
153
|
rootEl.style.cssText='display:flex;flex-direction:column;height:100%;min-height:300px;margin:0';
|
|
153
154
|
var mounted=false;
|
|
154
155
|
var lastEnvelope=null;
|
|
156
|
+
// ggui#1501: a content-hashed runtime URL -> its unhashed twin (same origin,
|
|
157
|
+
// same path, only the hash removed). Every server replica serves the twin no-cache, so
|
|
158
|
+
// a hash no serving replica has (rolling deploy, rollback, replay) retries it once.
|
|
159
|
+
var RT_HASHED=new RegExp(${JSON.stringify(runtimeBundleHashedNameSource(RUNTIME_BUNDLE_PLAIN_NAME))});
|
|
160
|
+
var RT_BASE=${JSON.stringify(RUNTIME_BUNDLE_PLAIN_NAME.slice(0, RUNTIME_BUNDLE_PLAIN_NAME.lastIndexOf(".")))};
|
|
161
|
+
function plainRuntimeTwin(u){var m=RT_HASHED.exec(u);return m?m[1]+RT_BASE+m[2]+m[3]:null;}
|
|
155
162
|
// Text color pairs with the shell surface: themed var when the runtime
|
|
156
163
|
// injected theme CSS, else the scheme-scoped pre-theme ink the shell's
|
|
157
164
|
// own scheme <style> defines (#662) — legible on whichever neutral
|
|
@@ -261,6 +268,10 @@ async function mountFromMeta(envelope){
|
|
|
261
268
|
// bundle responds with the right CORS headers (the iframe-runtime
|
|
262
269
|
// mount sets them). The self-contained shell already uses this
|
|
263
270
|
// pattern; legacy postMessage shell now matches it.
|
|
271
|
+
rootEl.innerHTML='';
|
|
272
|
+
loadRuntime(runtimeUrl,false);
|
|
273
|
+
}
|
|
274
|
+
function loadRuntime(url,isTwin){
|
|
264
275
|
try{
|
|
265
276
|
var s=document.createElement('script');
|
|
266
277
|
s.type='module';
|
|
@@ -272,14 +283,21 @@ async function mountFromMeta(envelope){
|
|
|
272
283
|
// console -- the bundle ships ACAO=* so credentialed mode is
|
|
273
284
|
// unnecessary.
|
|
274
285
|
s.crossOrigin='anonymous';
|
|
275
|
-
s.src=
|
|
286
|
+
s.src=url;
|
|
276
287
|
s.onload=function(){mounted=true;};
|
|
277
288
|
s.onerror=function(e){
|
|
289
|
+
// ggui#1501: a hashed URL gets ONE retry through its unhashed twin;
|
|
290
|
+
// the twin, or any other URL, fails as before.
|
|
291
|
+
var twin=isTwin?null:plainRuntimeTwin(url);
|
|
292
|
+
if(twin){
|
|
293
|
+
try{console.warn('[ggui] runtime bundle failed to load; retrying its unhashed twin once');}catch(_){}
|
|
294
|
+
loadRuntime(twin,true);
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
278
297
|
var msg='Runtime bundle failed to load: '+(e&&e.message||'script error');
|
|
279
298
|
showFailure('This view could not load','BUNDLE_FETCH_FAILED: '+msg,function(){mountFromMeta(lastEnvelope);});
|
|
280
299
|
postBootstrapFailed('BUNDLE_FETCH_FAILED',msg);
|
|
281
300
|
};
|
|
282
|
-
rootEl.innerHTML='';
|
|
283
301
|
document.body.appendChild(s);
|
|
284
302
|
}catch(e){
|
|
285
303
|
var msg='Runtime bundle failed to load: '+(e&&e.message||e);
|
|
@@ -432,14 +450,20 @@ startInit();
|
|
|
432
450
|
// The design scope root (`.ggui-rcr-*`) and the sandbox-proxy outer
|
|
433
451
|
// document stay `background-color: transparent` by design (so a host
|
|
434
452
|
// themeing the chat surface shows through AROUND the rendered content).
|
|
435
|
-
// But nothing painted the rendered-content document's OWN backdrop
|
|
436
|
-
//
|
|
437
|
-
//
|
|
438
|
-
// iframe
|
|
439
|
-
//
|
|
440
|
-
//
|
|
441
|
-
//
|
|
442
|
-
//
|
|
453
|
+
// But nothing painted the rendered-content document's OWN backdrop,
|
|
454
|
+
// and an unpainted iframe document is only see-through while its used
|
|
455
|
+
// `color-scheme` matches its embedder's: on a mismatch the browser
|
|
456
|
+
// paints the iframe's canvas opaque in the document's own scheme
|
|
457
|
+
// colour (white for light, near-black for dark). This shell declares
|
|
458
|
+
// `light dark`, so its used scheme follows the viewer's preference,
|
|
459
|
+
// not the host's — a dark host under a light-preferring viewer got a
|
|
460
|
+
// white canvas. That is what the 2026-05-30 bug saw as
|
|
461
|
+
// Chrome-dark-versus-Safari-white (`8b52c42ed`); measured 2026-09-26
|
|
462
|
+
// (ggui#1125) Safari 26.3 and Chrome 154 behave alike, per scheme,
|
|
463
|
+
// not per engine. The component itself themed correctly (its scoped
|
|
464
|
+
// vars resolve); only the page behind it diverged. Painting the served
|
|
465
|
+
// document's own surface here removes the dependency on the schemes
|
|
466
|
+
// matching.
|
|
443
467
|
//
|
|
444
468
|
// The paint is inline, so no stylesheet `background` rule can undo
|
|
445
469
|
// it — which is why the constant's `var()` chain opens with the
|
|
@@ -963,6 +987,7 @@ export function buildSelfContainedShell(opts) {
|
|
|
963
987
|
// input is renamed to `wsToken` on the slice for wire-field parity.
|
|
964
988
|
...(opts.wsUrl !== undefined ? { wsUrl: opts.wsUrl } : {}),
|
|
965
989
|
...(opts.token !== undefined ? { wsToken: opts.token } : {}),
|
|
990
|
+
...(opts.token !== undefined && opts.viewKey !== undefined ? { viewKey: opts.viewKey } : {}),
|
|
966
991
|
...(opts.expiresAt !== undefined ? { expiresAt: opts.expiresAt } : {}),
|
|
967
992
|
// Polling fallback URL — lights up `@ggui-ai/live-channel`'s
|
|
968
993
|
// events-polling transport when WS is unavailable. Absent ⇒
|
|
@@ -1030,7 +1055,11 @@ export function buildSelfContainedShell(opts) {
|
|
|
1030
1055
|
// the only layer that can detect a compositing host at runtime —
|
|
1031
1056
|
// drops the backdrop to transparent itself where that is right,
|
|
1032
1057
|
// and every other context keeps the per-browser-consistent paint.
|
|
1033
|
-
|
|
1058
|
+
// ggui#1501: a hashed runtime URL that 404s (a hash no serving replica has,
|
|
1059
|
+
// during a rolling deploy, after a rollback, or on a replay) retries its
|
|
1060
|
+
// unhashed twin once. A custom `runtime.path` with another filename, or a
|
|
1061
|
+
// foreign URL, is not the hashed default name and gets no retry.
|
|
1062
|
+
return gguiShellHtml(bootstrap, { background: "surface", runtimeBundlePlainName: RUNTIME_BUNDLE_PLAIN_NAME });
|
|
1034
1063
|
}
|
|
1035
1064
|
/**
|
|
1036
1065
|
* A `resources/read` on a render locator that cannot return a mount.
|
|
@@ -1250,6 +1279,19 @@ function pickComponentFromGguiSession(render) {
|
|
|
1250
1279
|
*
|
|
1251
1280
|
* @public
|
|
1252
1281
|
*/
|
|
1282
|
+
/**
|
|
1283
|
+
* Why the read door issues no view key on this read (ggui#1415), or
|
|
1284
|
+
* `undefined` when it issues one: the mount does not opt in, or the caller
|
|
1285
|
+
* is anonymous (or carries no auth source).
|
|
1286
|
+
*/
|
|
1287
|
+
function viewKeyWithheldAtReadDoor(opts) {
|
|
1288
|
+
if (opts.issueViewKeys !== true)
|
|
1289
|
+
return "not_a_view_mount";
|
|
1290
|
+
const authSource = opts.getContext?.()?.authSource;
|
|
1291
|
+
if (authSource === undefined || authSource === "anonymous")
|
|
1292
|
+
return "anonymous_mount";
|
|
1293
|
+
return undefined;
|
|
1294
|
+
}
|
|
1253
1295
|
export function registerGguiRenderResourceTemplate(server, opts) {
|
|
1254
1296
|
// TWO templates registered against the same handler core:
|
|
1255
1297
|
//
|
|
@@ -1488,8 +1530,19 @@ export function registerGguiRenderResourceTemplate(server, opts) {
|
|
|
1488
1530
|
requestId: "resource-read",
|
|
1489
1531
|
}
|
|
1490
1532
|
: callerCtx;
|
|
1491
|
-
|
|
1533
|
+
const verdict = renderReadVerdict(candidate, fallbackCtx);
|
|
1534
|
+
if (verdict.allowed) {
|
|
1535
|
+
// ggui#1553: rung 4 admitted an app credential to a subject-bound
|
|
1536
|
+
// session. Door, app, source; never the session or the subject.
|
|
1537
|
+
if (verdict.appTrustOverSubject) {
|
|
1538
|
+
opts.logger?.info(RENDER_READ_APP_TRUST_OVER_SUBJECT, {
|
|
1539
|
+
door: "resources_read",
|
|
1540
|
+
appId: candidate.appId,
|
|
1541
|
+
source: fallbackCtx?.authSource ?? null,
|
|
1542
|
+
});
|
|
1543
|
+
}
|
|
1492
1544
|
return true;
|
|
1545
|
+
}
|
|
1493
1546
|
opts.logger?.warn("render_resource_read_denied", {
|
|
1494
1547
|
sessionId,
|
|
1495
1548
|
rowAppId: candidate.appId,
|
|
@@ -1552,6 +1605,16 @@ export function registerGguiRenderResourceTemplate(server, opts) {
|
|
|
1552
1605
|
if (record.blueprintId === null) {
|
|
1553
1606
|
return { ok: false, failure: blueprintUnresolvable("the record names no blueprint") };
|
|
1554
1607
|
}
|
|
1608
|
+
// ggui#1405 — an ephemeral id was never registered: the render served
|
|
1609
|
+
// fresh code at a key bound to other code. Say that, rather than
|
|
1610
|
+
// calling a row that never existed "gone".
|
|
1611
|
+
if (record.blueprintIdentity === "ephemeral") {
|
|
1612
|
+
return {
|
|
1613
|
+
ok: false,
|
|
1614
|
+
failure: blueprintUnresolvable("the render served code no blueprint stores (an ephemeral identity)"),
|
|
1615
|
+
terminal: true,
|
|
1616
|
+
};
|
|
1617
|
+
}
|
|
1555
1618
|
const blueprint = await blueprintStore.get(record.blueprintId);
|
|
1556
1619
|
if (blueprint === null) {
|
|
1557
1620
|
return {
|
|
@@ -1816,9 +1879,25 @@ export function registerGguiRenderResourceTemplate(server, opts) {
|
|
|
1816
1879
|
let wsUrl;
|
|
1817
1880
|
let wsToken;
|
|
1818
1881
|
let wsExpiresAt;
|
|
1882
|
+
let wsViewKey;
|
|
1819
1883
|
if (opts.mintWsToken) {
|
|
1820
1884
|
try {
|
|
1821
|
-
|
|
1885
|
+
// ggui#1415: a view root, and its key, only on a key-issuing mount
|
|
1886
|
+
// and for a caller that is not anonymous; any other read mints the
|
|
1887
|
+
// live credential alone.
|
|
1888
|
+
const withheld = viewKeyWithheldAtReadDoor(opts);
|
|
1889
|
+
let minted;
|
|
1890
|
+
if (opts.mintViewRoot !== undefined && withheld === undefined) {
|
|
1891
|
+
const root = opts.mintViewRoot(sessionId, accessibleStored.appId);
|
|
1892
|
+
wsViewKey = root.viewKey;
|
|
1893
|
+
minted = root;
|
|
1894
|
+
}
|
|
1895
|
+
else {
|
|
1896
|
+
minted = opts.mintWsToken(sessionId, accessibleStored.appId);
|
|
1897
|
+
if (opts.mintViewRoot !== undefined && withheld !== undefined) {
|
|
1898
|
+
opts.logger?.info("view_key_not_issued", { sessionId, src: "read", reason: withheld });
|
|
1899
|
+
}
|
|
1900
|
+
}
|
|
1822
1901
|
wsUrl = minted.wsUrl;
|
|
1823
1902
|
wsToken = minted.token;
|
|
1824
1903
|
// Forward the token TTL so the iframe-runtime can degrade to
|
|
@@ -1898,6 +1977,7 @@ export function registerGguiRenderResourceTemplate(server, opts) {
|
|
|
1898
1977
|
wsUrl,
|
|
1899
1978
|
token: wsToken,
|
|
1900
1979
|
...(wsExpiresAt !== undefined ? { expiresAt: wsExpiresAt } : {}),
|
|
1980
|
+
...(wsViewKey !== undefined ? { viewKey: wsViewKey } : {}),
|
|
1901
1981
|
}
|
|
1902
1982
|
: {}),
|
|
1903
1983
|
// Layered theme — the SAME resolver the tool-result slice uses
|
|
@@ -2165,6 +2245,11 @@ export function registerGguiRenderResourceTemplate(server, opts) {
|
|
|
2165
2245
|
}
|
|
2166
2246
|
else {
|
|
2167
2247
|
failure = reminted.failure;
|
|
2248
|
+
// An entitled read of an ephemeral render stops here: the registry
|
|
2249
|
+
// holds OTHER code at this key (ggui#1405), so the fallback below
|
|
2250
|
+
// would mount the wrong card under this render's locator.
|
|
2251
|
+
if (reminted.terminal === true)
|
|
2252
|
+
throw new ResourceReadFailure(failure);
|
|
2168
2253
|
}
|
|
2169
2254
|
}
|
|
2170
2255
|
// Registry-only fallback: render is gone (TTL / restart) but the
|
|
@@ -46,6 +46,7 @@ interface PerAppRouting {
|
|
|
46
46
|
readonly paramPattern: string;
|
|
47
47
|
readonly pathPrefix?: string;
|
|
48
48
|
readonly authorize?: (urlAppId: string, identity: AuthResult) => Promise<void>;
|
|
49
|
+
readonly perAppOnlySources?: readonly AuthResult["source"][];
|
|
49
50
|
}
|
|
50
51
|
/**
|
|
51
52
|
* Return shape for an `errorMapper` hook — a domain error mapped onto
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mcp-endpoint-routes.d.ts","sourceRoot":"","sources":["../src/mcp-endpoint-routes.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AAEH,OAAO,EAKL,KAAK,SAAS,EACf,MAAM,mBAAmB,CAAC;AAC3B,OAAO,KAAK,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,0BAA0B,CAAC;AACxE,OAAO,KAAK,EAAE,cAAc,EAAE,aAAa,EAAE,MAAM,8BAA8B,CAAC;AAElF,OAAO,KAAK,EAAE,OAAO,EAAqB,MAAM,SAAS,CAAC;AAC1D,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AAE1D,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,KAAK,CAAC;AAEvC,OAAO,EAAkB,KAAK,qBAAqB,EAAE,KAAK,UAAU,EAAE,MAAM,gBAAgB,CAAC;AAM7F,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAC1C,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAGlD,mFAAmF;AACnF,UAAU,aAAa;IACrB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,SAAS,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,QAAQ,EAAE,UAAU,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;
|
|
1
|
+
{"version":3,"file":"mcp-endpoint-routes.d.ts","sourceRoot":"","sources":["../src/mcp-endpoint-routes.ts"],"names":[],"mappings":"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"}
|
|
@@ -284,6 +284,34 @@ export function mountMcpEndpoints(opts) {
|
|
|
284
284
|
// may give the refusal a structured JSON-RPC `data` (bounded to
|
|
285
285
|
// 401 / 403, see `mapAuthorizationRefusal`); otherwise — and for
|
|
286
286
|
// every mapping outside those bounds — the default-deny 403 stands.
|
|
287
|
+
// ggui#1482 — a credential source the deployment lists as
|
|
288
|
+
// per-app-only is valid only at its app's endpoint. Every mount
|
|
289
|
+
// this handler serves without an app in its URL (the universal
|
|
290
|
+
// route, any isolated service) never runs `authorize`, so it
|
|
291
|
+
// refuses such a credential before dispatch rather than serve it
|
|
292
|
+
// without the checks that live there. (The control plane's own
|
|
293
|
+
// federated refusal above answers first on `/control`.) Named in
|
|
294
|
+
// the message so a caller corrects its URL instead of retrying.
|
|
295
|
+
// Untyped by contract, like every auth-class refusal (SPEC §7.1):
|
|
296
|
+
// it is about the caller's credential, not the app's state, so it
|
|
297
|
+
// carries no `data` and no registry code names it.
|
|
298
|
+
if (!hasUrlAppId && perAppRouting?.perAppOnlySources?.includes(identity.source)) {
|
|
299
|
+
reqLogger.warn("per_app_only_source_refused", {
|
|
300
|
+
route: handlerOpts?.mountPath ?? universalMcpPath,
|
|
301
|
+
path: req.path,
|
|
302
|
+
source: identity.source,
|
|
303
|
+
appId: appIdFromIdentity(identity),
|
|
304
|
+
});
|
|
305
|
+
res.status(403).json({
|
|
306
|
+
jsonrpc: "2.0",
|
|
307
|
+
error: {
|
|
308
|
+
code: MCP_ERROR_CODES.UNAUTHORIZED,
|
|
309
|
+
message: "this credential is valid only at its app's endpoint",
|
|
310
|
+
},
|
|
311
|
+
id: null,
|
|
312
|
+
});
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
287
315
|
if (hasUrlAppId && perAppRouting?.authorize) {
|
|
288
316
|
try {
|
|
289
317
|
await perAppRouting.authorize(urlAppId, identity);
|
|
@@ -342,6 +370,7 @@ export function mountMcpEndpoints(opts) {
|
|
|
342
370
|
reqLogger.debug?.("mcp_request", { appId: ctx.appId });
|
|
343
371
|
const mcp = buildMcpServer(info, routeHandlers, () => als.getStore() ?? ctx, reqLogger, {
|
|
344
372
|
...buildMcpOptions,
|
|
373
|
+
...(handlerOpts?.issueViewKeys === true ? { issueViewKeys: true } : {}),
|
|
345
374
|
});
|
|
346
375
|
const transport = new StreamableHTTPServerTransport({
|
|
347
376
|
sessionIdGenerator: undefined,
|
|
@@ -405,10 +434,13 @@ export function mountMcpEndpoints(opts) {
|
|
|
405
434
|
// intersects ['agent','runtime']. Handlers without an explicit tag
|
|
406
435
|
// default to ['agent'], so every untagged handler is agent-callable.
|
|
407
436
|
const agentRouteHandlers = filterHandlersByAudience(handlers, DATA_PLANE_AUDIENCES);
|
|
408
|
-
|
|
437
|
+
// The data plane delivers views to app-credentialed callers, so its read
|
|
438
|
+
// door is the one that issues view keys (ggui#1415).
|
|
439
|
+
const agentMcpHandler = makeMcpHandler(agentRouteHandlers, { issueViewKeys: true });
|
|
409
440
|
// Control plane — anonymous-capable (design-time tools answer
|
|
410
441
|
// bearer-less) with each ops tool re-imposing auth for itself.
|
|
411
442
|
const controlMcpHandler = makeMcpHandler(controlHandlers, {
|
|
443
|
+
mountPath: CONTROL_PATH,
|
|
412
444
|
anonymous: true,
|
|
413
445
|
rejectFederated: true,
|
|
414
446
|
// The control service captures this set BEFORE stripAudience
|
|
@@ -425,8 +457,10 @@ export function mountMcpEndpoints(opts) {
|
|
|
425
457
|
// whose hostname already says "mcp" may serve it at `/` so the
|
|
426
458
|
// bare-root URL is the universal endpoint — no path repeat.
|
|
427
459
|
// Exposes audience tags ['agent', 'runtime'] — runtime tools stay
|
|
428
|
-
// routable on the same endpoint
|
|
429
|
-
//
|
|
460
|
+
// routable on the same endpoint and ARE listed on its `tools/list`;
|
|
461
|
+
// their `_meta.ui.visibility: ['app']` marker asks a conforming HOST to
|
|
462
|
+
// withhold them from the model (SPEC §4.7 host obligation, ggui#1414) —
|
|
463
|
+
// the server validates the marker's shape and enforces nothing.
|
|
430
464
|
app.post(universalMcpPath, agentMcpHandler);
|
|
431
465
|
// Per-app endpoint — only mounted when the deployment opts in
|
|
432
466
|
// via `perAppRouting`. The same handler reads `req.params[paramName]`
|
|
@@ -496,7 +530,7 @@ export function mountMcpEndpoints(opts) {
|
|
|
496
530
|
// builder-kind identity with `source: 'anonymous'`. Default
|
|
497
531
|
// (undefined / false) preserves the auth-required posture of
|
|
498
532
|
// every canonical route.
|
|
499
|
-
const svcMcpHandler = makeMcpHandler(svc.handlers, svc.anonymous ? { anonymous: true } :
|
|
533
|
+
const svcMcpHandler = makeMcpHandler(svc.handlers, svc.anonymous ? { anonymous: true, mountPath: svc.path } : { mountPath: svc.path });
|
|
500
534
|
app.post(svc.path, svcMcpHandler);
|
|
501
535
|
app.get(svc.path, methodNotAllowed);
|
|
502
536
|
app.delete(svc.path, methodNotAllowed);
|
|
@@ -19,6 +19,13 @@
|
|
|
19
19
|
* cache-busting across deploys; this is not an integrity check (SRI
|
|
20
20
|
* would be a separate, full-length hash).
|
|
21
21
|
*/
|
|
22
|
+
/**
|
|
23
|
+
* The runtime bundle's PLAIN filename (`iframe-runtime.js`): the name every
|
|
24
|
+
* server mounts `no-cache`, and the name the content hash is inserted into.
|
|
25
|
+
* The shells' one-shot fallback (ggui#1501) retries this name when a hashed
|
|
26
|
+
* URL fails.
|
|
27
|
+
*/
|
|
28
|
+
export declare const RUNTIME_BUNDLE_PLAIN_NAME: string;
|
|
22
29
|
/** Truncated content hash used in the bundle's immutable URL name. */
|
|
23
30
|
export declare function computeRuntimeBundleHash(bytes: Buffer): string;
|
|
24
31
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"runtime-bundle-hash.d.ts","sourceRoot":"","sources":["../src/runtime-bundle-hash.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAMH,sEAAsE;AACtE,wBAAgB,wBAAwB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAE9D;AAED;;;;;;;GAOG;AACH,wBAAgB,uBAAuB,CACrC,SAAS,EAAE,MAAM,EACjB,IAAI,EAAE,MAAM,EACZ,SAAS,EAAE,MAAM,GAChB,MAAM,CAQR;AAED;;;;;;;;GAQG;AACH,wBAAgB,6BAA6B,CAC3C,QAAQ,EAAE,MAAM,EAChB,UAAU,GAAE,MAA4B,GACvC,MAAM,
|
|
1
|
+
{"version":3,"file":"runtime-bundle-hash.d.ts","sourceRoot":"","sources":["../src/runtime-bundle-hash.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAMH;;;;;GAKG;AACH,eAAO,MAAM,yBAAyB,EAAE,MAEvC,CAAC;AAEF,sEAAsE;AACtE,wBAAgB,wBAAwB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAE9D;AAED;;;;;;;GAOG;AACH,wBAAgB,uBAAuB,CACrC,SAAS,EAAE,MAAM,EACjB,IAAI,EAAE,MAAM,EACZ,SAAS,EAAE,MAAM,GAChB,MAAM,CAQR;AAED;;;;;;;;GAQG;AACH,wBAAgB,6BAA6B,CAC3C,QAAQ,EAAE,MAAM,EAChB,UAAU,GAAE,MAA4B,GACvC,MAAM,CASR;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,wBAAwB,CACtC,UAAU,GAAE,MAA4B,GACvC,MAAM,GAAG,SAAS,CAMpB"}
|
|
@@ -22,6 +22,13 @@
|
|
|
22
22
|
import { createHash } from "node:crypto";
|
|
23
23
|
import fs from "node:fs";
|
|
24
24
|
import { RUNTIME_BUNDLE_FILE, RUNTIME_BUNDLE_URL_PATH } from "@ggui-ai/iframe-runtime/server";
|
|
25
|
+
/**
|
|
26
|
+
* The runtime bundle's PLAIN filename (`iframe-runtime.js`): the name every
|
|
27
|
+
* server mounts `no-cache`, and the name the content hash is inserted into.
|
|
28
|
+
* The shells' one-shot fallback (ggui#1501) retries this name when a hashed
|
|
29
|
+
* URL fails.
|
|
30
|
+
*/
|
|
31
|
+
export const RUNTIME_BUNDLE_PLAIN_NAME = RUNTIME_BUNDLE_URL_PATH.slice(RUNTIME_BUNDLE_URL_PATH.lastIndexOf("/") + 1);
|
|
25
32
|
/** Truncated content hash used in the bundle's immutable URL name. */
|
|
26
33
|
export function computeRuntimeBundleHash(bytes) {
|
|
27
34
|
return createHash("sha256").update(bytes).digest("hex").slice(0, 12);
|
|
@@ -61,8 +68,7 @@ export function resolveHashedRuntimeBundleUrl(plainUrl, bundleFile = RUNTIME_BUN
|
|
|
61
68
|
// Bundle not built / not shipped — mint the plain revalidated name.
|
|
62
69
|
return plainUrl;
|
|
63
70
|
}
|
|
64
|
-
|
|
65
|
-
return insertRuntimeBundleHash(plainUrl, computeRuntimeBundleHash(bytes), plainName);
|
|
71
|
+
return insertRuntimeBundleHash(plainUrl, computeRuntimeBundleHash(bytes), RUNTIME_BUNDLE_PLAIN_NAME);
|
|
66
72
|
}
|
|
67
73
|
/**
|
|
68
74
|
* The 12-hex runtime-bundle content hash for the bundle at
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"runtime-bundle-route.d.ts","sourceRoot":"","sources":["../src/runtime-bundle-route.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAEvC,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAG1C,UAAU,YAAY;IACpB,iCAAiC;IACjC,QAAQ,CAAC,GAAG,EAAE,OAAO,CAAC;IACtB,oDAAoD;IACpD,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,sDAAsD;IACtD,QAAQ,CAAC,iBAAiB,EAAE,MAAM,CAAC;IACnC,6DAA6D;IAC7D,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB;;;;;;;;;;OAUG;IACH,QAAQ,CAAC,MAAM,CAAC,EAAE;QAChB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;QACtB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;KACzB,CAAC;CACH;AAED;;;;GAIG;AACH,wBAAgB,uBAAuB,CAAC,IAAI,EAAE,YAAY,GAAG,IAAI,
|
|
1
|
+
{"version":3,"file":"runtime-bundle-route.d.ts","sourceRoot":"","sources":["../src/runtime-bundle-route.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAEvC,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAG1C,UAAU,YAAY;IACpB,iCAAiC;IACjC,QAAQ,CAAC,GAAG,EAAE,OAAO,CAAC;IACtB,oDAAoD;IACpD,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,sDAAsD;IACtD,QAAQ,CAAC,iBAAiB,EAAE,MAAM,CAAC;IACnC,6DAA6D;IAC7D,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB;;;;;;;;;;OAUG;IACH,QAAQ,CAAC,MAAM,CAAC,EAAE;QAChB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;QACtB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;KACzB,CAAC;CACH;AAED;;;;GAIG;AACH,wBAAgB,uBAAuB,CAAC,IAAI,EAAE,YAAY,GAAG,IAAI,CAuFhE"}
|
|
@@ -55,6 +55,21 @@ export function mountRuntimeBundleRoute(opts) {
|
|
|
55
55
|
}
|
|
56
56
|
if (existsSync(runtimeBundleFile)) {
|
|
57
57
|
app.get(runtimePath, (_req, res) => {
|
|
58
|
+
// ggui#1501: with the hashed twin mounted, every STAMPED runtime URL
|
|
59
|
+
// is hashed, so a load of the plain name is a shell's one-shot
|
|
60
|
+
// fallback (a hash no serving replica has: rolling deploy, rollback,
|
|
61
|
+
// replay), or a probe; a probe reads the same, and that is the
|
|
62
|
+
// count's stated gap. Counted on the response's finish, whatever its
|
|
63
|
+
// status: the name is `no-cache`, so a CDN revalidates it with a
|
|
64
|
+
// conditional GET on viewer loads, which keeps the count per load
|
|
65
|
+
// (a CDN's minimum TTL can coalesce a burst within it), and a 304 is
|
|
66
|
+
// a fallback as much as a 200 is. No user-agent field: behind a CDN
|
|
67
|
+
// the origin sees the CDN's own.
|
|
68
|
+
if (opts.hashed !== undefined) {
|
|
69
|
+
res.on("finish", () => {
|
|
70
|
+
logger.info("runtime_bundle_plain_served", { status: res.statusCode });
|
|
71
|
+
});
|
|
72
|
+
}
|
|
58
73
|
res.setHeader("Content-Type", "application/javascript; charset=utf-8");
|
|
59
74
|
// Short cache — operators iterating on the renderer want fresh
|
|
60
75
|
// copies after rebuild. Production long-term caching lives on
|