@ggui-ai/mcp-server 0.2.0-alpha.4 → 0.3.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.
Files changed (152) hide show
  1. package/dist/admin-blueprints-transport.d.ts.map +1 -1
  2. package/dist/admin-blueprints-transport.js +2 -1
  3. package/dist/admin-oauth-providers-transport.d.ts.map +1 -1
  4. package/dist/admin-oauth-providers-transport.js +7 -5
  5. package/dist/api-renders-routes.d.ts +85 -0
  6. package/dist/api-renders-routes.d.ts.map +1 -0
  7. package/dist/api-renders-routes.js +372 -0
  8. package/dist/build-mcp.d.ts +1 -1
  9. package/dist/build-mcp.d.ts.map +1 -1
  10. package/dist/build-mcp.js +39 -5
  11. package/dist/code-routes.d.ts +47 -0
  12. package/dist/code-routes.d.ts.map +1 -0
  13. package/dist/code-routes.js +81 -0
  14. package/dist/code-store-fs.js +2 -2
  15. package/dist/console-auth.d.ts +10 -10
  16. package/dist/console-auth.d.ts.map +1 -1
  17. package/dist/console-auth.js +5 -5
  18. package/dist/console-blueprint-routes.d.ts +71 -0
  19. package/dist/console-blueprint-routes.d.ts.map +1 -0
  20. package/dist/console-blueprint-routes.js +348 -0
  21. package/dist/console-chat-routes.d.ts +80 -0
  22. package/dist/console-chat-routes.d.ts.map +1 -0
  23. package/dist/console-chat-routes.js +182 -0
  24. package/dist/console-config-routes.d.ts +37 -0
  25. package/dist/console-config-routes.d.ts.map +1 -0
  26. package/dist/console-config-routes.js +91 -0
  27. package/dist/console-headers.d.ts +1 -1
  28. package/dist/console-info-routes.d.ts +84 -0
  29. package/dist/console-info-routes.d.ts.map +1 -0
  30. package/dist/console-info-routes.js +135 -0
  31. package/dist/console-keys-routes.d.ts +50 -0
  32. package/dist/console-keys-routes.d.ts.map +1 -0
  33. package/dist/console-keys-routes.js +222 -0
  34. package/dist/console-llm-keys-routes.d.ts +47 -0
  35. package/dist/console-llm-keys-routes.d.ts.map +1 -0
  36. package/dist/console-llm-keys-routes.js +443 -0
  37. package/dist/console-mcp-tools-routes.d.ts +41 -0
  38. package/dist/console-mcp-tools-routes.d.ts.map +1 -0
  39. package/dist/console-mcp-tools-routes.js +60 -0
  40. package/dist/console-registry-routes.d.ts +66 -0
  41. package/dist/console-registry-routes.d.ts.map +1 -0
  42. package/dist/console-registry-routes.js +276 -0
  43. package/dist/console-session-routes.d.ts +89 -0
  44. package/dist/console-session-routes.d.ts.map +1 -0
  45. package/dist/console-session-routes.js +385 -0
  46. package/dist/console-sessions-routes.d.ts +52 -0
  47. package/dist/console-sessions-routes.d.ts.map +1 -0
  48. package/dist/console-sessions-routes.js +106 -0
  49. package/dist/console-static-routes.d.ts +54 -0
  50. package/dist/console-static-routes.d.ts.map +1 -0
  51. package/dist/console-static-routes.js +190 -0
  52. package/dist/console-theme-routes.d.ts +3 -3
  53. package/dist/console-theme-routes.js +1 -1
  54. package/dist/console-timeline.d.ts +5 -5
  55. package/dist/console-timeline.d.ts.map +1 -1
  56. package/dist/console-timeline.js +27 -26
  57. package/dist/console-welcome.js +2 -2
  58. package/dist/email-login.d.ts.map +1 -1
  59. package/dist/email-login.js +2 -3
  60. package/dist/ggui-session-channel/action-ingress.d.ts +54 -0
  61. package/dist/ggui-session-channel/action-ingress.d.ts.map +1 -0
  62. package/dist/ggui-session-channel/action-ingress.js +228 -0
  63. package/dist/ggui-session-channel/channel-subscriptions.d.ts +97 -0
  64. package/dist/ggui-session-channel/channel-subscriptions.d.ts.map +1 -0
  65. package/dist/ggui-session-channel/channel-subscriptions.js +224 -0
  66. package/dist/ggui-session-channel/internal-types.d.ts +102 -0
  67. package/dist/ggui-session-channel/internal-types.d.ts.map +1 -0
  68. package/dist/ggui-session-channel/internal-types.js +6 -0
  69. package/dist/ggui-session-channel/outbound.d.ts +81 -0
  70. package/dist/ggui-session-channel/outbound.d.ts.map +1 -0
  71. package/dist/ggui-session-channel/outbound.js +174 -0
  72. package/dist/ggui-session-channel/socket-router.d.ts +38 -0
  73. package/dist/ggui-session-channel/socket-router.d.ts.map +1 -0
  74. package/dist/ggui-session-channel/socket-router.js +213 -0
  75. package/dist/ggui-session-channel/subscribe.d.ts +165 -0
  76. package/dist/ggui-session-channel/subscribe.d.ts.map +1 -0
  77. package/dist/ggui-session-channel/subscribe.js +370 -0
  78. package/dist/ggui-session-channel/subscriber-lifecycle.d.ts +40 -0
  79. package/dist/ggui-session-channel/subscriber-lifecycle.d.ts.map +1 -0
  80. package/dist/ggui-session-channel/subscriber-lifecycle.js +123 -0
  81. package/dist/ggui-session-channel.d.ts +425 -0
  82. package/dist/ggui-session-channel.d.ts.map +1 -0
  83. package/dist/ggui-session-channel.js +262 -0
  84. package/dist/health-routes.d.ts +76 -0
  85. package/dist/health-routes.d.ts.map +1 -0
  86. package/dist/health-routes.js +145 -0
  87. package/dist/index.d.ts +10 -11
  88. package/dist/index.d.ts.map +1 -1
  89. package/dist/index.js +8 -9
  90. package/dist/instructions-presets.d.ts +3 -3
  91. package/dist/instructions-presets.js +24 -24
  92. package/dist/llm-backed-negotiator.d.ts +68 -67
  93. package/dist/llm-backed-negotiator.d.ts.map +1 -1
  94. package/dist/llm-backed-negotiator.js +82 -221
  95. package/dist/mcp-apps-outbound.d.ts +47 -48
  96. package/dist/mcp-apps-outbound.d.ts.map +1 -1
  97. package/dist/mcp-apps-outbound.js +154 -177
  98. package/dist/mcp-endpoint-routes.d.ts +88 -0
  99. package/dist/mcp-endpoint-routes.d.ts.map +1 -0
  100. package/dist/mcp-endpoint-routes.js +359 -0
  101. package/dist/mcp-mounts.d.ts +2 -76
  102. package/dist/mcp-mounts.d.ts.map +1 -1
  103. package/dist/mcp-mounts.js +0 -76
  104. package/dist/oauth-as-routes.d.ts +60 -0
  105. package/dist/oauth-as-routes.d.ts.map +1 -0
  106. package/dist/oauth-as-routes.js +82 -0
  107. package/dist/oauth-clients-routes.d.ts +39 -0
  108. package/dist/oauth-clients-routes.d.ts.map +1 -0
  109. package/dist/oauth-clients-routes.js +87 -0
  110. package/dist/oauth-login-types.d.ts +1 -20
  111. package/dist/oauth-login-types.d.ts.map +1 -1
  112. package/dist/oauth-login-types.js +30 -7
  113. package/dist/oauth-login.d.ts.map +1 -1
  114. package/dist/oauth-login.js +3 -2
  115. package/dist/oauth-providers-store.d.ts.map +1 -1
  116. package/dist/oauth-providers-store.js +5 -5
  117. package/dist/oauth.d.ts +9 -8
  118. package/dist/oauth.d.ts.map +1 -1
  119. package/dist/oauth.js +41 -19
  120. package/dist/pairing-transport.d.ts.map +1 -1
  121. package/dist/pairing-transport.js +2 -1
  122. package/dist/request-context.d.ts +2 -2
  123. package/dist/request-context.js +2 -2
  124. package/dist/reserved-validators.d.ts.map +1 -1
  125. package/dist/reserved-validators.js +9 -1
  126. package/dist/route-param.d.ts +9 -0
  127. package/dist/route-param.d.ts.map +1 -0
  128. package/dist/route-param.js +10 -0
  129. package/dist/runtime-bundle-route.d.ts +43 -0
  130. package/dist/runtime-bundle-route.d.ts.map +1 -0
  131. package/dist/runtime-bundle-route.js +80 -0
  132. package/dist/schema-compat.d.ts +64 -62
  133. package/dist/schema-compat.d.ts.map +1 -1
  134. package/dist/schema-compat.js +23 -51
  135. package/dist/server.d.ts +179 -193
  136. package/dist/server.d.ts.map +1 -1
  137. package/dist/server.js +644 -3759
  138. package/dist/storage.d.ts +5 -5
  139. package/dist/storage.d.ts.map +1 -1
  140. package/dist/storage.js +5 -5
  141. package/dist/thread-transport.d.ts.map +1 -1
  142. package/dist/thread-transport.js +4 -3
  143. package/dist/user-session-auth.d.ts +7 -21
  144. package/dist/user-session-auth.d.ts.map +1 -1
  145. package/dist/user-session-auth.js +7 -28
  146. package/package.json +16 -15
  147. package/dist/mcp-apps-inbound.d.ts +0 -86
  148. package/dist/mcp-apps-inbound.d.ts.map +0 -1
  149. package/dist/mcp-apps-inbound.js +0 -283
  150. package/dist/render-channel.d.ts +0 -694
  151. package/dist/render-channel.d.ts.map +0 -1
  152. package/dist/render-channel.js +0 -1775
@@ -1,78 +1,43 @@
1
1
  /**
2
2
  * LLM-backed `HandshakeNegotiator` for the OSS server.
3
3
  *
4
- * Composes BYOK credentials + a `ProviderAdapter` from
5
- * `@ggui-ai/ui-gen/providers` into an `LLMCaller`, then runs
6
- * `@ggui-ai/negotiator`'s `negotiate()` pipeline. Mirrors the cloud
7
- * `createBedrockNegotiator` pattern (cloud/ggui-protocol-pod/src/
8
- * tools/handshake.ts), stripped of Bedrock-specific embedding +
9
- * vector-store wiring — those are absent on OSS by default and
10
- * `negotiate()` handles missing RAG deps gracefully.
4
+ * A thin adapter over the SHARED handshake-decision core
5
+ * (`decideHandshake` in `@ggui-ai/mcp-server-handlers`). This file owns
6
+ * only this server's seams; the decision spine is shared verbatim
7
+ * across every negotiator binding of the server family — same code,
8
+ * different adapter.
11
9
  *
12
- * ## What this binding does
10
+ * ## OSS adapter seams
13
11
  *
14
- * On every `ggui_handshake` call:
12
+ * - **LLM** — `buildLlmCaller` composes BYOK credentials + a
13
+ * `ProviderAdapter` from `@ggui-ai/ui-gen/providers` into an
14
+ * `LLMCaller` (anthropic / openai / google / openrouter / bedrock).
15
+ * The adapter's `resolveLlm(ctx)` returns it, or `undefined` when no
16
+ * creds resolve (⇒ the core returns a no-LLM `create` fallback).
17
+ * - **Pools** — a single per-app blueprint pool from `deps.cache`
18
+ * (scope defaults to `ctx.appId`). Absent ⇒ no pools ⇒ synth-only.
19
+ * - **warn** — `console.warn` for swallowed operational errors.
15
20
  *
16
- * 1. Resolves BYOK creds via the supplied `resolveLlm(ctx)`. No
17
- * creds → returns a "create" result with a `no-creds` reason.
18
- * 2. Selects the matching `ProviderAdapter` for the resolved
19
- * provider (anthropic / openai / google / openrouter / bedrock).
20
- * 3. Wraps the adapter into an `LLMCaller` (single-shot text
21
- * completion).
22
- * 4. Calls `negotiate(deps, input)` with `embedding: undefined,
23
- * vectors: undefined` — RAG search is skipped, decision LLM
24
- * runs against an empty candidate list, returns a sensible
25
- * create/update decision based on session state + agent
26
- * prompt.
27
- * 5. Maps the `NegotiateResult` onto the OSS-shape
28
- * `HandshakeNegotiatorResult`.
21
+ * ## Shared decision spine (in the core, not here)
29
22
  *
30
- * ## Failure modes
23
+ * find-similar across pools (exact-key → coverage guard → judge →
24
+ * atomic reuse) ⇒ else synth-repair create via
25
+ * `ensureConformingContract`. Operational errors fail open; programmer
26
+ * errors re-throw. See `decide-handshake.ts` for the full contract.
31
27
  *
32
- * Operational errors (network flap, provider 5xx, rate limit) fail
33
- * open: returns a "create" result with the error reason. Bugs
34
- * (TypeError / ReferenceError / RangeError / SyntaxError) re-throw
35
- * — those are programmer errors that should surface, not be
36
- * silently swallowed.
28
+ * ## selectVariant
37
29
  *
38
- * ## Cost posture
39
- *
40
- * One LLM call per handshake (when creds resolve). Operators
41
- * concerned about cost can either (a) skip handshake and call
42
- * `ggui_render` directly with `{story}`, or (b) bind a different
43
- * negotiator (e.g., the cache-backed one for read-only cache
44
- * lookups) via `createGguiServer({handshake: {negotiator: ...}})`.
30
+ * The optional LLM-driven variant-selection seam is OSS-specific and
31
+ * stays on this binding (it is not part of the shared decide spine).
45
32
  *
46
33
  * ## Default binding
47
34
  *
48
- * Bound by default in `createGguiServer` when handshake is enabled
49
- * AND a `resolveLlm` is wired into `generation`. OSS is cloud-aligned:
50
- * same `negotiate()` pipeline, just with degraded RAG when local
51
- * infrastructure isn't bound.
35
+ * Bound by default in `createGguiServer` when handshake is enabled AND a
36
+ * `resolveLlm` is wired into `generation`.
52
37
  */
53
- import { DEFAULT_GENERATOR_SLUG, matchBlueprint, } from "@ggui-ai/mcp-server-handlers/renders";
54
- import { ensureConformingContract } from "@ggui-ai/negotiator";
55
- import { dataContractSchema, lintContract } from "@ggui-ai/protocol";
56
- import { blueprintKey } from "@ggui-ai/protocol/blueprint-key";
38
+ import { decideHandshake, } from "@ggui-ai/mcp-server-handlers/renders";
39
+ import { isRecord } from "@ggui-ai/protocol";
57
40
  import { selectAdapter } from "@ggui-ai/ui-gen/providers";
58
- import { createHash, randomUUID } from "node:crypto";
59
- /**
60
- * Operational error classifier — mirrors the cloud helper. Bugs
61
- * surface; provider failures + network blips degrade to a "create"
62
- * stub so the agent isn't blocked by a transient infrastructure
63
- * issue.
64
- */
65
- function isOperationalError(err) {
66
- if (err instanceof TypeError)
67
- return false;
68
- if (err instanceof ReferenceError)
69
- return false;
70
- if (err instanceof RangeError)
71
- return false;
72
- if (err instanceof SyntaxError)
73
- return false;
74
- return true;
75
- }
76
41
  /**
77
42
  * Wrap a resolved BYOK credential pair into an `LLMCaller` the
78
43
  * negotiator can call. The adapter is chosen via `selectAdapter`
@@ -195,174 +160,70 @@ async function anthropicCallStructured(args) {
195
160
  }
196
161
  return toolUse.input;
197
162
  }
198
- const DEFAULT_GEN_LATENCY_MS = 30_000;
199
- function buildCreateFallback(draftContract, reason, _estimatedLatencyMs) {
200
- // No-LLM fallback (no BYOK creds, or an operational error during synth).
201
- // The handshake backstop (validateContract) THROWS on a malformed draft,
202
- // and there is no LLM here to repair it — so deterministically
203
- // substitute the trivially-conforming empty contract (+ loud findings)
204
- // when the draft fails the gate. NEVER return a raw malformed draft into
205
- // the throwing backstop (that would revive the handshake-retry loop).
206
- // A clean draft is kept verbatim (origin: agent).
207
- const lint = lintContract(draftContract);
208
- const contract = lint.errors.length === 0 ? dataContractSchema.parse(draftContract) : {};
209
- const findings = lint.errors.map((e) => ({
210
- code: e.code,
211
- severity: "error",
212
- path: e.path,
213
- message: e.message,
214
- }));
215
- const contractHash = blueprintKey(contract);
216
- const suggestion = {
217
- origin: "agent",
218
- rationale: reason,
219
- blueprintMeta: {
220
- blueprintId: `bp_${randomUUID()}`,
221
- contractHash,
222
- generator: "ui-gen-default-haiku-4-5",
223
- variance: {},
224
- },
225
- ...(findings.length > 0 ? { validationFindings: findings } : {}),
226
- };
227
- return {
228
- action: "create",
229
- reason,
230
- suggestion,
231
- effectiveContract: contract,
232
- };
163
+ /**
164
+ * Build the handshake pool list: the per-app pool (from `cache`) first —
165
+ * so a deployment's own blueprints win on exact-key first-match — then
166
+ * any read-only seed pools.
167
+ */
168
+ export function assembleHandshakePools(deps) {
169
+ const perAppPool = deps.cache
170
+ ? {
171
+ registry: deps.cache,
172
+ ...(deps.installedBlueprints ? { installedBlueprints: deps.installedBlueprints } : {}),
173
+ }
174
+ : undefined;
175
+ return [...(perAppPool ? [perAppPool] : []), ...(deps.seedPools ?? [])];
233
176
  }
234
177
  /**
235
178
  * Build an LLM-backed `HandshakeNegotiator` for the OSS server.
236
- * Wires BYOK creds + an LLM provider adapter into
237
- * `@ggui-ai/negotiator`'s `negotiate()` pipeline. Operators get the
238
- * same negotiation shape cloud uses, with RAG gracefully degraded
239
- * (no embedding / vectors required).
179
+ *
180
+ * Thin wrapper over the shared `decideHandshake` core
181
+ * (`@ggui-ai/mcp-server-handlers`): injects the OSS adapter — a BYOK
182
+ * LLM resolver (`resolveLlm` → `buildLlmCaller`) and a single per-app
183
+ * blueprint pool (`deps.cache`, scope defaults to `ctx.appId`). The
184
+ * decision spine (find-similar → coverage guard → judge → atomic
185
+ * reuse, else synth-repair create) is shared verbatim across every
186
+ * negotiator binding; only the injected adapter differs.
187
+ * `selectVariant` is specific to this binding.
240
188
  *
241
189
  * @public
242
190
  */
243
191
  export function createLlmBackedHandshakeNegotiator(deps) {
244
- const estimatedLatencyMs = deps.estimatedGenerationLatencyMs ?? DEFAULT_GEN_LATENCY_MS;
245
- return {
246
- async decide({ intent, blueprintDraft, gadgets, ctx }) {
247
- const draftContract = blueprintDraft.contract;
248
- // Handshake-time exact-key fast path. Runs BEFORE the BYOK
249
- // creds resolve + LLM synth — a cache hit needs neither the
250
- // operator's API key nor a model round-trip. When the agent's
251
- // draft canonical-key-equals a registered blueprint, return
252
- // `origin: 'cache'` with the matched blueprint's contract +
253
- // componentCode hash so the paired push.accept short-circuits
254
- // straight into commitCachedRender.
255
- //
256
- // No-match (or any throw) falls through to today's synth path
257
- // — the negotiator stays useful when the registry is cold,
258
- // when matchBlueprint hiccups, or when the deployment skipped
259
- // the cache deps entirely.
260
- // Cache match needs a SHAPE-VALID contract — a malformed draft
261
- // (wrapper-nesting, bad type spelling, …) can't canonical-key-match
262
- // a registered blueprint, so skip the probe and fall straight to
263
- // validate/repair via ensureConformingContract.
264
- const parsedDraft = dataContractSchema.safeParse(draftContract);
265
- if (deps.cache && parsedDraft.success) {
266
- try {
267
- const matchDeps = {
268
- registry: deps.cache,
269
- ...(deps.installedBlueprints ? { installedBlueprints: deps.installedBlueprints } : {}),
270
- };
271
- const matchResult = await matchBlueprint(matchDeps, ctx.appId, {
272
- intent,
273
- contract: parsedDraft.data,
274
- });
275
- if (matchResult.strategy === "exact-key") {
276
- const matched = matchResult.blueprint;
277
- const codeHash = createHash("sha256").update(matched.componentCode).digest("hex");
278
- const suggestion = {
279
- origin: "cache",
280
- rationale: matchResult.reason,
281
- blueprintMeta: {
282
- blueprintId: matched.id,
283
- contractHash: matched.contractKey,
284
- codeHash,
285
- generator: DEFAULT_GENERATOR_SLUG,
286
- variance: {},
287
- selectedReason: matchResult.reason,
288
- },
289
- };
290
- return {
291
- action: "reuse",
292
- reason: matchResult.reason,
293
- suggestion,
294
- effectiveContract: matched.contract,
295
- };
296
- }
297
- // Other strategies (no-match, semantic) fall through to
298
- // the negotiate() path below. Semantic doesn't fire when
299
- // a contract is supplied (the fuzzy-match gate blocks it);
300
- // listing it here is structural completeness, not a
301
- // reachable branch today.
302
- }
303
- catch (err) {
304
- if (!isOperationalError(err))
305
- throw err;
306
- // Registry hiccup — log + fall through to synth so the
307
- // handshake never crashes on a transient cache backend
308
- // issue.
309
- const message = err instanceof Error ? err.message : String(err);
310
- // eslint-disable-next-line no-console -- operator-visible signal
311
- console.warn(`[llm-backed-negotiator] matchBlueprint exact-key probe failed; falling through to synth: ${message}`);
312
- }
313
- }
192
+ // Capture the store so the adapter resolver closes over a concrete
193
+ // value (no `?.` chain inside the hot path; the spread below already
194
+ // gates on presence).
195
+ const catalogStore = deps.catalogStore;
196
+ const pools = assembleHandshakePools(deps);
197
+ const adapter = {
198
+ // BYOK seam: resolve per-ctx creds + wrap into an LLMCaller. No
199
+ // creds ⇒ undefined ⇒ the core returns a no-LLM create fallback.
200
+ resolveLlm: async (ctx) => {
314
201
  const creds = await deps.resolveLlm(ctx);
315
- if (!creds) {
316
- return buildCreateFallback(draftContract, "no-creds: no BYOK credentials resolved for the configured provider; ggui_render will surface the same error and the handshake stays a no-op create.", estimatedLatencyMs);
317
- }
318
- try {
319
- const llm = buildLlmCaller(creds.selection, creds.providerKey);
320
- // Create path (no cache hit): the agent's DRAFT is the basis.
321
- // ensureConformingContract validates it against the deterministic
322
- // gate (lintContract / validateContract) and, on errors, repairs
323
- // it in place via the bounded LLM loop — GUARANTEEING a contract
324
- // that passes the handshake backstop, so the handshake never
325
- // hard-fails on a malformed draft. origin: 'agent' = draft was
326
- // already clean; 'synth' = repaired (or minimal-conforming
327
- // fallback when unrepairable).
328
- const conforming = await ensureConformingContract({ llm }, {
329
- intent,
330
- draft: draftContract,
331
- ...(gadgets !== undefined ? { appGadgets: gadgets } : {}),
332
- });
333
- const contractHash = blueprintKey(conforming.contract);
334
- const suggestion = {
335
- origin: conforming.origin,
336
- rationale: conforming.reasoning,
337
- blueprintMeta: {
338
- blueprintId: `bp_${randomUUID()}`,
339
- contractHash,
340
- generator: "ui-gen-default-haiku-4-5",
341
- variance: {},
342
- },
343
- ...(conforming.findings.length > 0
344
- ? { validationFindings: conforming.findings }
345
- : {}),
346
- };
347
- return {
348
- action: "create",
349
- reason: conforming.reasoning,
350
- suggestion,
351
- effectiveContract: conforming.contract,
352
- };
353
- }
354
- catch (err) {
355
- if (!isOperationalError(err))
356
- throw err;
357
- const message = err instanceof Error ? err.message : String(err);
358
- const errorClass = err instanceof Error ? err.name : "unknown";
359
- return buildCreateFallback(draftContract, `negotiator-degraded: ${errorClass} during decision LLM call — ${message}. Falling back to bare-create; the paired ggui_render will still generate the UI.`, estimatedLatencyMs);
360
- }
202
+ return creds
203
+ ? buildLlmCaller(creds.selection, creds.providerKey)
204
+ : undefined;
205
+ },
206
+ // Per-app pool first, then seed pools. No pools ⇒ the core takes
207
+ // the synth-only path.
208
+ ...(pools.length > 0 ? { pools } : {}),
209
+ warn: (message) => {
210
+ // eslint-disable-next-line no-console -- operator-visible signal
211
+ console.warn(message);
361
212
  },
213
+ // READ side of tool-identity canonicalization. When a catalog store
214
+ // is wired, the core resolves the per-app catalog by ctx.appId and
215
+ // runs canonicalizeToolIdentity before keying. Absent ⇒ the seam is
216
+ // a no-op (Tier 2).
217
+ ...(catalogStore
218
+ ? { toolIdentityCatalog: (ctx) => catalogStore.get(ctx.appId) }
219
+ : {}),
220
+ };
221
+ return {
222
+ decide: (input) => decideHandshake(adapter, input),
362
223
  // LLM-driven variant selection. Reads each candidate's
363
224
  // `variance` + `validatorScore` + `isOperatorDefault` + the
364
- // generator slug, asks the LLM to pick the best fit for the
365
- // current request's `intent` + `variance`, and returns a
225
+ // provenance (`source`), asks the LLM to pick the best fit for
226
+ // the current request's `intent` + `variance`, and returns a
366
227
  // calibrated decision. The caller (`selectVariantWithLlm`)
367
228
  // thresholds on `confidence`.
368
229
  //
@@ -395,7 +256,7 @@ export const VARIANT_SELECTION_SYSTEM_PROMPT = `You are the variant selector for
395
256
 
396
257
  Each variant carries:
397
258
  - blueprintId: stable identity (you MUST echo back exactly one of these).
398
- - generator: which generator built it (e.g. "ui-gen-default-haiku-4-5", "ui-gen-advanced-opus-4-7"). Advanced wins on visual polish; default wins on simplicity.
259
+ - source: provenance. {kind:"llm", generator, model} = engine-generated (advanced generators win on visual polish; default generators win on simplicity). {kind:"user"} = operator-registered hand-authored code. {kind:"curated"} = catalog-shipped system blueprint.
399
260
  - validatorScore: optional 0-1 self-assessed quality from the advanced generator's validators. Higher is better, undefined ⇒ unknown.
400
261
  - isOperatorDefault: true ⇒ the human operator pinned this as the default. Strong signal.
401
262
  - variance.persona: free-form tag ("minimalist", "data-dense", "mobile-first"…).
@@ -452,7 +313,7 @@ const VARIANT_SELECTION_TOOL_SCHEMA = {
452
313
  export function buildVariantSelectionUserMessage(candidates, context) {
453
314
  const projectedCandidates = candidates.map((c) => ({
454
315
  blueprintId: c.blueprintId,
455
- generator: c.generator,
316
+ source: c.source,
456
317
  ...(c.validatorScore !== undefined ? { validatorScore: c.validatorScore } : {}),
457
318
  ...(c.isOperatorDefault === true ? { isOperatorDefault: true } : {}),
458
319
  variance: {
@@ -530,7 +391,7 @@ async function runVariantSelectionLlm(llm, candidates, context) {
530
391
  * `VariantSelectionResult.reason` for telemetry.
531
392
  */
532
393
  export function parseVariantSelectionResponse(raw) {
533
- if (raw === null || typeof raw !== "object") {
394
+ if (!isRecord(raw)) {
534
395
  throw new Error("variant-selection: response is not an object");
535
396
  }
536
397
  const obj = raw;