@ggui-ai/mcp-server 0.25.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 +48 -20
- package/dist/console-auth.d.ts +10 -10
- package/dist/console-auth.d.ts.map +1 -1
- 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 +29 -17
- package/dist/ggui-session-channel/subscribe.d.ts.map +1 -1
- package/dist/ggui-session-channel/subscribe.js +247 -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 +13 -5
- package/dist/ggui-session-channel.d.ts.map +1 -1
- package/dist/ggui-session-channel.js +2 -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 +1 -36
- package/dist/llm-backed-negotiator.d.ts.map +1 -1
- package/dist/llm-backed-negotiator.js +0 -196
- package/dist/mcp-apps-outbound.d.ts +39 -90
- package/dist/mcp-apps-outbound.d.ts.map +1 -1
- package/dist/mcp-apps-outbound.js +45 -3
- package/dist/mcp-endpoint-routes.d.ts.map +1 -1
- package/dist/mcp-endpoint-routes.js +4 -1
- package/dist/server.d.ts +108 -33
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +181 -48
- 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/README.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# @ggui-ai/mcp-server
|
|
2
2
|
|
|
3
|
-
Self-hosted [MCP](https://modelcontextprotocol.io) server for the **
|
|
3
|
+
Self-hosted [MCP](https://modelcontextprotocol.io) server for the **GGUI protocol** —
|
|
4
4
|
the interface layer that lets AI agents describe UIs in natural language and have
|
|
5
|
-
|
|
5
|
+
GGUI generate ephemeral interfaces for them.
|
|
6
6
|
|
|
7
7
|
This package is the HTTP/MCP binding layer. It composes
|
|
8
8
|
[`@ggui-ai/mcp-server-handlers`](https://www.npmjs.com/package/@ggui-ai/mcp-server-handlers)
|
|
@@ -13,7 +13,27 @@
|
|
|
13
13
|
* wsToken-gated: same credential as the live-channel WS upgrade
|
|
14
14
|
* (`?wsToken=<token>` on `/ws`). Drift-free with the WS surface —
|
|
15
15
|
* the iframe-runtime already has the token from the bootstrap
|
|
16
|
-
* envelope
|
|
16
|
+
* envelope.
|
|
17
|
+
*
|
|
18
|
+
* Each read renews the token for its holder: a POSSESSION renewal
|
|
19
|
+
* (ggui#1496 part B, slice 2). The renewal carries its chain's root
|
|
20
|
+
* forward (`rootIat`: the presented token's, or its `iat` when it is
|
|
21
|
+
* itself a root) and its `exp` never passes `rootIat + refresh window`,
|
|
22
|
+
* so a chain of `/state` renewals ends one window after its root was
|
|
23
|
+
* minted, at the same second on every gate (for a token minted under the
|
|
24
|
+
* current window: one minted under a longer window, by an earlier config or
|
|
25
|
+
* another server sharing the secret, stays live on the other gates until
|
|
26
|
+
* its own `exp`). Past it, a new credential needs an authorized door, such
|
|
27
|
+
* as `ggui_runtime_refresh_ws_token`.
|
|
28
|
+
*
|
|
29
|
+
* Responses (/state):
|
|
30
|
+
* - 200 — the slice envelope, with the renewed trio and the
|
|
31
|
+
* token-bearing `pollingUrl` / `sseUrl`.
|
|
32
|
+
* - 401 — wsToken missing / invalid / wrong-scope.
|
|
33
|
+
* - 404 — sessionId does not resolve.
|
|
34
|
+
* - 410 — `wsToken expired`: past its `exp`, or its chain has no second
|
|
35
|
+
* left (the bound's second arrived after the expiry check, or the
|
|
36
|
+
* token was minted under a longer window).
|
|
17
37
|
*
|
|
18
38
|
* Distinct from `/r/:shortCode` (JSON branch):
|
|
19
39
|
* - `/r/...` was shortCode-gated (bearer-by-obscurity; anyone with
|
|
@@ -42,6 +62,15 @@
|
|
|
42
62
|
* `sinceSequence` is below the server's replay horizon OR strictly
|
|
43
63
|
* greater than `lastSequence` (cursor is from a stale deployment
|
|
44
64
|
* or the render was reset). Clients re-mount from /state.
|
|
65
|
+
*
|
|
66
|
+
* Log (/events): the first 200 per server process, session and kind logs
|
|
67
|
+
* `render_channel_polled_first {sessionId, appId, kind}`, so a view on the
|
|
68
|
+
* HTTP polling rung leaves a mount mark tagged with its app, as
|
|
69
|
+
* `render_channel_subscribed` does for the ws and sse rungs. `kind` is
|
|
70
|
+
* `cold-mount` for the boot fetch (`sinceSequence=0&limit=1`) and `poll`
|
|
71
|
+
* otherwise. It fires after every gate, so the `sessionId` is one the
|
|
72
|
+
* token's app owns, and never carries the query string (the wsToken
|
|
73
|
+
* rides there).
|
|
45
74
|
*/
|
|
46
75
|
import type { AppMetadataStore, CodeStore, GguiSessionStore } from "@ggui-ai/mcp-server-core";
|
|
47
76
|
import { type McpAppAiGguiRenderMeta } from "@ggui-ai/protocol/integrations/mcp-apps";
|
|
@@ -85,15 +114,27 @@ interface MountOptions {
|
|
|
85
114
|
readonly hash: string;
|
|
86
115
|
readonly base: string;
|
|
87
116
|
}) => string | undefined;
|
|
88
|
-
/**
|
|
89
|
-
|
|
117
|
+
/**
|
|
118
|
+
* `/state`'s one live-credential minter (ggui#1496 part B, slice 2): a
|
|
119
|
+
* possession renewal. It mints a ws token that carries the presented
|
|
120
|
+
* chain's `rootIat` forward, with its `exp` clamped to
|
|
121
|
+
* `rootIat + refresh window`, so a chain of `/state` renewals ends there.
|
|
122
|
+
* `undefined` means the chain has no second left, answered as the ordinary
|
|
123
|
+
* expiry.
|
|
124
|
+
*/
|
|
125
|
+
readonly mintChainedBootstrap: (sessionId: string, appId: string, rootIat: number) => {
|
|
90
126
|
wsUrl: string;
|
|
91
127
|
token: string;
|
|
92
128
|
expiresAt: string;
|
|
93
|
-
};
|
|
129
|
+
} | undefined;
|
|
94
130
|
/** Per-request runtime-bundle URL resolver (tunnel/proxy aware). */
|
|
95
131
|
readonly resolveRuntimeUrl: () => string;
|
|
96
|
-
/**
|
|
132
|
+
/**
|
|
133
|
+
* Structured logger for store-read failures, for the `/state`
|
|
134
|
+
* renewals refused because a chain expired (`state_chain_expired`),
|
|
135
|
+
* and for the first successful `/events` read per session and kind
|
|
136
|
+
* (`render_channel_polled_first`).
|
|
137
|
+
*/
|
|
97
138
|
readonly logger: Logger;
|
|
98
139
|
}
|
|
99
140
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"api-renders-routes.d.ts","sourceRoot":"","sources":["../src/api-renders-routes.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"api-renders-routes.d.ts","sourceRoot":"","sources":["../src/api-renders-routes.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAyEG;AAEH,OAAO,KAAK,EAAE,gBAAgB,EAAE,SAAS,EAAE,gBAAgB,EAAE,MAAM,0BAA0B,CAAC;AAQ9F,OAAO,EAGL,KAAK,sBAAsB,EAC5B,MAAM,yCAAyC,CAAC;AACjD,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAEvC,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAM1C,UAAU,YAAY;IACpB,iCAAiC;IACjC,QAAQ,CAAC,GAAG,EAAE,OAAO,CAAC;IACtB,+CAA+C;IAC/C,QAAQ,CAAC,WAAW,EAAE,gBAAgB,CAAC;IACvC,wEAAwE;IACxE,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,sEAAsE;IACtE,QAAQ,CAAC,gBAAgB,CAAC,EAAE,gBAAgB,CAAC;IAC7C,+DAA+D;IAC/D,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAC1B,0DAA0D;IAC1D,QAAQ,CAAC,SAAS,CAAC,EAAE,sBAAsB,CAAC,WAAW,CAAC,CAAC;IACzD,sEAAsE;IACtE,QAAQ,CAAC,SAAS,CAAC,EAAE,SAAS,CAAC;IAC/B,sEAAsE;IACtE,QAAQ,CAAC,aAAa,CAAC,EAAE,MAAM,CAAC;IAChC;;;;;;;OAOG;IACH,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAC9B;;;;;;OAMG;IACH,QAAQ,CAAC,iBAAiB,CAAC,EAAE,CAAC,IAAI,EAAE;QAClC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;QACtB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;QACtB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;KACvB,KAAK,MAAM,GAAG,SAAS,CAAC;IACzB;;;;;;;OAOG;IACH,QAAQ,CAAC,oBAAoB,EAAE,CAC7B,SAAS,EAAE,MAAM,EACjB,KAAK,EAAE,MAAM,EACb,OAAO,EAAE,MAAM,KACZ;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAA;KAAE,GAAG,SAAS,CAAC;IACrE,oEAAoE;IACpE,QAAQ,CAAC,iBAAiB,EAAE,MAAM,MAAM,CAAC;IACzC;;;;;OAKG;IACH,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACzB;AAED;;;;GAIG;AACH,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,YAAY,GAAG,IAAI,CAkZ9D"}
|
|
@@ -13,7 +13,27 @@
|
|
|
13
13
|
* wsToken-gated: same credential as the live-channel WS upgrade
|
|
14
14
|
* (`?wsToken=<token>` on `/ws`). Drift-free with the WS surface —
|
|
15
15
|
* the iframe-runtime already has the token from the bootstrap
|
|
16
|
-
* envelope
|
|
16
|
+
* envelope.
|
|
17
|
+
*
|
|
18
|
+
* Each read renews the token for its holder: a POSSESSION renewal
|
|
19
|
+
* (ggui#1496 part B, slice 2). The renewal carries its chain's root
|
|
20
|
+
* forward (`rootIat`: the presented token's, or its `iat` when it is
|
|
21
|
+
* itself a root) and its `exp` never passes `rootIat + refresh window`,
|
|
22
|
+
* so a chain of `/state` renewals ends one window after its root was
|
|
23
|
+
* minted, at the same second on every gate (for a token minted under the
|
|
24
|
+
* current window: one minted under a longer window, by an earlier config or
|
|
25
|
+
* another server sharing the secret, stays live on the other gates until
|
|
26
|
+
* its own `exp`). Past it, a new credential needs an authorized door, such
|
|
27
|
+
* as `ggui_runtime_refresh_ws_token`.
|
|
28
|
+
*
|
|
29
|
+
* Responses (/state):
|
|
30
|
+
* - 200 — the slice envelope, with the renewed trio and the
|
|
31
|
+
* token-bearing `pollingUrl` / `sseUrl`.
|
|
32
|
+
* - 401 — wsToken missing / invalid / wrong-scope.
|
|
33
|
+
* - 404 — sessionId does not resolve.
|
|
34
|
+
* - 410 — `wsToken expired`: past its `exp`, or its chain has no second
|
|
35
|
+
* left (the bound's second arrived after the expiry check, or the
|
|
36
|
+
* token was minted under a longer window).
|
|
17
37
|
*
|
|
18
38
|
* Distinct from `/r/:shortCode` (JSON branch):
|
|
19
39
|
* - `/r/...` was shortCode-gated (bearer-by-obscurity; anyone with
|
|
@@ -42,23 +62,60 @@
|
|
|
42
62
|
* `sinceSequence` is below the server's replay horizon OR strictly
|
|
43
63
|
* greater than `lastSequence` (cursor is from a stale deployment
|
|
44
64
|
* or the render was reset). Clients re-mount from /state.
|
|
65
|
+
*
|
|
66
|
+
* Log (/events): the first 200 per server process, session and kind logs
|
|
67
|
+
* `render_channel_polled_first {sessionId, appId, kind}`, so a view on the
|
|
68
|
+
* HTTP polling rung leaves a mount mark tagged with its app, as
|
|
69
|
+
* `render_channel_subscribed` does for the ws and sse rungs. `kind` is
|
|
70
|
+
* `cold-mount` for the boot fetch (`sinceSequence=0&limit=1`) and `poll`
|
|
71
|
+
* otherwise. It fires after every gate, so the `sessionId` is one the
|
|
72
|
+
* token's app owns, and never carries the query string (the wsToken
|
|
73
|
+
* rides there).
|
|
45
74
|
*/
|
|
46
|
-
import { verifyToken } from "@ggui-ai/mcp-server-core";
|
|
75
|
+
import { verifyToken, verifyWsTokenSignature } from "@ggui-ai/mcp-server-core";
|
|
47
76
|
import { deriveContractBundle, derivePublicEnvProjection, deriveRenderMeta, spreadRenderMetaViewOntoSlice, } from "@ggui-ai/mcp-server-handlers/renders";
|
|
48
77
|
import { composeSessionApiUrls, MCP_APP_AI_GGUI_RENDER_META_KEY, } from "@ggui-ai/protocol/integrations/mcp-apps";
|
|
78
|
+
import { createFirstSeen } from "./first-seen.js";
|
|
49
79
|
import { createPublicReadPreflight } from "./browser-cors.js";
|
|
80
|
+
/** How many (kind, session) pairs `/events` remembers for its first-success line. */
|
|
81
|
+
const POLLED_FIRST_CAPACITY = 10_000;
|
|
50
82
|
/**
|
|
51
83
|
* Mount `GET /api/sessions/:sessionId/state` +
|
|
52
84
|
* `GET /api/sessions/:sessionId/events` onto the express app.
|
|
53
85
|
* Returns nothing — the routes self-register.
|
|
54
86
|
*/
|
|
55
87
|
export function mountApiRendersRoutes(opts) {
|
|
56
|
-
const { app, renderStore, secret, appMetadataStore, themeId, themeMode, codeStore, publicBaseUrl, codeBaseUrl,
|
|
88
|
+
const { app, renderStore, secret, appMetadataStore, themeId, themeMode, codeStore, publicBaseUrl, codeBaseUrl, mintChainedBootstrap, resolveRuntimeUrl, logger, } = opts;
|
|
57
89
|
// The origin the content-addressable routes are reached at (asset
|
|
58
90
|
// host if fronted separately, else the public origin); the request
|
|
59
91
|
// host is the last resort for local/tunnel deployments.
|
|
60
92
|
const staticBase = codeBaseUrl ?? publicBaseUrl;
|
|
93
|
+
// Which (kind, session) pairs `/events` has already logged a first
|
|
94
|
+
// success for. Bounded, so a long-running process never grows with its
|
|
95
|
+
// session count; a session forgotten under that bound logs again.
|
|
96
|
+
const polledFirst = createFirstSeen(POLLED_FIRST_CAPACITY);
|
|
61
97
|
// ggui#1231 — a public `*` read owns its preflight (null-origin frames).
|
|
98
|
+
/**
|
|
99
|
+
* One line per `/state` renewal refused because its chain expired
|
|
100
|
+
* (ggui#1496 S6, ggui#1540): the presented token is past its `exp`, or
|
|
101
|
+
* its chain has no second left. Its signature is checked at any age, so
|
|
102
|
+
* only a token this server signed is counted, and the line carries ids
|
|
103
|
+
* only: the session and how old the chain's root is. A chain ends at its
|
|
104
|
+
* bound (the last renewal's `exp` is clamped to `rootIat + window`), so a
|
|
105
|
+
* `rootAgeSec` at or past the refresh window is a chain that reached it,
|
|
106
|
+
* and a smaller one a token that went unrenewed. Both answer the same 410,
|
|
107
|
+
* whose documented action is the authorized refresh.
|
|
108
|
+
*/
|
|
109
|
+
function logStateChainExpired(token) {
|
|
110
|
+
const signed = verifyWsTokenSignature(token, secret);
|
|
111
|
+
if (!signed.ok)
|
|
112
|
+
return;
|
|
113
|
+
const rootIat = signed.claims.rootIat ?? signed.claims.iat;
|
|
114
|
+
logger.info("state_chain_expired", {
|
|
115
|
+
sessionId: signed.claims.sessionId,
|
|
116
|
+
rootAgeSec: Math.max(0, Math.floor(Date.now() / 1000) - rootIat),
|
|
117
|
+
});
|
|
118
|
+
}
|
|
62
119
|
app.options("/api/sessions/:sessionId/state", createPublicReadPreflight());
|
|
63
120
|
app.get("/api/sessions/:sessionId/state", async (req, res) => {
|
|
64
121
|
// CORS on EVERY response, gates included: cross-origin frames can
|
|
@@ -85,6 +142,7 @@ export function mountApiRendersRoutes(opts) {
|
|
|
85
142
|
// surface a refresh-vs-rehandshake prompt instead of treating
|
|
86
143
|
// it as a hostile request.
|
|
87
144
|
if (verify.reason === "expired") {
|
|
145
|
+
logStateChainExpired(wsToken);
|
|
88
146
|
res.status(410).type("text/plain").send("wsToken expired");
|
|
89
147
|
return;
|
|
90
148
|
}
|
|
@@ -152,31 +210,36 @@ export function mountApiRendersRoutes(opts) {
|
|
|
152
210
|
// Silent — wrappers calling getPublicEnv throw clearly.
|
|
153
211
|
}
|
|
154
212
|
}
|
|
155
|
-
// Live-mode credential trio
|
|
156
|
-
//
|
|
157
|
-
//
|
|
158
|
-
//
|
|
159
|
-
//
|
|
160
|
-
//
|
|
161
|
-
//
|
|
162
|
-
//
|
|
163
|
-
//
|
|
164
|
-
//
|
|
165
|
-
|
|
213
|
+
// Live-mode credential trio, renewed on every /state read so the
|
|
214
|
+
// iframe-runtime gets a live token + the host learns `wsUrl` (the
|
|
215
|
+
// CSP-permitted WebSocket origin). Without `wsUrl`, any caller of
|
|
216
|
+
// /state — including the restore-bootstrap path in every host
|
|
217
|
+
// (claude.ai, ChatGPT, our sample-agent) — gets a slice whose CSP
|
|
218
|
+
// `connect-src` omits the `ws://` scheme, the browser blocks the
|
|
219
|
+
// upgrade, and props_update never reaches the iframe. The
|
|
220
|
+
// render-commit handler stamps the same trio on its resultMeta.
|
|
221
|
+
//
|
|
222
|
+
// This read is a POSSESSION renewal (ggui#1496 part B, slice 2): the
|
|
223
|
+
// presented token is the only credential, so the renewal carries the
|
|
224
|
+
// chain's root forward (its `rootIat`, or its `iat` when it is itself
|
|
225
|
+
// a root) and never outlives `rootIat + refresh window`. A chain with
|
|
226
|
+
// no second left is the ordinary expiry: the same 410 and body as a
|
|
227
|
+
// token past its `exp`, whose documented action is the authorized
|
|
228
|
+
// refresh (`ggui_runtime_refresh_ws_token`).
|
|
229
|
+
const liveTrio = mintChainedBootstrap(stored.id, stored.appId, verify.claims.rootIat ?? verify.claims.iat);
|
|
230
|
+
if (liveTrio === undefined) {
|
|
231
|
+
logStateChainExpired(wsToken);
|
|
232
|
+
res.status(410).type("text/plain").send("wsToken expired");
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
166
235
|
// Token-bearing session-API URL pair (pollingUrl → /events,
|
|
167
236
|
// sseUrl → /stream), composed via the protocol's ONE composer so
|
|
168
237
|
// this surface cannot drift from the render/update resultMeta
|
|
169
|
-
// stamping.
|
|
170
|
-
// both URLs embed the fresh long-TTL token, so a minter-less
|
|
171
|
-
// deployment honestly omits them (the old unconditional
|
|
172
|
-
// token-less `/state`-shaped pollingUrl could only 401 through
|
|
173
|
-
// the iframe-runtime's /events composer).
|
|
238
|
+
// stamping. Both embed the renewed token.
|
|
174
239
|
const sessionApiBase = publicBaseUrl
|
|
175
240
|
? publicBaseUrl.replace(/\/$/, "")
|
|
176
241
|
: `${req.protocol}://${req.get("host") ?? ""}`;
|
|
177
|
-
const sessionApiUrls =
|
|
178
|
-
? composeSessionApiUrls(sessionApiBase, stored.id, liveTrio.token)
|
|
179
|
-
: undefined;
|
|
242
|
+
const sessionApiUrls = composeSessionApiUrls(sessionApiBase, stored.id, liveTrio.token);
|
|
180
243
|
// Static-component delivery via codeUrl (the same content-addressable
|
|
181
244
|
// channel the code routes serve). Polling clients are render-capable
|
|
182
245
|
// and need the URL to mount/refresh the static-component variant.
|
|
@@ -210,13 +273,8 @@ export function mountApiRendersRoutes(opts) {
|
|
|
210
273
|
catch {
|
|
211
274
|
// Silent — the caller falls back to live-mode delivery, and
|
|
212
275
|
// here that fallback is structural rather than hoped for:
|
|
213
|
-
// `
|
|
214
|
-
//
|
|
215
|
-
// + a token secret, which are the same preconditions that
|
|
216
|
-
// assign the minter. So a reader of THIS file cannot see the
|
|
217
|
-
// guarantee — it lives at the mount site, and it is what
|
|
218
|
-
// keeps the silence honest. Lose that pairing and this catch
|
|
219
|
-
// becomes a swallow.
|
|
276
|
+
// `mintChainedBootstrap` is required on the options type and
|
|
277
|
+
// this response always carries the live trio minted above.
|
|
220
278
|
}
|
|
221
279
|
try {
|
|
222
280
|
const bundle = await deriveContractBundle(render);
|
|
@@ -241,16 +299,11 @@ export function mountApiRendersRoutes(opts) {
|
|
|
241
299
|
sessionId: stored.id,
|
|
242
300
|
appId: stored.appId,
|
|
243
301
|
runtimeUrl: resolveRuntimeUrl(),
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
}
|
|
250
|
-
: {}),
|
|
251
|
-
...(sessionApiUrls !== undefined
|
|
252
|
-
? { pollingUrl: sessionApiUrls.pollingUrl, sseUrl: sessionApiUrls.sseUrl }
|
|
253
|
-
: {}),
|
|
302
|
+
wsUrl: liveTrio.wsUrl,
|
|
303
|
+
wsToken: liveTrio.token,
|
|
304
|
+
expiresAt: liveTrio.expiresAt,
|
|
305
|
+
pollingUrl: sessionApiUrls.pollingUrl,
|
|
306
|
+
sseUrl: sessionApiUrls.sseUrl,
|
|
254
307
|
...(themeId !== undefined ? { themeId } : {}),
|
|
255
308
|
...(themeMode !== undefined ? { themeMode } : {}),
|
|
256
309
|
...(statePublicEnv !== undefined && Object.keys(statePublicEnv).length > 0
|
|
@@ -402,6 +455,12 @@ export function mountApiRendersRoutes(opts) {
|
|
|
402
455
|
// (Wave 7 of flatten-render-identity, 2026-05-28). The store
|
|
403
456
|
// returns events in protocol-canonical shape (seq + type +
|
|
404
457
|
// timestamp[ISO] + data); no projection needed.
|
|
458
|
+
// The polling rung's mount mark (see the header): once per server
|
|
459
|
+
// process, session and kind, and only here, past every gate.
|
|
460
|
+
const kind = sinceSequence === 0 && limit === 1 ? "cold-mount" : "poll";
|
|
461
|
+
if (polledFirst.first(`${kind}:${sessionId}`)) {
|
|
462
|
+
logger.info("render_channel_polled_first", { sessionId, appId: stored.appId, kind });
|
|
463
|
+
}
|
|
405
464
|
res.setHeader("Cache-Control", "no-store, no-cache, must-revalidate");
|
|
406
465
|
res.setHeader("Content-Type", "application/json; charset=utf-8");
|
|
407
466
|
res.status(200).json({
|
|
@@ -31,7 +31,8 @@
|
|
|
31
31
|
* Cursor spaces: `Last-Event-ID` (browser-stamped on auto-reconnect)
|
|
32
32
|
* WINS over `?sinceSequence=`; the query seeds the first connect
|
|
33
33
|
* (EventSource cannot set headers). `?fromSeq=` mirrors
|
|
34
|
-
* `SubscribePayload.fromSeq` for the stream-buffer plane
|
|
34
|
+
* `SubscribePayload.fromSeq` for the stream-buffer plane (and
|
|
35
|
+
* `?fromEpoch=` its `fromEpoch`, ggui#1531) — note it is
|
|
35
36
|
* frozen in the EventSource URL, so duplicate `data` frames after an
|
|
36
37
|
* auto-reconnect are expected; ledger + stream deliveries are
|
|
37
38
|
* documented at-least-once and clients dedupe by seq.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"api-renders-stream-route.d.ts","sourceRoot":"","sources":["../src/api-renders-stream-route.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"api-renders-stream-route.d.ts","sourceRoot":"","sources":["../src/api-renders-stream-route.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6CG;AAEH,OAAO,KAAK,EAAc,gBAAgB,EAAE,MAAM,0BAA0B,CAAC;AAG7E,OAAO,KAAK,EAAE,OAAO,EAAY,MAAM,SAAS,CAAC;AACjD,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,2BAA2B,CAAC;AAE1E,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAG1C;;;;GAIG;AACH,eAAO,MAAM,gBAAgB,QAAS,CAAC;AA2DvC,MAAM,WAAW,iCAAiC;IAChD,iCAAiC;IACjC,QAAQ,CAAC,GAAG,EAAE,OAAO,CAAC;IACtB,2DAA2D;IAC3D,QAAQ,CAAC,WAAW,EAAE,gBAAgB,CAAC;IACvC,wEAAwE;IACxE,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB;;;;;OAKG;IACH,QAAQ,CAAC,eAAe,EAAE,MAAM,wBAAwB,GAAG,IAAI,CAAC;IAChE,0EAA0E;IAC1E,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAC9B,yBAAyB;IACzB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACzB;AAED;;;GAGG;AACH,wBAAgB,0BAA0B,CAAC,IAAI,EAAE,iCAAiC,GAAG,IAAI,CAuNxF"}
|
|
@@ -31,7 +31,8 @@
|
|
|
31
31
|
* Cursor spaces: `Last-Event-ID` (browser-stamped on auto-reconnect)
|
|
32
32
|
* WINS over `?sinceSequence=`; the query seeds the first connect
|
|
33
33
|
* (EventSource cannot set headers). `?fromSeq=` mirrors
|
|
34
|
-
* `SubscribePayload.fromSeq` for the stream-buffer plane
|
|
34
|
+
* `SubscribePayload.fromSeq` for the stream-buffer plane (and
|
|
35
|
+
* `?fromEpoch=` its `fromEpoch`, ggui#1531) — note it is
|
|
35
36
|
* frozen in the EventSource URL, so duplicate `data` frames after an
|
|
36
37
|
* auto-reconnect are expected; ledger + stream deliveries are
|
|
37
38
|
* documented at-least-once and clients dedupe by seq.
|
|
@@ -172,6 +173,11 @@ export function mountApiRendersStreamRoute(opts) {
|
|
|
172
173
|
}
|
|
173
174
|
fromSeq = parsed;
|
|
174
175
|
}
|
|
176
|
+
// The epoch `fromSeq` was counted in (ggui#1531). Opaque and
|
|
177
|
+
// server-minted: any non-empty value is compared as it is, and one
|
|
178
|
+
// that does not match the session's replays everything retained.
|
|
179
|
+
const fromEpochRaw = req.query["fromEpoch"];
|
|
180
|
+
const fromEpoch = typeof fromEpochRaw === "string" && fromEpochRaw.length > 0 ? fromEpochRaw : undefined;
|
|
175
181
|
// Last-Event-ID (browser-stamped on auto-reconnect) WINS over the
|
|
176
182
|
// query seed. Malformed header → warn + fall back to the query
|
|
177
183
|
// (never a 4xx: the header is browser-controlled, and failing the
|
|
@@ -291,8 +297,11 @@ export function mountApiRendersStreamRoute(opts) {
|
|
|
291
297
|
appId: stored.appId,
|
|
292
298
|
identity,
|
|
293
299
|
sink,
|
|
300
|
+
// The stream is wsToken-only; the identity above is synthetic.
|
|
301
|
+
credential: "ws_token",
|
|
294
302
|
...(sinceSequence !== undefined ? { sinceSequence } : {}),
|
|
295
303
|
...(fromSeq !== undefined ? { fromSeq } : {}),
|
|
304
|
+
...(fromEpoch !== undefined ? { fromEpoch } : {}),
|
|
296
305
|
});
|
|
297
306
|
detachFn = detach;
|
|
298
307
|
if (torn) {
|
|
@@ -305,7 +314,10 @@ export function mountApiRendersStreamRoute(opts) {
|
|
|
305
314
|
// Lost race with render eviction (the pre-gate passed moments
|
|
306
315
|
// ago) or a store failure inside the subscribe tail. Headers are
|
|
307
316
|
// out, so no status rewrite — end the stream; the reconnect gets
|
|
308
|
-
// the authoritative pre-gate verdict.
|
|
317
|
+
// the authoritative pre-gate verdict. A tail that failed after
|
|
318
|
+
// registering has already unregistered its subscriber and ended
|
|
319
|
+
// the sink with `internal_error` (ggui#1528); `teardown` has no
|
|
320
|
+
// handle to add, and a second `end` is a no-op.
|
|
309
321
|
logger.warn("sse_attach_failed", { sessionId, error: String(err) });
|
|
310
322
|
teardown();
|
|
311
323
|
sink.end("service_restart");
|
package/dist/build-mcp.d.ts
CHANGED
|
@@ -13,12 +13,28 @@ import { type ZodRawShape } from 'zod';
|
|
|
13
13
|
import { type HandlerContext, type SharedHandler } from '@ggui-ai/mcp-server-handlers';
|
|
14
14
|
import type { Logger } from './logger.js';
|
|
15
15
|
import { type LoadingIndicatorOption, type GguiRenderResourceTemplateOptions } from './mcp-apps-outbound.js';
|
|
16
|
+
import { type ViewProofGate } from './view-proof-gate.js';
|
|
16
17
|
export interface ServerInfo {
|
|
17
18
|
readonly name: string;
|
|
18
19
|
readonly version: string;
|
|
19
20
|
readonly description?: string;
|
|
20
21
|
}
|
|
21
22
|
export interface BuildMcpServerOptions {
|
|
23
|
+
/**
|
|
24
|
+
* The view-origin proof's measuring gate (ggui#1415), built once per
|
|
25
|
+
* server with the secret its ws envelopes are signed with. For a tool
|
|
26
|
+
* that declares a view proof, the gate verifies the call's proof before
|
|
27
|
+
* the handler runs, puts the verdict on the handler's context and names
|
|
28
|
+
* it on the `tool_invoked` line. It never refuses or fails a call.
|
|
29
|
+
* Absent: a declared call's line says `viewProofUnverifiable: true`.
|
|
30
|
+
*/
|
|
31
|
+
readonly viewProofGate?: ViewProofGate;
|
|
32
|
+
/**
|
|
33
|
+
* Whether this mount's read door (`resources/read` of a render
|
|
34
|
+
* locator) may issue a view key (ggui#1415). Set only for the data
|
|
35
|
+
* plane, the mount that delivers a view to an app-credentialed caller.
|
|
36
|
+
*/
|
|
37
|
+
readonly issueViewKeys?: boolean;
|
|
22
38
|
/**
|
|
23
39
|
* When set, register the MCP Apps outbound wiring on every fresh
|
|
24
40
|
* server instance — advertises the `io.modelcontextprotocol/ui`
|
package/dist/build-mcp.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"build-mcp.d.ts","sourceRoot":"","sources":["../src/build-mcp.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAIpE,OAAO,EAAK,KAAK,WAAW,EAAE,MAAM,KAAK,CAAC;AAC1C,OAAO,EAEL,KAAK,cAAc,EACnB,KAAK,aAAa,EACnB,MAAM,8BAA8B,CAAC;AACtC,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAE1C,OAAO,EACL,KAAK,sBAAsB,EAE3B,KAAK,iCAAiC,EACvC,MAAM,wBAAwB,CAAC;
|
|
1
|
+
{"version":3,"file":"build-mcp.d.ts","sourceRoot":"","sources":["../src/build-mcp.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAIpE,OAAO,EAAK,KAAK,WAAW,EAAE,MAAM,KAAK,CAAC;AAC1C,OAAO,EAEL,KAAK,cAAc,EACnB,KAAK,aAAa,EACnB,MAAM,8BAA8B,CAAC;AACtC,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAE1C,OAAO,EACL,KAAK,sBAAsB,EAE3B,KAAK,iCAAiC,EACvC,MAAM,wBAAwB,CAAC;AAChC,OAAO,EAAoB,KAAK,aAAa,EAAE,MAAM,sBAAsB,CAAC;AAE5E,MAAM,WAAW,UAAU;IACzB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;CAC/B;AAED,MAAM,WAAW,qBAAqB;IACpC;;;;;;;OAOG;IACH,QAAQ,CAAC,aAAa,CAAC,EAAE,aAAa,CAAC;IACvC;;;;OAIG;IACH,QAAQ,CAAC,aAAa,CAAC,EAAE,OAAO,CAAC;IACjC;;;;;;;;;;OAUG;IACH,QAAQ,CAAC,eAAe,CAAC,EAAE,OAAO,CAAC;IACnC;;;;OAIG;IACH,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAC5B,8DAA8D;IAC9D,QAAQ,CAAC,gBAAgB,CAAC,EAAE,sBAAsB,CAAC;IACnD;;;;;;;;;;OAUG;IACH,QAAQ,CAAC,aAAa,CAAC,EAAE,iCAAiC,CAAC;IAC3D;;;;;;;;;OASG;IACH,QAAQ,CAAC,aAAa,CAAC,EAAE,MAAM,CAAC;IAChC;;;;;;;;OAQG;IACH,QAAQ,CAAC,gBAAgB,CAAC,EAAE,SAAS,CAAC,MAAM,GAAG,SAAS,CAAC,EAAE,CAAC;IAC5D;;;;;;;;;;;;;;;;;;OAkBG;IACH,QAAQ,CAAC,YAAY,CAAC,EAAE,aAAa,CAAC,KAAK,GAAG,MAAM,GAAG,SAAS,CAAC,CAAC;IAElE;;;;;;;;;;;;;;OAcG;IACH,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,CAAC;IAE/B;;;;;;;;;;;;;;;;;;OAkBG;IACH,QAAQ,CAAC,cAAc,CAAC,EAAE,aAAa,CAAC,CAAC,MAAM,EAAE,SAAS,KAAK,IAAI,CAAC,CAAC;IAErE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OAoCG;IACH,QAAQ,CAAC,kBAAkB,CAAC,EAAE,OAAO,CAAC;CACvC;AAqPD,wBAAgB,cAAc,CAC5B,IAAI,EAAE,UAAU,EAChB,QAAQ,EAAE,aAAa,CAAC,aAAa,CAAC,WAAW,EAAE,WAAW,CAAC,CAAC,EAChE,UAAU,EAAE,MAAM,cAAc,EAChC,MAAM,EAAE,MAAM,EACd,IAAI,GAAE,qBAA0B,GAC/B,SAAS,CAkSX"}
|
package/dist/build-mcp.js
CHANGED
|
@@ -15,6 +15,7 @@ import { z } from 'zod';
|
|
|
15
15
|
import { isHandlerFailure, } from '@ggui-ai/mcp-server-handlers';
|
|
16
16
|
import { GGUI_RENDER_RESOURCE_URI } from '@ggui-ai/protocol/integrations/mcp-apps';
|
|
17
17
|
import { installMcpAppsOutbound, } from './mcp-apps-outbound.js';
|
|
18
|
+
import { runViewProofGate } from './view-proof-gate.js';
|
|
18
19
|
/**
|
|
19
20
|
* Build a fresh MCP server with every handler registered.
|
|
20
21
|
*
|
|
@@ -147,6 +148,12 @@ function classifyFailurePayload(payload) {
|
|
|
147
148
|
* request's signal is aborted by the time the line is written — a superset
|
|
148
149
|
* of "cancelled mid-poll", read at log time, never a false negative. A
|
|
149
150
|
* committed dispatch adds `consumerPresent`, the doorbell's gate.
|
|
151
|
+
*
|
|
152
|
+
* `ggui_runtime_sync_context` is not a session tool here: a refused sync's
|
|
153
|
+
* session is only claimed. Its line names a refusal (`ok: false` and the
|
|
154
|
+
* `code`) and nothing else from this function. A tool that declares a view
|
|
155
|
+
* proof (ggui#1415) also carries the measuring gate's fields and
|
|
156
|
+
* `authSource` on every one of its lines (`view-proof-gate.ts`).
|
|
150
157
|
*/
|
|
151
158
|
const SESSION_TOOLS = new Set([
|
|
152
159
|
'ggui_runtime_pull',
|
|
@@ -159,6 +166,14 @@ const SESSION_TOOLS = new Set([
|
|
|
159
166
|
/** ggui#1474 — session tools whose logged session is the OUTPUT's (the gated or committed one), not the input's. */
|
|
160
167
|
const OUTPUT_SESSION_TOOLS = new Set(['ggui_amend', 'ggui_update', 'ggui_render']);
|
|
161
168
|
function sessionFields(tool, input, output, ctx) {
|
|
169
|
+
// ggui#1415: a sync names its refusal (`ok: false` and the code) so a
|
|
170
|
+
// refused call is countable. Its session is not logged as owned: a
|
|
171
|
+
// refused call's session is only claimed (the gate's `claimedSessionId`).
|
|
172
|
+
if (tool === 'ggui_runtime_sync_context') {
|
|
173
|
+
if (output['ok'] === true)
|
|
174
|
+
return {};
|
|
175
|
+
return typeof output['code'] === 'string' ? { ok: false, code: output['code'] } : { ok: false };
|
|
176
|
+
}
|
|
162
177
|
if (!SESSION_TOOLS.has(tool))
|
|
163
178
|
return {};
|
|
164
179
|
const sessionId = OUTPUT_SESSION_TOOLS.has(tool) ? output['sessionId'] : input['sessionId'];
|
|
@@ -217,12 +232,14 @@ function claimedSessionField(tool, input) {
|
|
|
217
232
|
};
|
|
218
233
|
}
|
|
219
234
|
export function buildMcpServer(info, handlers, getContext, logger, opts = {}) {
|
|
235
|
+
// `instructions` is a ServerOptions field (the SECOND argument): the
|
|
236
|
+
// SDK sends it in the `initialize` result. In the first argument (the
|
|
237
|
+
// Implementation) it was dropped, and no host ever received it (ggui#1579).
|
|
220
238
|
const server = new McpServer({
|
|
221
239
|
name: info.name,
|
|
222
240
|
version: info.version,
|
|
223
241
|
...(info.description ? { description: info.description } : {}),
|
|
224
|
-
|
|
225
|
-
});
|
|
242
|
+
}, opts.instructions ? { instructions: opts.instructions } : undefined);
|
|
226
243
|
// Content-addressed shell URI (stale-shell bust) — when MCP Apps
|
|
227
244
|
// outbound wiring registers the shell, declarations advertising the
|
|
228
245
|
// BARE `ui://ggui/render` are rewritten to the versioned twin below
|
|
@@ -240,7 +257,14 @@ export function buildMcpServer(info, handlers, getContext, logger, opts = {}) {
|
|
|
240
257
|
// per-session resource handler's render-read gate sees the
|
|
241
258
|
// caller (`renderReadAllowed`, @ggui-ai/mcp-server-handlers/renders).
|
|
242
259
|
...(opts.selfContained !== undefined
|
|
243
|
-
? {
|
|
260
|
+
? {
|
|
261
|
+
selfContained: {
|
|
262
|
+
...opts.selfContained,
|
|
263
|
+
getContext,
|
|
264
|
+
logger,
|
|
265
|
+
...(opts.issueViewKeys === true ? { issueViewKeys: true } : {}),
|
|
266
|
+
},
|
|
267
|
+
}
|
|
244
268
|
: {}),
|
|
245
269
|
...(opts.publicBaseUrl !== undefined
|
|
246
270
|
? { publicBaseUrl: opts.publicBaseUrl }
|
|
@@ -291,12 +315,24 @@ export function buildMcpServer(info, handlers, getContext, logger, opts = {}) {
|
|
|
291
315
|
// an object, so narrow with the validating predicate and DROP
|
|
292
316
|
// anything else rather than asserting.
|
|
293
317
|
const requestMeta = extra?._meta;
|
|
294
|
-
const
|
|
318
|
+
const requestCtx = {
|
|
295
319
|
...baseCtx,
|
|
296
320
|
...(isRecord(requestMeta) ? { requestMeta } : {}),
|
|
297
321
|
...(extra?.signal !== undefined ? { signal: extra.signal } : {}),
|
|
298
322
|
};
|
|
299
323
|
const start = Date.now();
|
|
324
|
+
// ggui#1415: the measuring gate. For a tool that declares a view
|
|
325
|
+
// proof it verifies the proof, puts the verdict (and the request's
|
|
326
|
+
// row-read memo) on the context and returns the line's proof fields.
|
|
327
|
+
// Total: it never throws and never changes the handler's answer.
|
|
328
|
+
const gated = await runViewProofGate(opts.viewProofGate, handler, input, requestCtx);
|
|
329
|
+
const ctx = gated.ctx;
|
|
330
|
+
const proofFields = {
|
|
331
|
+
...gated.fields,
|
|
332
|
+
...(ctx.authSource !== undefined && (handler.viewProof !== undefined || handler.name === 'ggui_runtime_declare_tool_catalog')
|
|
333
|
+
? { authSource: ctx.authSource }
|
|
334
|
+
: {}),
|
|
335
|
+
};
|
|
300
336
|
try {
|
|
301
337
|
const data = await handler.handler(input, ctx);
|
|
302
338
|
// First-class in-result failure channel. A handler that
|
|
@@ -319,6 +355,7 @@ export function buildMcpServer(info, handlers, getContext, logger, opts = {}) {
|
|
|
319
355
|
// malformed-row refusal, ggui#839), so its session is owned and
|
|
320
356
|
// the per-session fields apply (ggui#1395).
|
|
321
357
|
...sessionFields(handler.name, input, validated, ctx),
|
|
358
|
+
...proofFields,
|
|
322
359
|
elapsedMs: Date.now() - start,
|
|
323
360
|
});
|
|
324
361
|
return {
|
|
@@ -341,6 +378,7 @@ export function buildMcpServer(info, handlers, getContext, logger, opts = {}) {
|
|
|
341
378
|
appId: ctx.appId,
|
|
342
379
|
outcome: 'success',
|
|
343
380
|
...sessionFields(handler.name, input, validated, ctx),
|
|
381
|
+
...proofFields,
|
|
344
382
|
elapsedMs: Date.now() - start,
|
|
345
383
|
});
|
|
346
384
|
// When the handler's output carries a `nextStep`, lead the
|
|
@@ -369,25 +407,14 @@ export function buildMcpServer(info, handlers, getContext, logger, opts = {}) {
|
|
|
369
407
|
const gestureHint = nextStepHint !== undefined && nextStepHint.includes('ggui_consume')
|
|
370
408
|
? nextStepHint
|
|
371
409
|
: undefined;
|
|
372
|
-
// The consume → amend hint (ggui#1399 step 2)
|
|
373
|
-
//
|
|
374
|
-
//
|
|
375
|
-
//
|
|
376
|
-
//
|
|
377
|
-
const amendHint = nextStepHint !== undefined && nextStepHint.includes('ggui_amend(')
|
|
378
|
-
? nextStepHint
|
|
379
|
-
: undefined;
|
|
410
|
+
// The consume → amend hint (ggui#1399 step 2) rides `nextStep` in
|
|
411
|
+
// the structured result only, with NO plain-text lead (ggui#1397):
|
|
412
|
+
// a lead ("repaint … then re-call ggui_consume") was measured as the
|
|
413
|
+
// carrier of informational taps left unanswered on gpt-6-luna, and
|
|
414
|
+
// removing it held every repaint. Probes 3 and 4 are on #1397.
|
|
380
415
|
return {
|
|
381
416
|
structuredContent: validated,
|
|
382
417
|
content: [
|
|
383
|
-
...(amendHint !== undefined
|
|
384
|
-
? [
|
|
385
|
-
{
|
|
386
|
-
type: 'text',
|
|
387
|
-
text: `You drained a gesture. If it changed what the user is looking at, repaint the SAME card in place with ${amendHint} — do not render a new one — then re-call ggui_consume for the next gesture.`,
|
|
388
|
-
},
|
|
389
|
-
]
|
|
390
|
-
: []),
|
|
391
418
|
...(gestureHint !== undefined
|
|
392
419
|
? [
|
|
393
420
|
{
|
|
@@ -417,6 +444,7 @@ export function buildMcpServer(info, handlers, getContext, logger, opts = {}) {
|
|
|
417
444
|
outcome: 'error',
|
|
418
445
|
errorClass: errorClassName(err),
|
|
419
446
|
...claimedSessionField(handler.name, input),
|
|
447
|
+
...proofFields,
|
|
420
448
|
elapsedMs: Date.now() - start,
|
|
421
449
|
});
|
|
422
450
|
throw err;
|
package/dist/console-auth.d.ts
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Embedded-ui cookie auth plane.
|
|
3
3
|
*
|
|
4
|
-
* This module owns the
|
|
5
|
-
*
|
|
4
|
+
* This module owns the second of the two token kinds the server mints
|
|
5
|
+
* for the embedded UI (a third, the reconnect "session" token, is minted
|
|
6
|
+
* by no server since ggui#1488):
|
|
6
7
|
*
|
|
7
|
-
* 1. bootstrap tokens
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* 3. console cookies — longer-TTL, reusable, same-origin
|
|
8
|
+
* 1. ws (bootstrap) tokens — short-TTL, reusable within TTL, MCP Apps
|
|
9
|
+
* iframes.
|
|
10
|
+
* 2. console cookies — longer-TTL, reusable, same-origin
|
|
11
11
|
* browser-only, issued by this server
|
|
12
12
|
* for ITS OWN console landing/viewer
|
|
13
13
|
* pages. **Scoped narrowly**: consumed
|
|
@@ -17,10 +17,10 @@
|
|
|
17
17
|
* `/threads`, or any other ingress.
|
|
18
18
|
*
|
|
19
19
|
* Isolation invariant (load-bearing). The cookie uses the SAME HMAC
|
|
20
|
-
* shape as the bootstrap
|
|
20
|
+
* shape as the ws (bootstrap) token but a distinct `kind` claim
|
|
21
21
|
* (`'console-session'`). That makes cross-kind confusion
|
|
22
|
-
* impossible: a cookie value CAN'T verify as a
|
|
23
|
-
*
|
|
22
|
+
* impossible: a cookie value CAN'T verify as a ws token, nor any
|
|
23
|
+
* other kind as a cookie. Secrets are shared across kinds because they live in the
|
|
24
24
|
* same trust domain (server-minted same-origin creds) and kind
|
|
25
25
|
* discrimination is sufficient.
|
|
26
26
|
*
|
|
@@ -63,7 +63,7 @@ export interface DevtoolCookieMint {
|
|
|
63
63
|
export interface MintDevtoolCookieInput {
|
|
64
64
|
readonly sessionId: string;
|
|
65
65
|
readonly appId: string;
|
|
66
|
-
/** HMAC secret. Shared with
|
|
66
|
+
/** HMAC secret. Shared with the ws (bootstrap) tokens by design. */
|
|
67
67
|
readonly secret: string;
|
|
68
68
|
/** Cookie TTL in seconds. Defaults to 8 hours. */
|
|
69
69
|
readonly ttlSec?: number;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"console-auth.d.ts","sourceRoot":"","sources":["../src/console-auth.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwCG;AACH,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,WAAW,CAAC;AAOrD;;;;;GAKG;AACH,eAAO,MAAM,mBAAmB,yBAAyB,CAAC;AAE1D;;;;;GAKG;AACH,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAC;IACjC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;CACxB;AAED,MAAM,WAAW,sBAAsB;IACrC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,
|
|
1
|
+
{"version":3,"file":"console-auth.d.ts","sourceRoot":"","sources":["../src/console-auth.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwCG;AACH,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,WAAW,CAAC;AAOrD;;;;;GAKG;AACH,eAAO,MAAM,mBAAmB,yBAAyB,CAAC;AAE1D;;;;;GAKG;AACH,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAC;IACjC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;CACxB;AAED,MAAM,WAAW,sBAAsB;IACrC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,oEAAoE;IACpE,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,kDAAkD;IAClD,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IACzB;;;;;OAKG;IACH,QAAQ,CAAC,MAAM,CAAC,EAAE,OAAO,CAAC;IAC1B,oCAAoC;IACpC,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IACvB;;;;OAIG;IACH,QAAQ,CAAC,QAAQ,CAAC,EAAE,QAAQ,GAAG,KAAK,GAAG,MAAM,CAAC;CAC/C;AAED;;;;GAIG;AACH,wBAAgB,iBAAiB,CAC/B,KAAK,EAAE,sBAAsB,GAC5B,iBAAiB,CAqBnB;AAED;;;;;;;;;GASG;AACH,wBAAgB,oBAAoB,CAClC,YAAY,EAAE,MAAM,GAAG,SAAS,GAC/B,MAAM,GAAG,IAAI,CAcf;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,4BAA4B,CAC1C,OAAO,EAAE,mBAAmB,GAC3B,MAAM,GAAG,IAAI,CAIf;AAED;;;;GAIG;AACH,MAAM,WAAW,mBAAmB;IAClC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;CACxB;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,mBAAmB,CACjC,WAAW,EAAE,MAAM,EACnB,MAAM,EAAE,MAAM,GACb,mBAAmB,GAAG,IAAI,CAO5B"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A bounded "seen before?" set, for a log line that should fire once per key
|
|
3
|
+
* per server process without growing with the number of keys.
|
|
4
|
+
*
|
|
5
|
+
* Insertion-ordered: at capacity, the oldest key is forgotten, so a key
|
|
6
|
+
* offered again after `capacity` newer keys counts as new and its line fires
|
|
7
|
+
* a second time. A reader of such a line takes the earliest per key.
|
|
8
|
+
*/
|
|
9
|
+
export interface FirstSeen {
|
|
10
|
+
/** True the first time `key` is offered (or once it has been forgotten); false after. */
|
|
11
|
+
first(key: string): boolean;
|
|
12
|
+
}
|
|
13
|
+
export declare function createFirstSeen(capacity: number): FirstSeen;
|
|
14
|
+
//# sourceMappingURL=first-seen.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"first-seen.d.ts","sourceRoot":"","sources":["../src/first-seen.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,MAAM,WAAW,SAAS;IACxB,yFAAyF;IACzF,KAAK,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CAC7B;AAED,wBAAgB,eAAe,CAAC,QAAQ,EAAE,MAAM,GAAG,SAAS,CAgB3D"}
|