@ggui-ai/protocol 0.2.0-alpha.4 → 0.4.0-rc.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 +16 -16
- package/dist/envelope-adapters.d.ts +6 -5
- package/dist/envelope-adapters.d.ts.map +1 -1
- package/dist/envelope-adapters.js +3 -3
- package/dist/envelopes/builders.d.ts +6 -32
- package/dist/envelopes/builders.d.ts.map +1 -1
- package/dist/envelopes/builders.js +3 -25
- package/dist/errors/version-mismatch.d.ts +1 -1
- package/dist/errors/version-mismatch.js +1 -1
- package/dist/gadgets/resolve-app-gadgets.d.ts +10 -0
- package/dist/gadgets/resolve-app-gadgets.d.ts.map +1 -0
- package/dist/gadgets/resolve-app-gadgets.js +19 -0
- package/dist/gadgets/resolve-contract-gadgets.d.ts +6 -6
- package/dist/gadgets/resolve-contract-gadgets.js +5 -5
- package/dist/gadgets/stdlib-gadgets.d.ts +1 -1
- package/dist/gadgets/stdlib-gadgets.d.ts.map +1 -1
- package/dist/gadgets/stdlib-gadgets.js +43 -22
- package/dist/iframe-bridge.d.ts +4 -43
- package/dist/iframe-bridge.d.ts.map +1 -1
- package/dist/iframe-bridge.js +4 -141
- package/dist/index.d.ts +60 -62
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +66 -61
- package/dist/integrations/mcp-apps.d.ts +83 -38
- package/dist/integrations/mcp-apps.d.ts.map +1 -1
- package/dist/integrations/mcp-apps.js +98 -52
- package/dist/recommended-prompts.d.ts +1 -1
- package/dist/recommended-prompts.js +1 -1
- package/dist/registry/blueprint-key.d.ts +3 -0
- package/dist/registry/blueprint-key.d.ts.map +1 -1
- package/dist/registry/blueprint-key.js +9 -0
- package/dist/registry/blueprint-stamp.d.ts +12 -0
- package/dist/registry/blueprint-stamp.d.ts.map +1 -0
- package/dist/registry/blueprint-stamp.js +12 -0
- package/dist/registry/canonicalize-contract.d.ts +31 -2
- package/dist/registry/canonicalize-contract.d.ts.map +1 -1
- package/dist/registry/canonicalize-contract.js +125 -13
- package/dist/registry/portable-blueprint.d.ts +71 -0
- package/dist/registry/portable-blueprint.d.ts.map +1 -0
- package/dist/registry/portable-blueprint.js +111 -0
- package/dist/registry/summarize-contract.d.ts +7 -2
- package/dist/registry/summarize-contract.d.ts.map +1 -1
- package/dist/registry/summarize-contract.js +12 -3
- package/dist/registry/variant-key.d.ts +17 -0
- package/dist/registry/variant-key.d.ts.map +1 -0
- package/dist/registry/variant-key.js +22 -0
- package/dist/schema-learning/derive-contract.d.ts +2 -2
- package/dist/schema-learning/derive-contract.js +2 -2
- package/dist/schema-learning/merge.d.ts.map +1 -1
- package/dist/schema-learning/merge.js +3 -2
- package/dist/schemas/app-theme.d.ts +38 -0
- package/dist/schemas/app-theme.d.ts.map +1 -0
- package/dist/schemas/app-theme.js +43 -0
- package/dist/schemas/blueprint.d.ts +30 -11
- package/dist/schemas/blueprint.d.ts.map +1 -1
- package/dist/schemas/blueprint.js +62 -26
- package/dist/schemas/data-contract.d.ts +28 -30
- package/dist/schemas/data-contract.d.ts.map +1 -1
- package/dist/schemas/data-contract.js +26 -26
- package/dist/schemas/handshake-suggestion.d.ts +2 -4
- package/dist/schemas/handshake-suggestion.d.ts.map +1 -1
- package/dist/schemas/handshake-suggestion.js +5 -14
- package/dist/schemas/mcp.d.ts +294 -59
- package/dist/schemas/mcp.d.ts.map +1 -1
- package/dist/schemas/mcp.js +350 -94
- package/dist/schemas/ops-blueprint.d.ts +12 -12
- package/dist/schemas/ops-blueprint.d.ts.map +1 -1
- package/dist/schemas/ops-blueprint.js +14 -24
- package/dist/screen-blueprints/define.d.ts +2 -2
- package/dist/screen-blueprints/match.d.ts +12 -7
- package/dist/screen-blueprints/match.d.ts.map +1 -1
- package/dist/screen-blueprints/match.js +7 -5
- package/dist/screen-blueprints/types.d.ts +14 -18
- package/dist/screen-blueprints/types.d.ts.map +1 -1
- package/dist/transport/websocket.d.ts +5 -26
- package/dist/transport/websocket.d.ts.map +1 -1
- package/dist/types/app-config.d.ts +0 -1
- package/dist/types/app-config.d.ts.map +1 -1
- package/dist/types/auth.d.ts +1 -46
- package/dist/types/auth.d.ts.map +1 -1
- package/dist/types/blueprint-source.d.ts +123 -0
- package/dist/types/blueprint-source.d.ts.map +1 -0
- package/dist/types/blueprint-source.js +126 -0
- package/dist/types/blueprint.d.ts +25 -18
- package/dist/types/blueprint.d.ts.map +1 -1
- package/dist/types/contract-inference.d.ts +2 -43
- package/dist/types/contract-inference.d.ts.map +1 -1
- package/dist/types/data-contract.d.ts +59 -344
- package/dist/types/data-contract.d.ts.map +1 -1
- package/dist/types/data-contract.js +5 -12
- package/dist/types/events.d.ts +31 -69
- package/dist/types/events.d.ts.map +1 -1
- package/dist/types/{render-event.d.ts → ggui-session-event.d.ts} +15 -15
- package/dist/types/ggui-session-event.d.ts.map +1 -0
- package/dist/types/{render-event.js → ggui-session-event.js} +5 -5
- package/dist/types/handshake-suggestion.d.ts +49 -35
- package/dist/types/handshake-suggestion.d.ts.map +1 -1
- package/dist/types/host-context.d.ts +8 -8
- package/dist/types/host-context.d.ts.map +1 -1
- package/dist/types/host-context.js +5 -5
- package/dist/types/invoke.d.ts +1 -13
- package/dist/types/invoke.d.ts.map +1 -1
- package/dist/types/{canvas-lifecycle.d.ts → lifecycle.d.ts} +35 -37
- package/dist/types/lifecycle.d.ts.map +1 -0
- package/dist/types/{canvas-lifecycle.js → lifecycle.js} +12 -14
- package/dist/types/live-channel.d.ts +48 -104
- package/dist/types/live-channel.d.ts.map +1 -1
- package/dist/types/llm-route.d.ts +4 -4
- package/dist/types/llm-route.d.ts.map +1 -1
- package/dist/types/llm-route.js +24 -2
- package/dist/types/llm.d.ts +13 -24
- package/dist/types/llm.d.ts.map +1 -1
- package/dist/types/llm.js +23 -53
- package/dist/types/mcp.d.ts +74 -177
- package/dist/types/mcp.d.ts.map +1 -1
- package/dist/types/mcp.js +11 -4
- package/dist/types/oauth-user-id.d.ts +12 -0
- package/dist/types/oauth-user-id.d.ts.map +1 -0
- package/dist/types/oauth-user-id.js +10 -0
- package/dist/types/portable-blueprint.d.ts +61 -0
- package/dist/types/portable-blueprint.d.ts.map +1 -0
- package/dist/types/portable-blueprint.js +8 -0
- package/dist/types/{session.d.ts → render.d.ts} +51 -53
- package/dist/types/render.d.ts.map +1 -0
- package/dist/types/thread.d.ts.map +1 -1
- package/dist/types/thread.js +3 -3
- package/dist/types/ui-generator.d.ts +24 -47
- package/dist/types/ui-generator.d.ts.map +1 -1
- package/dist/types/ui-generator.js +1 -53
- package/dist/validation/ajv-runtime.d.ts +3 -3
- package/dist/validation/ajv-runtime.d.ts.map +1 -1
- package/dist/validation/ajv-runtime.js +6 -5
- package/dist/validation/contract-validator.d.ts +23 -22
- package/dist/validation/contract-validator.d.ts.map +1 -1
- package/dist/validation/contract-validator.js +28 -26
- package/dist/validation/cross-references.d.ts +3 -3
- package/dist/validation/cross-references.js +3 -3
- package/dist/validation/hygiene-rules.d.ts +9 -9
- package/dist/validation/hygiene-rules.js +13 -13
- package/dist/validation/is-record.d.ts +17 -0
- package/dist/validation/is-record.d.ts.map +1 -0
- package/dist/validation/is-record.js +18 -0
- package/dist/validation/lint-contract.d.ts +1 -1
- package/dist/validation/lint-contract.js +3 -3
- package/dist/validation/name-invariants.d.ts +4 -4
- package/dist/validation/name-invariants.js +4 -4
- package/dist/validation/reserved-channels.d.ts +43 -46
- package/dist/validation/reserved-channels.d.ts.map +1 -1
- package/dist/validation/reserved-channels.js +14 -177
- package/dist/validation/resolve-stream-channel.d.ts +3 -9
- package/dist/validation/resolve-stream-channel.d.ts.map +1 -1
- package/dist/validation/resolve-stream-channel.js +3 -4
- package/dist/validation/sanitize-error.d.ts +10 -9
- package/dist/validation/sanitize-error.d.ts.map +1 -1
- package/dist/validation/sanitize-error.js +19 -18
- package/dist/validation/schema-compat-invariants.d.ts +1 -1
- package/dist/validation/schema-compat-invariants.js +3 -3
- package/dist/validation/schema-meta-validation.d.ts +4 -4
- package/dist/validation/schema-meta-validation.d.ts.map +1 -1
- package/dist/validation/schema-meta-validation.js +7 -9
- package/dist/validation/schema-subset.d.ts +9 -8
- package/dist/validation/schema-subset.d.ts.map +1 -1
- package/dist/validation/ui-security.d.ts +0 -17
- package/dist/validation/ui-security.d.ts.map +1 -1
- package/dist/validation/ui-security.js +17 -22
- package/dist/validation/zod-to-json-schema.d.ts +1 -1
- package/dist/validation/zod-to-json-schema.d.ts.map +1 -1
- package/dist/validation/zod-to-json-schema.js +4 -3
- package/dist/version.d.ts +818 -9
- package/dist/version.d.ts.map +1 -1
- package/dist/version.js +817 -8
- package/package.json +3 -3
- package/dist/bridge/invoke-agent.d.ts +0 -65
- package/dist/bridge/invoke-agent.d.ts.map +0 -1
- package/dist/bridge/invoke-agent.js +0 -113
- package/dist/stream/stream-parser.d.ts +0 -62
- package/dist/stream/stream-parser.d.ts.map +0 -1
- package/dist/stream/stream-parser.js +0 -199
- package/dist/types/canvas-lifecycle.d.ts.map +0 -1
- package/dist/types/credential.d.ts +0 -41
- package/dist/types/credential.d.ts.map +0 -1
- package/dist/types/credential.js +0 -32
- package/dist/types/data-bindings.d.ts +0 -322
- package/dist/types/data-bindings.d.ts.map +0 -1
- package/dist/types/data-bindings.js +0 -29
- package/dist/types/feedback.d.ts +0 -22
- package/dist/types/feedback.d.ts.map +0 -1
- package/dist/types/feedback.js +0 -7
- package/dist/types/mcp-proxy.d.ts +0 -67
- package/dist/types/mcp-proxy.d.ts.map +0 -1
- package/dist/types/mcp-proxy.js +0 -46
- package/dist/types/openrouter-models.d.ts +0 -23
- package/dist/types/openrouter-models.d.ts.map +0 -1
- package/dist/types/openrouter-models.js +0 -4843
- package/dist/types/region.d.ts +0 -26
- package/dist/types/region.d.ts.map +0 -1
- package/dist/types/region.js +0 -36
- package/dist/types/render-event.d.ts.map +0 -1
- package/dist/types/session.d.ts.map +0 -1
- /package/dist/types/{session.js → render.js} +0 -0
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* This module is the **boundary** for MCP Apps outbound delivery. Anything
|
|
5
5
|
* MCP-Apps-specific that ggui exposes to the rest of the codebase lives
|
|
6
|
-
* here — never in `types/live-channel.ts`, `types/mcp.ts`, `types/
|
|
6
|
+
* here — never in `types/live-channel.ts`, `types/mcp.ts`, `types/render.ts`,
|
|
7
7
|
* or any other core module. Consumers opt in via the subpath import:
|
|
8
8
|
*
|
|
9
9
|
* ```ts
|
|
@@ -21,12 +21,14 @@
|
|
|
21
21
|
* drift is bounded to callers that explicitly import from here.
|
|
22
22
|
*
|
|
23
23
|
* Core still carries two fields that make the bootstrap flow work —
|
|
24
|
-
* `SubscribePayload.
|
|
24
|
+
* `SubscribePayload.wsToken?: string` and `AckPayload.sessionToken?:
|
|
25
25
|
* string`. Those are deliberately framed as **general transport bootstrap
|
|
26
26
|
* credentials** (opaque strings), not MCP-Apps-specific. Any future
|
|
27
27
|
* bootstrap mechanism (short-code auto-login, signed-URL bootstrap, etc.)
|
|
28
28
|
* reuses the same slots.
|
|
29
29
|
*/
|
|
30
|
+
import { appThemeSchema } from '../schemas/app-theme.js';
|
|
31
|
+
import { isRecord } from '../validation/is-record.js';
|
|
30
32
|
/**
|
|
31
33
|
* MCP capability name ggui servers advertise in their MCP `initialize`
|
|
32
34
|
* response capabilities when they implement the MCP Apps outbound path.
|
|
@@ -92,14 +94,14 @@ export function deriveContextName(slotKey) {
|
|
|
92
94
|
// ```jsonc
|
|
93
95
|
// "_meta": {
|
|
94
96
|
// "ai.ggui/render": {
|
|
95
|
-
//
|
|
97
|
+
// sessionId, appId, runtimeUrl,
|
|
96
98
|
// wsUrl?, wsToken?, expiresAt?,
|
|
97
99
|
// pollingUrl?,
|
|
98
|
-
// themeId?, themeMode?,
|
|
100
|
+
// themeId?, themeMode?, theme?,
|
|
99
101
|
// gadgets?, publicEnv?, streamWebSocketLocalTools?,
|
|
100
|
-
//
|
|
102
|
+
// permissionsPolicy?,
|
|
101
103
|
// lastSequence?,
|
|
102
|
-
// propsJson?,
|
|
104
|
+
// propsJson?, contextSlots?,
|
|
103
105
|
// contractHash?, validatorsUrl?,
|
|
104
106
|
// codeUrl?, codeHash?, kind?
|
|
105
107
|
// }
|
|
@@ -128,7 +130,7 @@ export const MCP_APP_AI_GGUI_RENDER_META_KEY = 'ai.ggui/render';
|
|
|
128
130
|
* Read the `ai.ggui/render` slice off a parsed JSON-RPC `_meta` object.
|
|
129
131
|
*
|
|
130
132
|
* Structural validation only. Missing key returns `{ok: true, meta: undefined}`
|
|
131
|
-
* — not a failure. Required-fields gate (
|
|
133
|
+
* — not a failure. Required-fields gate (sessionId / appId / runtimeUrl)
|
|
132
134
|
* fires only when the key is present. Field-level optional-field
|
|
133
135
|
* defensive parsing (e.g. context-slot schema narrowing, expiresAt date
|
|
134
136
|
* parse) lives downstream in the iframe-runtime's `validateMeta`.
|
|
@@ -136,21 +138,20 @@ export const MCP_APP_AI_GGUI_RENDER_META_KEY = 'ai.ggui/render';
|
|
|
136
138
|
* @public
|
|
137
139
|
*/
|
|
138
140
|
export function parseMcpAppAiGguiRenderMeta(meta) {
|
|
139
|
-
if (meta
|
|
141
|
+
if (!isRecord(meta)) {
|
|
140
142
|
return { ok: true };
|
|
141
143
|
}
|
|
142
|
-
const
|
|
143
|
-
const raw = m[MCP_APP_AI_GGUI_RENDER_META_KEY];
|
|
144
|
+
const raw = meta[MCP_APP_AI_GGUI_RENDER_META_KEY];
|
|
144
145
|
if (raw === undefined) {
|
|
145
146
|
return { ok: true };
|
|
146
147
|
}
|
|
147
|
-
if (
|
|
148
|
+
if (!isRecord(raw)) {
|
|
148
149
|
return { ok: false, reason: 'MALFORMED_RENDER' };
|
|
149
150
|
}
|
|
150
151
|
const s = raw;
|
|
151
152
|
// Identity — required when slice is present.
|
|
152
|
-
if (typeof s.
|
|
153
|
-
s.
|
|
153
|
+
if (typeof s.sessionId !== 'string' ||
|
|
154
|
+
s.sessionId.length === 0 ||
|
|
154
155
|
typeof s.appId !== 'string' ||
|
|
155
156
|
s.appId.length === 0 ||
|
|
156
157
|
typeof s.runtimeUrl !== 'string' ||
|
|
@@ -200,8 +201,14 @@ export function parseMcpAppAiGguiRenderMeta(meta) {
|
|
|
200
201
|
cHash.length > 0 &&
|
|
201
202
|
typeof vUrl === 'string' &&
|
|
202
203
|
vUrl.length > 0;
|
|
204
|
+
// Theme overlay — validated via the complete `appThemeSchema` so the
|
|
205
|
+
// parsed value is typed `AppTheme` without an unguarded cast. A
|
|
206
|
+
// malformed/absent overlay yields `undefined` and is dropped from the
|
|
207
|
+
// slice (tolerant degrade, consistent with the other optional fields).
|
|
208
|
+
const themeParse = s.theme !== undefined ? appThemeSchema.safeParse(s.theme) : undefined;
|
|
209
|
+
const parsedTheme = themeParse?.success === true ? themeParse.data : undefined;
|
|
203
210
|
const slice = {
|
|
204
|
-
|
|
211
|
+
sessionId: s.sessionId,
|
|
205
212
|
appId: s.appId,
|
|
206
213
|
runtimeUrl: s.runtimeUrl,
|
|
207
214
|
...(hasW && hasT ? { wsUrl: aw, wsToken: at } : {}),
|
|
@@ -211,6 +218,12 @@ export function parseMcpAppAiGguiRenderMeta(meta) {
|
|
|
211
218
|
...(s.themeMode !== undefined
|
|
212
219
|
? { themeMode: s.themeMode }
|
|
213
220
|
: {}),
|
|
221
|
+
// `theme` is the only structured optional field on the slice, so
|
|
222
|
+
// it gets the schema as its parser (the rest are scalar/array
|
|
223
|
+
// casts). `appThemeSchema` is the complete validator — a malformed
|
|
224
|
+
// overlay degrades to "no overlay" rather than failing the whole
|
|
225
|
+
// slice, matching the tolerant posture of the other optional fields.
|
|
226
|
+
...(parsedTheme !== undefined ? { theme: parsedTheme } : {}),
|
|
214
227
|
...(s.gadgets !== undefined
|
|
215
228
|
? { gadgets: s.gadgets }
|
|
216
229
|
: {}),
|
|
@@ -222,19 +235,11 @@ export function parseMcpAppAiGguiRenderMeta(meta) {
|
|
|
222
235
|
streamWebSocketLocalTools: s.streamWebSocketLocalTools,
|
|
223
236
|
}
|
|
224
237
|
: {}),
|
|
225
|
-
...(s.appCallableTools !== undefined
|
|
226
|
-
? { appCallableTools: s.appCallableTools }
|
|
227
|
-
: {}),
|
|
228
238
|
...(s.permissionsPolicy !== undefined
|
|
229
239
|
? { permissionsPolicy: s.permissionsPolicy }
|
|
230
240
|
: {}),
|
|
231
241
|
...(ls !== undefined ? { lastSequence: ls } : {}),
|
|
232
242
|
...(s.propsJson !== undefined ? { propsJson: s.propsJson } : {}),
|
|
233
|
-
...(s.actionNextSteps !== undefined
|
|
234
|
-
? {
|
|
235
|
-
actionNextSteps: s.actionNextSteps,
|
|
236
|
-
}
|
|
237
|
-
: {}),
|
|
238
243
|
...(s.contextSlots !== undefined
|
|
239
244
|
? {
|
|
240
245
|
contextSlots: s.contextSlots,
|
|
@@ -295,15 +300,14 @@ export const MCP_APP_AI_GGUI_HOST_SESSION_META_KEY = 'ai.ggui/host-session';
|
|
|
295
300
|
* @public
|
|
296
301
|
*/
|
|
297
302
|
export function parseMcpAppAiGguiHostSessionMeta(meta) {
|
|
298
|
-
if (meta
|
|
303
|
+
if (!isRecord(meta)) {
|
|
299
304
|
return { ok: true };
|
|
300
305
|
}
|
|
301
|
-
const
|
|
302
|
-
const raw = m[MCP_APP_AI_GGUI_HOST_SESSION_META_KEY];
|
|
306
|
+
const raw = meta[MCP_APP_AI_GGUI_HOST_SESSION_META_KEY];
|
|
303
307
|
if (raw === undefined) {
|
|
304
308
|
return { ok: true };
|
|
305
309
|
}
|
|
306
|
-
if (
|
|
310
|
+
if (!isRecord(raw)) {
|
|
307
311
|
return { ok: false, reason: 'MALFORMED_HOST_SESSION' };
|
|
308
312
|
}
|
|
309
313
|
const r = raw;
|
|
@@ -322,41 +326,44 @@ export function parseMcpAppAiGguiHostSessionMeta(meta) {
|
|
|
322
326
|
};
|
|
323
327
|
}
|
|
324
328
|
/**
|
|
325
|
-
* Type guard: narrows a `
|
|
329
|
+
* Type guard: narrows a `GguiSession` (or unknown) to {@link McpAppsGguiSession}.
|
|
326
330
|
* Uses the discriminator.
|
|
327
331
|
*/
|
|
328
|
-
export function
|
|
332
|
+
export function isMcpAppsGguiSession(entry) {
|
|
329
333
|
return (entry !== null &&
|
|
330
334
|
typeof entry === 'object' &&
|
|
331
335
|
entry.type === 'mcpApps');
|
|
332
336
|
}
|
|
333
337
|
/**
|
|
334
|
-
* Structural validator for an `
|
|
338
|
+
* Structural validator for an `McpAppsGguiSession` — not a Zod schema
|
|
335
339
|
* so we don't force a Zod dependency here. Returns null on failure
|
|
336
340
|
* (caller maps to an appropriate error code). Required when accepting
|
|
337
341
|
* one over the wire from an agent: the discriminator alone isn't
|
|
338
342
|
* enough.
|
|
339
343
|
*/
|
|
340
|
-
export function
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
if (
|
|
352
|
-
return
|
|
344
|
+
export function validateMcpAppsGguiSession(input) {
|
|
345
|
+
return hasMcpAppsGguiSessionShape(input) ? input : null;
|
|
346
|
+
}
|
|
347
|
+
/** Structural predicate behind {@link validateMcpAppsGguiSession}. */
|
|
348
|
+
function hasMcpAppsGguiSessionShape(input) {
|
|
349
|
+
if (!isRecord(input))
|
|
350
|
+
return false;
|
|
351
|
+
if (input.type !== 'mcpApps')
|
|
352
|
+
return false;
|
|
353
|
+
if (typeof input.id !== 'string' || input.id.length === 0)
|
|
354
|
+
return false;
|
|
355
|
+
if (typeof input.createdAt !== 'string')
|
|
356
|
+
return false;
|
|
357
|
+
const source = input.source;
|
|
358
|
+
if (!isRecord(source))
|
|
359
|
+
return false;
|
|
353
360
|
if (typeof source.connectorId !== 'string' || source.connectorId.length === 0)
|
|
354
|
-
return
|
|
361
|
+
return false;
|
|
355
362
|
if (typeof source.toolName !== 'string' || source.toolName.length === 0)
|
|
356
|
-
return
|
|
363
|
+
return false;
|
|
357
364
|
if (typeof source.resourceUri !== 'string' || !source.resourceUri.startsWith('ui://'))
|
|
358
|
-
return
|
|
359
|
-
return
|
|
365
|
+
return false;
|
|
366
|
+
return true;
|
|
360
367
|
}
|
|
361
368
|
/**
|
|
362
369
|
* The closed set of valid lifecycle states. Exposed as a `readonly`
|
|
@@ -380,7 +387,7 @@ export const MCP_APP_LIFECYCLE_STATES = [
|
|
|
380
387
|
* - Outer envelope is an object with `type === 'ggui:lifecycle'`.
|
|
381
388
|
* - `event` is an object with `state` matching {@link
|
|
382
389
|
* MCP_APP_LIFECYCLE_STATES}.
|
|
383
|
-
* - If `
|
|
390
|
+
* - If `sessionId` is present, it is a non-empty string.
|
|
384
391
|
* - If `error` is present, it is an object with string `code` +
|
|
385
392
|
* `message`.
|
|
386
393
|
*
|
|
@@ -400,8 +407,8 @@ export function isMcpAppLifecycleMessage(message) {
|
|
|
400
407
|
if (!MCP_APP_LIFECYCLE_STATES.includes(e.state)) {
|
|
401
408
|
return false;
|
|
402
409
|
}
|
|
403
|
-
if (e.
|
|
404
|
-
if (typeof e.
|
|
410
|
+
if (e.sessionId !== undefined) {
|
|
411
|
+
if (typeof e.sessionId !== 'string' || e.sessionId.length === 0) {
|
|
405
412
|
return false;
|
|
406
413
|
}
|
|
407
414
|
}
|
|
@@ -415,6 +422,43 @@ export function isMcpAppLifecycleMessage(message) {
|
|
|
415
422
|
}
|
|
416
423
|
return true;
|
|
417
424
|
}
|
|
425
|
+
// =============================================================================
|
|
426
|
+
// Renderer → host postMessage envelope vocabulary
|
|
427
|
+
//
|
|
428
|
+
// The renderer (inside the MCP Apps iframe) speaks to its parent via a
|
|
429
|
+
// small closed family of `{type: 'ggui:…'}` postMessage envelopes. The
|
|
430
|
+
// DISCRIMINATOR vocabulary is protocol-owned and lives HERE, in one
|
|
431
|
+
// place — hosts on every platform (web iframe hosts, React Native
|
|
432
|
+
// WebView hosts) and the renderer itself import these constants rather
|
|
433
|
+
// than re-encoding the strings, so a tag rename is a single-point
|
|
434
|
+
// change and recognizer drift (a host classifying a tag no renderer
|
|
435
|
+
// emits) is structurally impossible.
|
|
436
|
+
//
|
|
437
|
+
// Payload OWNERSHIP follows the vocabulary's semantics:
|
|
438
|
+
// - `ggui:lifecycle` — protocol-owned end to end (see
|
|
439
|
+
// {@link McpAppLifecycleMessage} above): the payload is a wire
|
|
440
|
+
// contract with host obligations.
|
|
441
|
+
// - `ggui:renderer-ready` / `ggui:bootstrap-failed` — envelope shape
|
|
442
|
+
// is protocol-owned ({@link McpAppRendererReadyMessage} /
|
|
443
|
+
// {@link McpAppBootstrapFailedMessage}); the renderer narrows
|
|
444
|
+
// `reason` to its own closed reason union at the emission site.
|
|
445
|
+
// - `ggui:observe` — only the TAG is protocol-owned. The event union
|
|
446
|
+
// it carries is renderer-internal telemetry vocabulary
|
|
447
|
+
// (`ObservabilityEvent` in the renderer package); hosts treat it
|
|
448
|
+
// as extensibly-closed.
|
|
449
|
+
// =============================================================================
|
|
450
|
+
/** Envelope tag: renderer alive + bundle evaluated (pre-`ui/initialize`). */
|
|
451
|
+
export const MCP_APP_RENDERER_READY_TYPE = 'ggui:renderer-ready';
|
|
452
|
+
/** Envelope tag: a boot-path failure (parse / initialize / handshake). */
|
|
453
|
+
export const MCP_APP_BOOTSTRAP_FAILED_TYPE = 'ggui:bootstrap-failed';
|
|
454
|
+
/** Envelope tag: renderer-internal observability event (telemetry). */
|
|
455
|
+
export const MCP_APP_OBSERVE_TYPE = 'ggui:observe';
|
|
456
|
+
/**
|
|
457
|
+
* Envelope tag: mount-lifecycle transition. Constant twin of the
|
|
458
|
+
* literal on {@link McpAppLifecycleMessage} — the annotation ties the
|
|
459
|
+
* two so they cannot drift.
|
|
460
|
+
*/
|
|
461
|
+
export const MCP_APP_LIFECYCLE_TYPE = 'ggui:lifecycle';
|
|
418
462
|
/**
|
|
419
463
|
* The three canonical gesture kinds — useful for exhaustiveness checks
|
|
420
464
|
* in `switch (kind) { ... }` blocks. Frozen so consumers can safely use
|
|
@@ -438,12 +482,12 @@ export const SUBMIT_ACTION_KINDS = [
|
|
|
438
482
|
* MUST validate shape before consuming.
|
|
439
483
|
*/
|
|
440
484
|
export function isGguiSubmitActionInput(value) {
|
|
441
|
-
if (value
|
|
485
|
+
if (!isRecord(value))
|
|
442
486
|
return false;
|
|
443
487
|
const v = value;
|
|
444
488
|
if (typeof v.kind !== 'string' || v.kind.length === 0)
|
|
445
489
|
return false;
|
|
446
|
-
if (typeof v.
|
|
490
|
+
if (typeof v.sessionId !== 'string' || v.sessionId.length === 0)
|
|
447
491
|
return false;
|
|
448
492
|
if (typeof v.appId !== 'string' || v.appId.length === 0)
|
|
449
493
|
return false;
|
|
@@ -451,7 +495,9 @@ export function isGguiSubmitActionInput(value) {
|
|
|
451
495
|
return false;
|
|
452
496
|
if (typeof v.firedAt !== 'string' || v.firedAt.length === 0)
|
|
453
497
|
return false;
|
|
454
|
-
|
|
498
|
+
// Payload-object presence is the invariant: a JSON OBJECT, never an
|
|
499
|
+
// array — array payloads reject even for unknown extension kinds.
|
|
500
|
+
if (!isRecord(v.payload))
|
|
455
501
|
return false;
|
|
456
502
|
// Per-kind payload narrowing for the closed primary set. Unknown
|
|
457
503
|
// kinds pass through with whatever payload-object the caller supplied
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
* @example
|
|
40
40
|
* ```ts
|
|
41
41
|
* import { query } from '@anthropic-ai/claude-agent-sdk';
|
|
42
|
-
* import { GGUI_AGENT_SYSTEM_PROMPT } from '@ggui-ai/protocol
|
|
42
|
+
* import { GGUI_AGENT_SYSTEM_PROMPT } from '@ggui-ai/protocol';
|
|
43
43
|
*
|
|
44
44
|
* query({
|
|
45
45
|
* prompt: userInput,
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
* @example
|
|
40
40
|
* ```ts
|
|
41
41
|
* import { query } from '@anthropic-ai/claude-agent-sdk';
|
|
42
|
-
* import { GGUI_AGENT_SYSTEM_PROMPT } from '@ggui-ai/protocol
|
|
42
|
+
* import { GGUI_AGENT_SYSTEM_PROMPT } from '@ggui-ai/protocol';
|
|
43
43
|
*
|
|
44
44
|
* query({
|
|
45
45
|
* prompt: userInput,
|
|
@@ -6,4 +6,7 @@ import type { DataContract } from '../types/data-contract.js';
|
|
|
6
6
|
* Node versions (sha256 + utf-8 encoding are well-specified).
|
|
7
7
|
*/
|
|
8
8
|
export declare function blueprintKey(contract: DataContract | undefined): string;
|
|
9
|
+
export { variantKey } from './variant-key.js';
|
|
10
|
+
export { toPortableBlueprint, fromPortableBlueprint, PORTABLE_BLUEPRINT_SCHEMA_VERSION, PORTABLE_BLUEPRINT_V1_REJECTION, type PortableBlueprintSource, type PortableBlueprintImportResult, } from './portable-blueprint.js';
|
|
11
|
+
export * from './blueprint-stamp.js';
|
|
9
12
|
//# sourceMappingURL=blueprint-key.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"blueprint-key.d.ts","sourceRoot":"","sources":["../../src/registry/blueprint-key.ts"],"names":[],"mappings":"AAiBA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,2BAA2B,CAAC;AAG9D;;;;;GAKG;AACH,wBAAgB,YAAY,CAAC,QAAQ,EAAE,YAAY,GAAG,SAAS,GAAG,MAAM,CAGvE"}
|
|
1
|
+
{"version":3,"file":"blueprint-key.d.ts","sourceRoot":"","sources":["../../src/registry/blueprint-key.ts"],"names":[],"mappings":"AAiBA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,2BAA2B,CAAC;AAG9D;;;;;GAKG;AACH,wBAAgB,YAAY,CAAC,QAAQ,EAAE,YAAY,GAAG,SAAS,GAAG,MAAM,CAGvE;AAMD,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAE9C,OAAO,EACL,mBAAmB,EACnB,qBAAqB,EACrB,iCAAiC,EACjC,+BAA+B,EAC/B,KAAK,uBAAuB,EAC5B,KAAK,6BAA6B,GACnC,MAAM,yBAAyB,CAAC;AAIjC,cAAc,sBAAsB,CAAC"}
|
|
@@ -26,3 +26,12 @@ export function blueprintKey(contract) {
|
|
|
26
26
|
const canonical = canonicalizeContracts(contract);
|
|
27
27
|
return createHash('sha256').update(canonical).digest('hex').slice(0, 16);
|
|
28
28
|
}
|
|
29
|
+
// The `@ggui-ai/protocol/blueprint-key` subpath maps to this single
|
|
30
|
+
// file (NOT a barrel), so the variant axis of the reuse key must be
|
|
31
|
+
// re-exported here to be importable at that subpath alongside
|
|
32
|
+
// `blueprintKey`. See `variant-key.ts` for the implementation.
|
|
33
|
+
export { variantKey } from './variant-key.js';
|
|
34
|
+
export { toPortableBlueprint, fromPortableBlueprint, PORTABLE_BLUEPRINT_SCHEMA_VERSION, PORTABLE_BLUEPRINT_V1_REJECTION, } from './portable-blueprint.js';
|
|
35
|
+
// `computeToolCatalogHash` pulls `node:crypto`, so it lives behind this
|
|
36
|
+
// server-only subpath alongside `blueprintKey` rather than the root barrel.
|
|
37
|
+
export * from './blueprint-stamp.js';
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/** Structural shape of a tool-identity catalog (handlers' ToolIdentityCatalog is assignable to this). */
|
|
2
|
+
export type ToolCatalogShape = Record<string, {
|
|
3
|
+
readonly name: string;
|
|
4
|
+
readonly version?: string;
|
|
5
|
+
}>;
|
|
6
|
+
/**
|
|
7
|
+
* Deterministic 16-char hex hash of a tool-identity catalog. Key order is
|
|
8
|
+
* normalized so the hash is stable across deployments that build the catalog
|
|
9
|
+
* in different orders.
|
|
10
|
+
*/
|
|
11
|
+
export declare function computeToolCatalogHash(catalog: ToolCatalogShape): string;
|
|
12
|
+
//# sourceMappingURL=blueprint-stamp.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"blueprint-stamp.d.ts","sourceRoot":"","sources":["../../src/registry/blueprint-stamp.ts"],"names":[],"mappings":"AAEA,yGAAyG;AACzG,MAAM,MAAM,gBAAgB,GAAG,MAAM,CAAC,MAAM,EAAE;IAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC,CAAC;AAEpG;;;;GAIG;AACH,wBAAgB,sBAAsB,CAAC,OAAO,EAAE,gBAAgB,GAAG,MAAM,CAKxE"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
/**
|
|
3
|
+
* Deterministic 16-char hex hash of a tool-identity catalog. Key order is
|
|
4
|
+
* normalized so the hash is stable across deployments that build the catalog
|
|
5
|
+
* in different orders.
|
|
6
|
+
*/
|
|
7
|
+
export function computeToolCatalogHash(catalog) {
|
|
8
|
+
const normalized = Object.keys(catalog)
|
|
9
|
+
.sort()
|
|
10
|
+
.map((k) => [k, catalog[k].name, catalog[k].version ?? null]);
|
|
11
|
+
return createHash("sha256").update(JSON.stringify(normalized)).digest("hex").slice(0, 16);
|
|
12
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { DataContract } from '../types/data-contract.js';
|
|
2
|
+
import type { BlueprintVariance } from '../types/blueprint.js';
|
|
2
3
|
type JsonValue = string | number | boolean | null | JsonValue[] | {
|
|
3
4
|
[key: string]: JsonValue;
|
|
4
5
|
};
|
|
@@ -15,12 +16,19 @@ type JsonValue = string | number | boolean | null | JsonValue[] | {
|
|
|
15
16
|
*
|
|
16
17
|
* Exported for tests / debugging — production callers should use
|
|
17
18
|
* `canonicalizeContracts()`.
|
|
19
|
+
*
|
|
20
|
+
* `stripProse` (default `true`) controls the `description`/`usage`
|
|
21
|
+
* STRIPPED_KEYS strip. The contract pipeline strips (prose is
|
|
22
|
+
* informational); the variance pipeline ({@link canonicalizeVariance})
|
|
23
|
+
* passes `false` because variance prose is load-bearing signal.
|
|
18
24
|
*/
|
|
19
|
-
export declare function canonicalizeValue(value: unknown): JsonValue | undefined;
|
|
25
|
+
export declare function canonicalizeValue(value: unknown, stripProse?: boolean): JsonValue | undefined;
|
|
20
26
|
/**
|
|
21
27
|
* Produce the canonical bytes for a `DataContract` value, suitable
|
|
22
28
|
* for hashing or external content-address lookups. Stable across
|
|
23
|
-
* paraphrase, key order, whitespace,
|
|
29
|
+
* paraphrase, key order, whitespace, description-only edits, and
|
|
30
|
+
* `serverInfo.version` (server-version metadata; `serverInfo.name` is
|
|
31
|
+
* identity and is preserved).
|
|
24
32
|
*
|
|
25
33
|
* Empty / undefined / `{}` all collapse to the same canonical bytes,
|
|
26
34
|
* which produces a stable `blueprintKey` for the "no-contract" case.
|
|
@@ -31,5 +39,26 @@ export declare function canonicalizeValue(value: unknown): JsonValue | undefined
|
|
|
31
39
|
* implementations using any JCS library produce the same bytes.
|
|
32
40
|
*/
|
|
33
41
|
export declare function canonicalizeContracts(contract: DataContract | undefined): string;
|
|
42
|
+
/**
|
|
43
|
+
* Canonical bytes for a {@link BlueprintVariance} block — the input to
|
|
44
|
+
* `variantKey()`. Same JCS + NFC pipeline as {@link canonicalizeContracts}
|
|
45
|
+
* with two DELIBERATE divergences from the contract pipeline:
|
|
46
|
+
*
|
|
47
|
+
* 1. **No `description`/`usage` strip.** For a contract, prose is
|
|
48
|
+
* informational and stripped so a doc tweak doesn't invalidate a
|
|
49
|
+
* cache key. For variance, the prose IS the signal: `seedPrompt`
|
|
50
|
+
* and any `context` value steer the generated `componentCode`, so
|
|
51
|
+
* stripping them would collapse genuinely distinct variants onto
|
|
52
|
+
* one key (false reuse). This is the inverse of the contract rule.
|
|
53
|
+
* 2. **Self-normalizing (D9).** Empty-string fields, empty objects,
|
|
54
|
+
* and empty arrays are elided internally so `undefined`, `{}`,
|
|
55
|
+
* `{persona:''}`, and an all-empty block all collapse to the same
|
|
56
|
+
* "default variant" canonical form. Callers never pre-normalize —
|
|
57
|
+
* the accept path (verbatim `blueprintMeta.variance`) and the
|
|
58
|
+
* override path produce identical keys for equivalent variance.
|
|
59
|
+
*
|
|
60
|
+
* Pure function — no I/O, no globals.
|
|
61
|
+
*/
|
|
62
|
+
export declare function canonicalizeVariance(variance: BlueprintVariance | undefined): string;
|
|
34
63
|
export {};
|
|
35
64
|
//# sourceMappingURL=canonicalize-contract.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"canonicalize-contract.d.ts","sourceRoot":"","sources":["../../src/registry/canonicalize-contract.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"canonicalize-contract.d.ts","sourceRoot":"","sources":["../../src/registry/canonicalize-contract.ts"],"names":[],"mappings":"AAqFA,OAAO,KAAK,EAAkB,YAAY,EAAE,MAAM,2BAA2B,CAAC;AAC9E,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,uBAAuB,CAAC;AAY/D,KAAK,SAAS,GACV,MAAM,GACN,MAAM,GACN,OAAO,GACP,IAAI,GACJ,SAAS,EAAE,GACX;IAAE,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,CAAA;CAAE,CAAC;AAEjC;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,iBAAiB,CAC/B,KAAK,EAAE,OAAO,EACd,UAAU,UAAO,GAChB,SAAS,GAAG,SAAS,CAgDvB;AA+CD;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,qBAAqB,CAAC,QAAQ,EAAE,YAAY,GAAG,SAAS,GAAG,MAAM,CAOhF;AA6BD;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,oBAAoB,CAClC,QAAQ,EAAE,iBAAiB,GAAG,SAAS,GACtC,MAAM,CAQR"}
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
* an agent's intent-prose override on a gadget export-use entry (the
|
|
35
35
|
* `description?` / `usage?` fields of each
|
|
36
36
|
* `clientCapabilities.gadgets[<package>][<exportName>]`, agent-authored
|
|
37
|
-
* at
|
|
37
|
+
* at render time) does not pollute the cache key.
|
|
38
38
|
*
|
|
39
39
|
* The strip is keyed on `description`/`usage` as STRING-VALUED fields,
|
|
40
40
|
* never as map keys. A gadget package, an `agentCapabilities.tools`
|
|
@@ -52,10 +52,22 @@
|
|
|
52
52
|
* - The four typed specs: `propsSpec`, `actionSpec`, `contextSpec`,
|
|
53
53
|
* `streamSpec` (including `streamSpec[*].source.tool` cross-refs).
|
|
54
54
|
* - The two catalogs: `agentCapabilities.tools[*]` (key names + their
|
|
55
|
-
* `inputSchema`/`outputSchema`) and
|
|
56
|
-
* (package-keyed — npm package names as
|
|
57
|
-
* the inner `exports` map keys; no
|
|
58
|
-
* on the wire).
|
|
55
|
+
* `toolInfo.inputSchema`/`toolInfo.outputSchema`) and
|
|
56
|
+
* `clientCapabilities.gadgets` (package-keyed — npm package names as
|
|
57
|
+
* outer keys, export names as the inner `exports` map keys; no
|
|
58
|
+
* `version`, no transport metadata on the wire).
|
|
59
|
+
*
|
|
60
|
+
* # Domain rule: `agentCapabilities.tools[*].serverInfo.version` is stripped
|
|
61
|
+
*
|
|
62
|
+
* `serverInfo` ({name, version?}) records the owning MCP server identity.
|
|
63
|
+
* `(serverInfo.name, toolName)` is the canonical cross-framework identity and
|
|
64
|
+
* stays in the hash — it disambiguates two servers that expose a byte-identical
|
|
65
|
+
* bare tool name (Todoist's `todo_add` vs Google-Tasks' `todo_add` must NOT
|
|
66
|
+
* reuse one another's UI). `serverInfo.version` is METADATA, not identity — a
|
|
67
|
+
* server version bump MUST NOT invalidate a registered blueprint — so ONLY the
|
|
68
|
+
* `version` field is removed (structurally, context-scoped to the tool-catalog
|
|
69
|
+
* recursion) BEFORE the JCS pass. `toolInfo.inputSchema` / `toolInfo.outputSchema`
|
|
70
|
+
* stay — they ARE identity (they shape the data).
|
|
59
71
|
* - Inner JSON Schema fields under every `schema:` wrapper (`type`,
|
|
60
72
|
* `enum`, `properties`, `required`, `items`, `additionalProperties`,
|
|
61
73
|
* `nullable`, `format`, …).
|
|
@@ -71,6 +83,7 @@
|
|
|
71
83
|
* Pure function — no I/O, no globals.
|
|
72
84
|
*/
|
|
73
85
|
import canonicalize from 'canonicalize';
|
|
86
|
+
import { isRecord } from '../validation/is-record.js';
|
|
74
87
|
/**
|
|
75
88
|
* Informational-prose field names removed from the canonical form —
|
|
76
89
|
* but ONLY where they are string-valued (see `canonicalizeValue`); a
|
|
@@ -92,8 +105,13 @@ const STRIPPED_KEYS = new Set(['description', 'usage']);
|
|
|
92
105
|
*
|
|
93
106
|
* Exported for tests / debugging — production callers should use
|
|
94
107
|
* `canonicalizeContracts()`.
|
|
108
|
+
*
|
|
109
|
+
* `stripProse` (default `true`) controls the `description`/`usage`
|
|
110
|
+
* STRIPPED_KEYS strip. The contract pipeline strips (prose is
|
|
111
|
+
* informational); the variance pipeline ({@link canonicalizeVariance})
|
|
112
|
+
* passes `false` because variance prose is load-bearing signal.
|
|
95
113
|
*/
|
|
96
|
-
export function canonicalizeValue(value) {
|
|
114
|
+
export function canonicalizeValue(value, stripProse = true) {
|
|
97
115
|
if (value === undefined)
|
|
98
116
|
return undefined;
|
|
99
117
|
if (value === null)
|
|
@@ -107,13 +125,13 @@ export function canonicalizeValue(value) {
|
|
|
107
125
|
if (Array.isArray(value)) {
|
|
108
126
|
const out = [];
|
|
109
127
|
for (const item of value) {
|
|
110
|
-
const c = canonicalizeValue(item);
|
|
128
|
+
const c = canonicalizeValue(item, stripProse);
|
|
111
129
|
if (c !== undefined)
|
|
112
130
|
out.push(c);
|
|
113
131
|
}
|
|
114
132
|
return out;
|
|
115
133
|
}
|
|
116
|
-
if (
|
|
134
|
+
if (isRecord(value)) {
|
|
117
135
|
const obj = value;
|
|
118
136
|
// NFC-normalize keys before sort so two contracts with the same
|
|
119
137
|
// key in different normalization forms (precomposed "café" vs
|
|
@@ -128,13 +146,14 @@ export function canonicalizeValue(value) {
|
|
|
128
146
|
// literally `description`/`usage` as a MAP key (a gadget package,
|
|
129
147
|
// an `agentCapabilities.tools` entry, a spec slot) maps to an
|
|
130
148
|
// object/array and MUST survive: stripping it collapses distinct
|
|
131
|
-
// contracts onto one blueprint cache key.
|
|
132
|
-
|
|
149
|
+
// contracts onto one blueprint cache key. Skipped entirely when
|
|
150
|
+
// `stripProse` is false (the variance pipeline).
|
|
151
|
+
.filter((k) => !(stripProse && STRIPPED_KEYS.has(k) && typeof obj[k] === 'string'))
|
|
133
152
|
.map((k) => ({ source: k, normalized: k.normalize('NFC') }))
|
|
134
153
|
.sort((a, b) => (a.normalized < b.normalized ? -1 : a.normalized > b.normalized ? 1 : 0));
|
|
135
154
|
const out = {};
|
|
136
155
|
for (const { source, normalized } of entries) {
|
|
137
|
-
const c = canonicalizeValue(obj[source]);
|
|
156
|
+
const c = canonicalizeValue(obj[source], stripProse);
|
|
138
157
|
if (c !== undefined)
|
|
139
158
|
out[normalized] = c;
|
|
140
159
|
}
|
|
@@ -143,10 +162,43 @@ export function canonicalizeValue(value) {
|
|
|
143
162
|
// Functions, symbols, bigint — not JSON-serializable; treat as absent.
|
|
144
163
|
return undefined;
|
|
145
164
|
}
|
|
165
|
+
/**
|
|
166
|
+
* Structurally reduce `agentCapabilities.tools[*].serverInfo` to its
|
|
167
|
+
* identity-bearing `name` before the generic JCS walk — `serverInfo.name`
|
|
168
|
+
* is load-bearing identity (it disambiguates two servers that expose a
|
|
169
|
+
* byte-identical bare tool name, e.g. Todoist vs Google-Tasks `todo_add`),
|
|
170
|
+
* while `serverInfo.version` is metadata that MUST NOT bust the cache key.
|
|
171
|
+
* Context-scoped to the tool catalog (NOT a global value-guard) so a spec
|
|
172
|
+
* slot / gadget package / tool literally named `serverInfo` elsewhere can
|
|
173
|
+
* never be collateral-stripped. Pure — returns a shallow-cloned projection;
|
|
174
|
+
* the input is never mutated. Returns the input unchanged when there is no
|
|
175
|
+
* `agentCapabilities.tools` to walk.
|
|
176
|
+
*/
|
|
177
|
+
function stripToolServerInfo(contract) {
|
|
178
|
+
const tools = contract.agentCapabilities?.tools;
|
|
179
|
+
if (!tools)
|
|
180
|
+
return contract;
|
|
181
|
+
const cleanedTools = {};
|
|
182
|
+
for (const [name, entry] of Object.entries(tools)) {
|
|
183
|
+
if (entry.serverInfo === undefined) {
|
|
184
|
+
cleanedTools[name] = entry;
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
187
|
+
// Keep serverInfo.name (identity); drop serverInfo.version (metadata).
|
|
188
|
+
const { version: _version, ...nameOnly } = entry.serverInfo;
|
|
189
|
+
cleanedTools[name] = { ...entry, serverInfo: nameOnly };
|
|
190
|
+
}
|
|
191
|
+
return {
|
|
192
|
+
...contract,
|
|
193
|
+
agentCapabilities: { ...contract.agentCapabilities, tools: cleanedTools },
|
|
194
|
+
};
|
|
195
|
+
}
|
|
146
196
|
/**
|
|
147
197
|
* Produce the canonical bytes for a `DataContract` value, suitable
|
|
148
198
|
* for hashing or external content-address lookups. Stable across
|
|
149
|
-
* paraphrase, key order, whitespace,
|
|
199
|
+
* paraphrase, key order, whitespace, description-only edits, and
|
|
200
|
+
* `serverInfo.version` (server-version metadata; `serverInfo.name` is
|
|
201
|
+
* identity and is preserved).
|
|
150
202
|
*
|
|
151
203
|
* Empty / undefined / `{}` all collapse to the same canonical bytes,
|
|
152
204
|
* which produces a stable `blueprintKey` for the "no-contract" case.
|
|
@@ -157,10 +209,70 @@ export function canonicalizeValue(value) {
|
|
|
157
209
|
* implementations using any JCS library produce the same bytes.
|
|
158
210
|
*/
|
|
159
211
|
export function canonicalizeContracts(contract) {
|
|
160
|
-
const stripped = canonicalizeValue(contract ?? {});
|
|
212
|
+
const stripped = canonicalizeValue(stripToolServerInfo(contract ?? {}));
|
|
161
213
|
// `canonicalize` may return undefined if every field was stripped;
|
|
162
214
|
// fall back to the empty-object canonical form so the hash function
|
|
163
215
|
// always receives a stable string.
|
|
164
216
|
const result = canonicalize(stripped);
|
|
165
217
|
return result ?? '{}';
|
|
166
218
|
}
|
|
219
|
+
/**
|
|
220
|
+
* Recursively elide "absent-equivalent" fields so semantically-empty
|
|
221
|
+
* variance collapses to the empty-object canonical form. Empty strings
|
|
222
|
+
* (`''`), empty objects (`{}`), empty arrays, `null`, and `undefined`
|
|
223
|
+
* are all dropped; a primitive that survives is kept verbatim. This is
|
|
224
|
+
* the D9 self-normalization step — callers never pre-normalize.
|
|
225
|
+
*/
|
|
226
|
+
function elideEmpty(value) {
|
|
227
|
+
if (value === undefined || value === null)
|
|
228
|
+
return undefined;
|
|
229
|
+
if (typeof value === 'string')
|
|
230
|
+
return value === '' ? undefined : value;
|
|
231
|
+
if (typeof value === 'number' || typeof value === 'boolean')
|
|
232
|
+
return value;
|
|
233
|
+
if (Array.isArray(value)) {
|
|
234
|
+
const out = [];
|
|
235
|
+
for (const item of value) {
|
|
236
|
+
const e = elideEmpty(item);
|
|
237
|
+
if (e !== undefined)
|
|
238
|
+
out.push(e);
|
|
239
|
+
}
|
|
240
|
+
return out.length === 0 ? undefined : out;
|
|
241
|
+
}
|
|
242
|
+
const out = {};
|
|
243
|
+
for (const key of Object.keys(value)) {
|
|
244
|
+
const e = elideEmpty(value[key]);
|
|
245
|
+
if (e !== undefined)
|
|
246
|
+
out[key] = e;
|
|
247
|
+
}
|
|
248
|
+
return Object.keys(out).length === 0 ? undefined : out;
|
|
249
|
+
}
|
|
250
|
+
/**
|
|
251
|
+
* Canonical bytes for a {@link BlueprintVariance} block — the input to
|
|
252
|
+
* `variantKey()`. Same JCS + NFC pipeline as {@link canonicalizeContracts}
|
|
253
|
+
* with two DELIBERATE divergences from the contract pipeline:
|
|
254
|
+
*
|
|
255
|
+
* 1. **No `description`/`usage` strip.** For a contract, prose is
|
|
256
|
+
* informational and stripped so a doc tweak doesn't invalidate a
|
|
257
|
+
* cache key. For variance, the prose IS the signal: `seedPrompt`
|
|
258
|
+
* and any `context` value steer the generated `componentCode`, so
|
|
259
|
+
* stripping them would collapse genuinely distinct variants onto
|
|
260
|
+
* one key (false reuse). This is the inverse of the contract rule.
|
|
261
|
+
* 2. **Self-normalizing (D9).** Empty-string fields, empty objects,
|
|
262
|
+
* and empty arrays are elided internally so `undefined`, `{}`,
|
|
263
|
+
* `{persona:''}`, and an all-empty block all collapse to the same
|
|
264
|
+
* "default variant" canonical form. Callers never pre-normalize —
|
|
265
|
+
* the accept path (verbatim `blueprintMeta.variance`) and the
|
|
266
|
+
* override path produce identical keys for equivalent variance.
|
|
267
|
+
*
|
|
268
|
+
* Pure function — no I/O, no globals.
|
|
269
|
+
*/
|
|
270
|
+
export function canonicalizeVariance(variance) {
|
|
271
|
+
// NFC-normalize + sort keys (canonicalizeValue), but withOUT the
|
|
272
|
+
// STRIPPED_KEYS prose strip (`stripProse: false`) — variance prose
|
|
273
|
+
// is load-bearing signal, the inverse of the contract rule.
|
|
274
|
+
const normalized = canonicalizeValue(variance ?? {}, false);
|
|
275
|
+
const elided = elideEmpty(normalized) ?? {};
|
|
276
|
+
const result = canonicalize(elided);
|
|
277
|
+
return result ?? '{}';
|
|
278
|
+
}
|