@ggui-ai/mcp-server 0.2.0-alpha.3 → 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 +25 -25
  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 -248
  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 +204 -183
  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,77 +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 { negotiate } from "@ggui-ai/negotiator";
55
- 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";
56
40
  import { selectAdapter } from "@ggui-ai/ui-gen/providers";
57
- import { createHash, randomUUID } from "node:crypto";
58
- /**
59
- * Operational error classifier — mirrors the cloud helper. Bugs
60
- * surface; provider failures + network blips degrade to a "create"
61
- * stub so the agent isn't blocked by a transient infrastructure
62
- * issue.
63
- */
64
- function isOperationalError(err) {
65
- if (err instanceof TypeError)
66
- return false;
67
- if (err instanceof ReferenceError)
68
- return false;
69
- if (err instanceof RangeError)
70
- return false;
71
- if (err instanceof SyntaxError)
72
- return false;
73
- return true;
74
- }
75
41
  /**
76
42
  * Wrap a resolved BYOK credential pair into an `LLMCaller` the
77
43
  * negotiator can call. The adapter is chosen via `selectAdapter`
@@ -194,202 +160,70 @@ async function anthropicCallStructured(args) {
194
160
  }
195
161
  return toolUse.input;
196
162
  }
197
- const DEFAULT_GEN_LATENCY_MS = 30_000;
198
- function buildCreateFallback(draftContract, reason, _estimatedLatencyMs) {
199
- // Fallback path stamps an `origin: 'agent'` suggestion against the
200
- // agent's draft. Gen-pending; no codeHash; provisional blueprintId.
201
- const contractHash = blueprintKey(draftContract);
202
- const suggestion = {
203
- origin: "agent",
204
- rationale: reason,
205
- blueprintMeta: {
206
- blueprintId: `bp_${randomUUID()}`,
207
- contractHash,
208
- generator: "ui-gen-default-haiku-4-5",
209
- variance: {},
210
- },
211
- };
212
- return {
213
- action: "create",
214
- reason,
215
- suggestion,
216
- effectiveContract: draftContract,
217
- };
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 ?? [])];
218
176
  }
219
177
  /**
220
178
  * Build an LLM-backed `HandshakeNegotiator` for the OSS server.
221
- * Wires BYOK creds + an LLM provider adapter into
222
- * `@ggui-ai/negotiator`'s `negotiate()` pipeline. Operators get the
223
- * same negotiation shape cloud uses, with RAG gracefully degraded
224
- * (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.
225
188
  *
226
189
  * @public
227
190
  */
228
191
  export function createLlmBackedHandshakeNegotiator(deps) {
229
- const estimatedLatencyMs = deps.estimatedGenerationLatencyMs ?? DEFAULT_GEN_LATENCY_MS;
230
- return {
231
- async decide({ intent, blueprintDraft, gadgets, ctx }) {
232
- const draftContract = blueprintDraft.contract;
233
- // Handshake-time exact-key fast path. Runs BEFORE the BYOK
234
- // creds resolve + LLM synth — a cache hit needs neither the
235
- // operator's API key nor a model round-trip. When the agent's
236
- // draft canonical-key-equals a registered blueprint, return
237
- // `origin: 'cache'` with the matched blueprint's contract +
238
- // componentCode hash so the paired push.accept short-circuits
239
- // straight into commitCachedRender.
240
- //
241
- // No-match (or any throw) falls through to today's synth path
242
- // — the negotiator stays useful when the registry is cold,
243
- // when matchBlueprint hiccups, or when the deployment skipped
244
- // the cache deps entirely.
245
- if (deps.cache) {
246
- try {
247
- const matchDeps = {
248
- registry: deps.cache,
249
- ...(deps.installedBlueprints ? { installedBlueprints: deps.installedBlueprints } : {}),
250
- };
251
- const matchResult = await matchBlueprint(matchDeps, ctx.appId, {
252
- intent,
253
- contract: draftContract,
254
- });
255
- if (matchResult.strategy === "exact-key") {
256
- const matched = matchResult.blueprint;
257
- const codeHash = createHash("sha256").update(matched.componentCode).digest("hex");
258
- const suggestion = {
259
- origin: "cache",
260
- rationale: matchResult.reason,
261
- blueprintMeta: {
262
- blueprintId: matched.id,
263
- contractHash: matched.contractKey,
264
- codeHash,
265
- generator: DEFAULT_GENERATOR_SLUG,
266
- variance: {},
267
- selectedReason: matchResult.reason,
268
- },
269
- };
270
- return {
271
- action: "reuse",
272
- reason: matchResult.reason,
273
- suggestion,
274
- effectiveContract: matched.contract,
275
- };
276
- }
277
- // Other strategies (no-match, semantic) fall through to
278
- // the negotiate() path below. Semantic doesn't fire when
279
- // a contract is supplied (the fuzzy-match gate blocks it);
280
- // listing it here is structural completeness, not a
281
- // reachable branch today.
282
- }
283
- catch (err) {
284
- if (!isOperationalError(err))
285
- throw err;
286
- // Registry hiccup — log + fall through to synth so the
287
- // handshake never crashes on a transient cache backend
288
- // issue.
289
- const message = err instanceof Error ? err.message : String(err);
290
- // eslint-disable-next-line no-console -- operator-visible signal
291
- console.warn(`[llm-backed-negotiator] matchBlueprint exact-key probe failed; falling through to synth: ${message}`);
292
- }
293
- }
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) => {
294
201
  const creds = await deps.resolveLlm(ctx);
295
- if (!creds) {
296
- 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);
297
- }
298
- try {
299
- const llm = buildLlmCaller(creds.selection, creds.providerKey);
300
- const declaredAgentTools = Object.keys(draftContract.agentCapabilities?.tools ?? {});
301
- const synthPrompt = blueprintDraft.variance?.seedPrompt ?? intent;
302
- const result = await negotiate({ llm }, {
303
- agent: {
304
- prompt: synthPrompt,
305
- ...(declaredAgentTools.length > 0 ? { agentTools: declaredAgentTools } : {}),
306
- ...(gadgets !== undefined ? { gadgets } : {}),
307
- },
308
- config: {
309
- appId: ctx.appId,
310
- // Handshake runs ahead of any concrete render — no renderId
311
- // is bound yet. The negotiator only uses `renderId` to key
312
- // its optional `readRenderState` callback, which the OSS
313
- // path doesn't wire here, so a stable placeholder works.
314
- renderId: "handshake",
315
- includeSharedPool: false,
316
- },
317
- });
318
- const decision = result.decision;
319
- const contract = decision.contract;
320
- const contractHash = result.storedContractHash ?? blueprintKey(contract);
321
- const draftHash = blueprintKey(draftContract);
322
- const blueprintId = decision.blueprintId;
323
- // Origin routing:
324
- // - blueprintId present (cache hit) → origin: 'cache'
325
- // - contract == draft → origin: 'agent'
326
- // - contract != draft → origin: 'synth'
327
- let suggestion;
328
- if (blueprintId) {
329
- suggestion = {
330
- origin: "cache",
331
- rationale: decision.reasoning ?? `cache match (${blueprintId})`,
332
- blueprintMeta: {
333
- blueprintId,
334
- contractHash,
335
- generator: "ui-gen-default-haiku-4-5",
336
- variance: {},
337
- },
338
- };
339
- }
340
- else if (contractHash === draftHash) {
341
- suggestion = {
342
- origin: "agent",
343
- rationale: decision.reasoning ?? "novel-but-clean contract",
344
- blueprintMeta: {
345
- blueprintId: `bp_${randomUUID()}`,
346
- contractHash,
347
- generator: "ui-gen-default-haiku-4-5",
348
- variance: {},
349
- },
350
- };
351
- }
352
- else {
353
- suggestion = {
354
- origin: "synth",
355
- rationale: decision.reasoning ?? "synth amended contract",
356
- blueprintMeta: {
357
- blueprintId: `bp_${randomUUID()}`,
358
- contractHash,
359
- generator: "ui-gen-default-haiku-4-5",
360
- variance: {},
361
- },
362
- amendments: {
363
- contractDiff: [
364
- {
365
- op: "replace",
366
- path: "",
367
- value: contract,
368
- },
369
- ],
370
- reasoning: decision.reasoning ?? "negotiator-amended contract",
371
- },
372
- };
373
- }
374
- return {
375
- action: blueprintId ? "reuse" : "create",
376
- reason: decision.reasoning ?? "negotiated",
377
- suggestion,
378
- effectiveContract: contract,
379
- };
380
- }
381
- catch (err) {
382
- if (!isOperationalError(err))
383
- throw err;
384
- const message = err instanceof Error ? err.message : String(err);
385
- const errorClass = err instanceof Error ? err.name : "unknown";
386
- 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);
387
- }
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);
388
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),
389
223
  // LLM-driven variant selection. Reads each candidate's
390
224
  // `variance` + `validatorScore` + `isOperatorDefault` + the
391
- // generator slug, asks the LLM to pick the best fit for the
392
- // 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
393
227
  // calibrated decision. The caller (`selectVariantWithLlm`)
394
228
  // thresholds on `confidence`.
395
229
  //
@@ -422,7 +256,7 @@ export const VARIANT_SELECTION_SYSTEM_PROMPT = `You are the variant selector for
422
256
 
423
257
  Each variant carries:
424
258
  - blueprintId: stable identity (you MUST echo back exactly one of these).
425
- - 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.
426
260
  - validatorScore: optional 0-1 self-assessed quality from the advanced generator's validators. Higher is better, undefined ⇒ unknown.
427
261
  - isOperatorDefault: true ⇒ the human operator pinned this as the default. Strong signal.
428
262
  - variance.persona: free-form tag ("minimalist", "data-dense", "mobile-first"…).
@@ -479,7 +313,7 @@ const VARIANT_SELECTION_TOOL_SCHEMA = {
479
313
  export function buildVariantSelectionUserMessage(candidates, context) {
480
314
  const projectedCandidates = candidates.map((c) => ({
481
315
  blueprintId: c.blueprintId,
482
- generator: c.generator,
316
+ source: c.source,
483
317
  ...(c.validatorScore !== undefined ? { validatorScore: c.validatorScore } : {}),
484
318
  ...(c.isOperatorDefault === true ? { isOperatorDefault: true } : {}),
485
319
  variance: {
@@ -557,7 +391,7 @@ async function runVariantSelectionLlm(llm, candidates, context) {
557
391
  * `VariantSelectionResult.reason` for telemetry.
558
392
  */
559
393
  export function parseVariantSelectionResponse(raw) {
560
- if (raw === null || typeof raw !== "object") {
394
+ if (!isRecord(raw)) {
561
395
  throw new Error("variant-selection: response is not an object");
562
396
  }
563
397
  const obj = raw;