@wrenai/genbi 0.0.4 → 0.0.5

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 (230) hide show
  1. package/README.md +64 -2
  2. package/dist/assets/{echarts-LTFC-ysd.js → echarts-DYM4hjvk.js} +2 -2
  3. package/dist/assets/index-A_Wrq7Ey.css +1 -0
  4. package/dist/assets/{index-Bo--QMUN.js → index-DsH129tV.js} +18 -17
  5. package/dist/index.html +2 -2
  6. package/dist-server/harness/bundle/schema.d.ts +38 -0
  7. package/dist-server/harness/bundle/schema.js +18 -10
  8. package/dist-server/harness/bundle/version.js +1 -1
  9. package/dist-server/harness/cli-args.d.ts +10 -2
  10. package/dist-server/harness/cli-args.js +24 -8
  11. package/dist-server/harness/cli.js +75 -9
  12. package/dist-server/harness/compile/cache.js +4 -4
  13. package/dist-server/harness/compile/compose-profile.d.ts +51 -14
  14. package/dist-server/harness/compile/compose-profile.js +85 -19
  15. package/dist-server/harness/compile/context-loader.d.ts +56 -0
  16. package/dist-server/harness/compile/context-loader.js +237 -0
  17. package/dist-server/harness/compile/errors.d.ts +19 -0
  18. package/dist-server/harness/compile/errors.js +35 -0
  19. package/dist-server/harness/compile/index.d.ts +4 -3
  20. package/dist-server/harness/compile/index.js +4 -3
  21. package/dist-server/harness/compile/pipeline.d.ts +15 -7
  22. package/dist-server/harness/compile/pipeline.js +62 -14
  23. package/dist-server/harness/compile/resolve-binary.d.ts +2 -2
  24. package/dist-server/harness/compile/resolve-binary.js +3 -3
  25. package/dist-server/harness/compile/types.d.ts +29 -0
  26. package/dist-server/harness/compile/warble-identity.d.ts +11 -0
  27. package/dist-server/harness/compile/warble-identity.js +13 -0
  28. package/dist-server/harness/components/ai-step.d.ts +4 -0
  29. package/dist-server/harness/components/ai-step.js +40 -0
  30. package/dist-server/harness/components/broker.d.ts +60 -0
  31. package/dist-server/harness/components/broker.js +117 -0
  32. package/dist-server/harness/components/capability-card.d.ts +98 -0
  33. package/dist-server/harness/components/capability-card.js +169 -0
  34. package/dist-server/harness/components/context.d.ts +2 -0
  35. package/dist-server/harness/components/context.js +30 -0
  36. package/dist-server/harness/components/display.d.ts +6 -0
  37. package/dist-server/harness/components/display.js +29 -0
  38. package/dist-server/harness/components/egress-judge.d.ts +12 -0
  39. package/dist-server/harness/components/egress-judge.js +27 -0
  40. package/dist-server/harness/components/egress.d.ts +142 -0
  41. package/dist-server/harness/components/egress.js +469 -0
  42. package/dist-server/harness/components/in-process.d.ts +7 -0
  43. package/dist-server/harness/components/in-process.js +245 -0
  44. package/dist-server/harness/components/normalize.d.ts +35 -0
  45. package/dist-server/harness/components/normalize.js +245 -0
  46. package/dist-server/harness/components/plan.d.ts +31 -0
  47. package/dist-server/harness/components/plan.js +184 -0
  48. package/dist-server/harness/components/report.d.ts +207 -0
  49. package/dist-server/harness/components/report.js +309 -0
  50. package/dist-server/harness/components/runner.d.ts +193 -0
  51. package/dist-server/harness/components/runner.js +492 -0
  52. package/dist-server/harness/components/semantic-guards.d.ts +20 -0
  53. package/dist-server/harness/components/semantic-guards.js +42 -0
  54. package/dist-server/harness/components/terminal-json.d.ts +18 -0
  55. package/dist-server/harness/components/terminal-json.js +82 -0
  56. package/dist-server/harness/components/usage.d.ts +14 -0
  57. package/dist-server/harness/components/usage.js +19 -0
  58. package/dist-server/harness/components/wren-access.d.ts +29 -0
  59. package/dist-server/harness/components/wren-access.js +233 -0
  60. package/dist-server/harness/components/zone-dry-run.d.ts +43 -0
  61. package/dist-server/harness/components/zone-dry-run.js +107 -0
  62. package/dist-server/harness/components/zone-gate.d.ts +76 -0
  63. package/dist-server/harness/components/zone-gate.js +144 -0
  64. package/dist-server/harness/events/index.d.ts +1 -1
  65. package/dist-server/harness/events/types.d.ts +52 -0
  66. package/dist-server/harness/index.d.ts +9 -2
  67. package/dist-server/harness/index.js +5 -1
  68. package/dist-server/harness/loop/executor.d.ts +1 -0
  69. package/dist-server/harness/loop/executor.js +37 -15
  70. package/dist-server/harness/npm-package-resolve.d.ts +2 -0
  71. package/dist-server/harness/npm-package-resolve.js +16 -4
  72. package/dist-server/harness/providers/adapters/openai-compatible.d.ts +13 -0
  73. package/dist-server/harness/providers/adapters/openai-compatible.js +17 -0
  74. package/dist-server/harness/providers/adapters/openai.d.ts +7 -0
  75. package/dist-server/harness/providers/adapters/openai.js +6 -0
  76. package/dist-server/harness/providers/adapters/pi-ai.d.ts +48 -0
  77. package/dist-server/harness/providers/adapters/pi-ai.js +518 -0
  78. package/dist-server/harness/providers/binding.d.ts +45 -4
  79. package/dist-server/harness/providers/binding.js +44 -8
  80. package/dist-server/harness/providers/index.d.ts +9 -3
  81. package/dist-server/harness/providers/index.js +10 -3
  82. package/dist-server/harness/providers/zone.d.ts +96 -0
  83. package/dist-server/harness/providers/zone.js +112 -0
  84. package/dist-server/harness/render/answering-query.d.ts +29 -0
  85. package/dist-server/harness/render/answering-query.js +62 -0
  86. package/dist-server/harness/render/envelope.d.ts +6 -0
  87. package/dist-server/harness/render/envelope.js +10 -3
  88. package/dist-server/harness/route/adapter-spec.d.ts +8 -9
  89. package/dist-server/harness/route/adapter-spec.js +13 -14
  90. package/dist-server/harness/route/codex-ask-events.js +2 -1
  91. package/dist-server/harness/route/codex-ask.d.ts +4 -0
  92. package/dist-server/harness/route/codex-ask.js +69 -30
  93. package/dist-server/harness/route/codex-host-query.d.ts +52 -0
  94. package/dist-server/harness/route/codex-host-query.js +191 -0
  95. package/dist-server/harness/route/codex-local-manifest.d.ts +9 -1
  96. package/dist-server/harness/route/codex-local-manifest.js +96 -15
  97. package/dist-server/harness/route/codex-wren-proxy.d.ts +33 -0
  98. package/dist-server/harness/route/codex-wren-proxy.js +227 -0
  99. package/dist-server/harness/route/describe.js +45 -6
  100. package/dist-server/harness/route/in-process.d.ts +22 -1
  101. package/dist-server/harness/route/in-process.js +54 -4
  102. package/dist-server/harness/route/route.js +5 -2
  103. package/dist-server/harness/route/types.d.ts +30 -1
  104. package/dist-server/harness/session/run.js +30 -10
  105. package/dist-server/harness/setup/runner.d.ts +2 -0
  106. package/dist-server/harness/setup/runner.js +38 -17
  107. package/dist-server/harness/setup/yaml-scalar.d.ts +23 -0
  108. package/dist-server/harness/setup/yaml-scalar.js +62 -0
  109. package/dist-server/server/adopt.js +1 -17
  110. package/dist-server/server/app.d.ts +1 -0
  111. package/dist-server/server/app.js +163 -14
  112. package/dist-server/server/auth-choice.d.ts +13 -1
  113. package/dist-server/server/auth-choice.js +66 -1
  114. package/dist-server/server/bin.js +177 -25
  115. package/dist-server/server/codex-local-grounding.d.ts +18 -0
  116. package/dist-server/server/codex-local-grounding.js +92 -0
  117. package/dist-server/server/codex-native-session.d.ts +25 -0
  118. package/dist-server/server/codex-native-session.js +1 -0
  119. package/dist-server/server/codex-runtime-composition.d.ts +34 -0
  120. package/dist-server/server/codex-runtime-composition.js +250 -0
  121. package/dist-server/server/codex-structured-ask.d.ts +23 -0
  122. package/dist-server/server/codex-structured-ask.js +102 -0
  123. package/dist-server/server/conn-config.js +4 -16
  124. package/dist-server/server/conversation-socket.d.ts +16 -0
  125. package/dist-server/server/conversation-socket.js +65 -0
  126. package/dist-server/server/db.d.ts +46 -0
  127. package/dist-server/server/db.js +88 -9
  128. package/dist-server/server/env-detect.d.ts +6 -0
  129. package/dist-server/server/env-detect.js +7 -0
  130. package/dist-server/server/fold.d.ts +22 -4
  131. package/dist-server/server/fold.js +37 -5
  132. package/dist-server/server/harness.d.ts +2 -2
  133. package/dist-server/server/harness.js +33 -7
  134. package/dist-server/server/interactive-terminal.d.ts +10 -6
  135. package/dist-server/server/interactive-terminal.js +33 -8
  136. package/dist-server/server/managed-wren-runtime.d.ts +116 -0
  137. package/dist-server/server/managed-wren-runtime.js +670 -0
  138. package/dist-server/server/native-artifacts.d.ts +23 -7
  139. package/dist-server/server/native-artifacts.js +43 -12
  140. package/dist-server/server/native-component-context.d.ts +19 -0
  141. package/dist-server/server/native-component-context.js +76 -0
  142. package/dist-server/server/native-component-preparation.d.ts +20 -0
  143. package/dist-server/server/native-component-preparation.js +129 -0
  144. package/dist-server/server/native-component-runtime.d.ts +36 -0
  145. package/dist-server/server/native-component-runtime.js +92 -0
  146. package/dist-server/server/native-components.d.ts +84 -0
  147. package/dist-server/server/native-components.js +160 -0
  148. package/dist-server/server/native-dispatch-process.d.ts +8 -0
  149. package/dist-server/server/native-dispatch-process.js +65 -0
  150. package/dist-server/server/native-runtime-spec.d.ts +89 -0
  151. package/dist-server/server/native-runtime-spec.js +169 -0
  152. package/dist-server/server/native-sessions.d.ts +172 -19
  153. package/dist-server/server/native-sessions.js +839 -244
  154. package/dist-server/server/native-shutdown.d.ts +6 -0
  155. package/dist-server/server/native-shutdown.js +28 -0
  156. package/dist-server/server/native-wren-home.d.ts +18 -0
  157. package/dist-server/server/native-wren-home.js +262 -0
  158. package/dist-server/server/node-pty-host.d.ts +2 -1
  159. package/dist-server/server/node-pty-host.js +3 -2
  160. package/dist-server/server/profile-registry.d.ts +139 -0
  161. package/dist-server/server/profile-registry.js +620 -0
  162. package/dist-server/server/runtime-binding.d.ts +4 -1
  163. package/dist-server/server/runtime-binding.js +50 -2
  164. package/dist-server/server/runtime-host/claude-component-step.d.ts +25 -0
  165. package/dist-server/server/runtime-host/claude-component-step.js +186 -0
  166. package/dist-server/server/runtime-host/codex-app-server.d.ts +75 -0
  167. package/dist-server/server/runtime-host/codex-app-server.js +239 -0
  168. package/dist-server/server/runtime-host/codex-compatibility.d.ts +110 -0
  169. package/dist-server/server/runtime-host/codex-compatibility.js +162 -0
  170. package/dist-server/server/runtime-host/codex-component-step.d.ts +19 -0
  171. package/dist-server/server/runtime-host/codex-component-step.js +308 -0
  172. package/dist-server/server/runtime-host/codex-conversation.d.ts +109 -0
  173. package/dist-server/server/runtime-host/codex-conversation.js +295 -0
  174. package/dist-server/server/runtime-host/codex-direct-tools.d.ts +23 -0
  175. package/dist-server/server/runtime-host/codex-direct-tools.js +59 -0
  176. package/dist-server/server/runtime-host/codex-events.d.ts +647 -0
  177. package/dist-server/server/runtime-host/codex-events.js +66 -0
  178. package/dist-server/server/runtime-host/codex-policy.d.ts +16 -0
  179. package/dist-server/server/runtime-host/codex-policy.js +151 -0
  180. package/dist-server/server/runtime-host/codex-process.d.ts +8 -0
  181. package/dist-server/server/runtime-host/codex-process.js +96 -0
  182. package/dist-server/server/runtime-host/codex-rpc.d.ts +47 -0
  183. package/dist-server/server/runtime-host/codex-rpc.js +182 -0
  184. package/dist-server/server/runtime-host/codex-session.d.ts +74 -0
  185. package/dist-server/server/runtime-host/codex-session.js +492 -0
  186. package/dist-server/server/runtime-host/codex-terminal-protocol.d.ts +35 -0
  187. package/dist-server/server/runtime-host/codex-terminal-protocol.js +243 -0
  188. package/dist-server/server/runtime-host/codex-terminal.d.ts +16 -0
  189. package/dist-server/server/runtime-host/codex-terminal.js +120 -0
  190. package/dist-server/server/runtime-host/index.d.ts +5 -0
  191. package/dist-server/server/runtime-host/index.js +3 -0
  192. package/dist-server/server/runtime-host/local.d.ts +29 -0
  193. package/dist-server/server/runtime-host/local.js +90 -0
  194. package/dist-server/server/runtime-host/policy.d.ts +8 -0
  195. package/dist-server/server/runtime-host/policy.js +78 -0
  196. package/dist-server/server/runtime-host/types.d.ts +52 -0
  197. package/dist-server/server/runtime-host/types.js +62 -0
  198. package/dist-server/server/structured-query-evidence.d.ts +7 -0
  199. package/dist-server/server/structured-query-evidence.js +47 -0
  200. package/dist-server/server/structured-runtime.d.ts +52 -0
  201. package/dist-server/server/structured-runtime.js +130 -0
  202. package/dist-server/server/structured-scope.d.ts +4 -0
  203. package/dist-server/server/structured-scope.js +12 -0
  204. package/dist-server/server/turn.d.ts +6 -1
  205. package/dist-server/server/turn.js +82 -25
  206. package/dist-server/server/wire-types.d.ts +67 -3
  207. package/dist-server/server/wren-profiles.js +1 -1
  208. package/managed-wren/README.md +60 -0
  209. package/managed-wren/dependency-wheels.json +282 -0
  210. package/managed-wren/manifest.json +373 -0
  211. package/managed-wren/release-inputs.json +45 -0
  212. package/managed-wren/runtime-tree.cjs +115 -0
  213. package/managed-wren/source-inputs.json +40 -0
  214. package/package.json +15 -6
  215. package/profiles/genbi-default/context/binding.yml +17 -7
  216. package/profiles/genbi-default/context/context.json +146 -0
  217. package/profiles/genbi-default/ir.golden.json +39 -43
  218. package/profiles/genbi-default/profile.yml +31 -0
  219. package/profiles/genbi-enrich-context/context/binding.yml +14 -3
  220. package/profiles/genbi-enrich-context/context/context.json +146 -0
  221. package/profiles/genbi-enrich-context/ir.golden.json +4 -5
  222. package/profiles/genbi-monitor/context/binding.yml +13 -5
  223. package/profiles/genbi-monitor/context/context.json +146 -0
  224. package/profiles/genbi-monitor/ir.golden.json +13 -7
  225. package/profiles/genbi-report/context/binding.yml +19 -0
  226. package/profiles/genbi-report/context/context.json +223 -0
  227. package/profiles/genbi-report/ir.golden.json +328 -0
  228. package/profiles/genbi-report/profile.yml +38 -0
  229. package/profiles/genbi-setup/ir.golden.json +3 -1
  230. package/dist/assets/index-Xs4q7pNo.css +0 -1
@@ -61,6 +61,7 @@ import { buildUniformTierBinding } from "../route/tier-binding.js";
61
61
  import { createNativeToolRegistry, createSetupExecutionTool, SETUP_EXECUTION_TOOL_NAME } from "../tools/index.js";
62
62
  import { withResolvedTools } from "../tools/wiring.js";
63
63
  import { CodexSetupEventMapper } from "./codex-events.js";
64
+ import { readYamlScalarField } from "./yaml-scalar.js";
64
65
  /** The warble component the setup wizard's connect step dispatches. */
65
66
  export const CONNECT_SOURCE_AGENT_ID = "connect_source";
66
67
  /** The Warble component the setup wizard's context step dispatches (`profiles/genbi-setup/components/build_context`). */
@@ -142,6 +143,7 @@ export class DispatchedSetupRunner {
142
143
  return resolveEffectiveMaxTurns(this.options.maxTurns, agentId);
143
144
  }
144
145
  async run(runOptions) {
146
+ runOptions.signal?.throwIfAborted();
145
147
  const { authChoice } = runOptions;
146
148
  const agentId = runOptions.agentId ?? CONNECT_SOURCE_AGENT_ID;
147
149
  // Only build_context needs the raised turn budget — generating an MDL is
@@ -176,6 +178,7 @@ export class DispatchedSetupRunner {
176
178
  ...(modelsConfig !== undefined ? { modelsConfig } : {}),
177
179
  ...(maxTurns !== undefined ? { maxTurns } : {}),
178
180
  ...(runOptions.onEvent !== undefined ? { onEvent: runOptions.onEvent } : {}),
181
+ ...(runOptions.signal !== undefined ? { signal: runOptions.signal } : {}),
179
182
  ...(runOptions.resumeSessionId !== undefined ? { resumeSessionId: runOptions.resumeSessionId } : {}),
180
183
  });
181
184
  return { finalText: result.finalText, ...(result.sessionId !== undefined ? { sessionId: result.sessionId } : {}) };
@@ -248,6 +251,7 @@ export class CodexSetupRunner {
248
251
  this.options = options;
249
252
  }
250
253
  async run(runOptions) {
254
+ runOptions.signal?.throwIfAborted();
251
255
  if (runOptions.authChoice.mode !== "subscription" || runOptions.authChoice.provider !== "codex") {
252
256
  throw new Error("Codex setup runner requires a Codex subscription auth choice");
253
257
  }
@@ -286,10 +290,9 @@ export class CodexSetupRunner {
286
290
  "--server-arg=--workspace-root",
287
291
  "--server-arg",
288
292
  turnRoot,
289
- "--source-tool",
290
- SETUP_EXECUTION_TOOL_NAME,
291
- "--context-tool",
292
- SETUP_EXECUTION_TOOL_NAME,
293
+ "--transport", "exec",
294
+ "--step-tool", `${agentId === CONNECT_SOURCE_AGENT_ID ? "connect" : "build"}=${SETUP_EXECUTION_TOOL_NAME}`,
295
+ "--require-tool", agentId === CONNECT_SOURCE_AGENT_ID ? "connect" : "build",
293
296
  "--timeout",
294
297
  String(timeoutMs),
295
298
  ...(this.options.codexBin ? ["--codex-bin", this.options.codexBin] : []),
@@ -299,7 +302,7 @@ export class CodexSetupRunner {
299
302
  emitter.emit({ kind: "run.start", mode: "B", agentId });
300
303
  const mapper = new CodexSetupEventMapper(tracePath);
301
304
  try {
302
- const rawFinalText = await spawnCodexSetup(cli.command, args, mapper, emitter.emit, timeoutMs + 5_000);
305
+ const rawFinalText = await spawnCodexSetup(cli.command, args, mapper, emitter.emit, timeoutMs + 5_000, runOptions.signal);
303
306
  const finalText = unwrapCodexSetupEnvelope(rawFinalText, expectedProducedField);
304
307
  emitter.emit({ kind: "run.finish", status: "answer" });
305
308
  return { finalText };
@@ -325,8 +328,12 @@ function defaultCodexSetupMcpInvocation(tracePath) {
325
328
  function sanitizedCodexSetupEnvironment() {
326
329
  return Object.fromEntries(Object.entries(process.env).filter(([key, value]) => value !== undefined && !CODEX_BILLING_ENV_KEYS.has(key.toUpperCase())));
327
330
  }
328
- function spawnCodexSetup(command, args, mapper, emit, timeoutMs) {
331
+ function spawnCodexSetup(command, args, mapper, emit, timeoutMs, abortSignal) {
329
332
  return new Promise((resolve, reject) => {
333
+ if (abortSignal?.aborted) {
334
+ reject(new Error("Codex setup cancelled"));
335
+ return;
336
+ }
330
337
  const child = spawn(command, [...args], {
331
338
  env: sanitizedCodexSetupEnvironment(),
332
339
  stdio: ["ignore", "pipe", "pipe"],
@@ -335,6 +342,7 @@ function spawnCodexSetup(command, args, mapper, emit, timeoutMs) {
335
342
  let stderr = "";
336
343
  let settled = false;
337
344
  let timedOut = false;
345
+ let cancelled = false;
338
346
  let killTimer;
339
347
  const terminate = (signal) => {
340
348
  if (child.pid !== undefined && process.platform !== "win32") {
@@ -348,7 +356,17 @@ function spawnCodexSetup(command, args, mapper, emit, timeoutMs) {
348
356
  }
349
357
  child.kill(signal);
350
358
  };
359
+ const cancel = () => {
360
+ if (settled || cancelled || timedOut)
361
+ return;
362
+ cancelled = true;
363
+ terminate("SIGTERM");
364
+ killTimer = setTimeout(() => terminate("SIGKILL"), 1_000);
365
+ };
366
+ abortSignal?.addEventListener("abort", cancel, { once: true });
351
367
  const timer = setTimeout(() => {
368
+ if (cancelled)
369
+ return;
352
370
  timedOut = true;
353
371
  terminate("SIGTERM");
354
372
  killTimer = setTimeout(() => terminate("SIGKILL"), 1_000);
@@ -365,6 +383,7 @@ function spawnCodexSetup(command, args, mapper, emit, timeoutMs) {
365
383
  catch (error) {
366
384
  settled = true;
367
385
  clearTimeout(timer);
386
+ abortSignal?.removeEventListener("abort", cancel);
368
387
  terminate("SIGTERM");
369
388
  killTimer = setTimeout(() => terminate("SIGKILL"), 1_000);
370
389
  reject(error);
@@ -379,12 +398,14 @@ function spawnCodexSetup(command, args, mapper, emit, timeoutMs) {
379
398
  return;
380
399
  settled = true;
381
400
  clearTimeout(timer);
401
+ abortSignal?.removeEventListener("abort", cancel);
382
402
  if (killTimer)
383
403
  clearTimeout(killTimer);
384
404
  reject(new Error(`warble-codex-local failed to start: ${error.message}`));
385
405
  });
386
406
  child.on("close", (code, signal) => {
387
407
  clearTimeout(timer);
408
+ abortSignal?.removeEventListener("abort", cancel);
388
409
  lines.close();
389
410
  // Do not cancel an already-scheduled process-group SIGKILL here. The
390
411
  // direct dispatcher can exit on SIGTERM while one of its descendants
@@ -392,7 +413,10 @@ function spawnCodexSetup(command, args, mapper, emit, timeoutMs) {
392
413
  if (settled)
393
414
  return;
394
415
  settled = true;
395
- if (timedOut) {
416
+ if (cancelled) {
417
+ reject(new Error("Codex setup cancelled"));
418
+ }
419
+ else if (timedOut) {
396
420
  reject(new Error(`warble-codex-local timed out after ${timeoutMs}ms`));
397
421
  }
398
422
  else if (code !== 0) {
@@ -483,6 +507,7 @@ export class InProcessSetupRunner {
483
507
  return resolveEffectiveMaxTurns(this.options.maxTurns, agentId);
484
508
  }
485
509
  async run(runOptions) {
510
+ runOptions.signal?.throwIfAborted();
486
511
  const { authChoice } = runOptions;
487
512
  const agentId = runOptions.agentId ?? CONNECT_SOURCE_AGENT_ID;
488
513
  if (authChoice.mode === "subscription") {
@@ -548,6 +573,7 @@ export class InProcessSetupRunner {
548
573
  registry,
549
574
  tools,
550
575
  userInput: runOptions.prompt,
576
+ ...(runOptions.signal ? { signal: runOptions.signal } : {}),
551
577
  onEvent: emitter.emit,
552
578
  ...(maxSteps !== undefined ? { maxSteps } : {}),
553
579
  }));
@@ -620,7 +646,9 @@ export class InProcessSetupRunner {
620
646
  * rather than pulling in a YAML parser dependency. Matches only an unindented
621
647
  * `field:` key (so it can't be fooled by a same-named key nested under
622
648
  * another mapping), strips a single layer of surrounding quotes, and returns
623
- * `undefined` if the file is missing/unreadable or the field isn't present.
649
+ * `undefined` if the file is missing/unreadable or the field isn't present. Values
650
+ * are read as YAML scalars (trailing comments dropped; blank or comment-only is `""`),
651
+ * via `./yaml-scalar.ts`.
624
652
  */
625
653
  function readProjectYamlField(yamlPath, field) {
626
654
  let content;
@@ -630,14 +658,7 @@ function readProjectYamlField(yamlPath, field) {
630
658
  catch {
631
659
  return undefined;
632
660
  }
633
- const pattern = new RegExp(`^${field}:\\s*(.+?)\\s*$`, "m");
634
- const match = pattern.exec(content);
635
- if (!match) {
636
- return undefined;
637
- }
638
- const raw = match[1];
639
- const unquoted = /^"(.*)"$/.exec(raw) ?? /^'(.*)'$/.exec(raw);
640
- return (unquoted ? unquoted[1] : raw).trim();
661
+ return readYamlScalarField(content, field);
641
662
  }
642
663
  /** Parses the small KEY=value subset of a project .env file used by Wren profiles. */
643
664
  function parseDotEnv(text) {
@@ -809,7 +830,7 @@ export function contextLifecycleIdentityFingerprint(root, name, selectedSourceTy
809
830
  const declaredSourceType = readProjectYamlField(projectYml, "data_source");
810
831
  const normalizedSelectedSourceType = selectedSourceType.trim().toLowerCase();
811
832
  const normalizedDeclaredSourceType = declaredSourceType?.trim().toLowerCase();
812
- if (profile === undefined || normalizedDeclaredSourceType === undefined || normalizedDeclaredSourceType !== normalizedSelectedSourceType)
833
+ if (!profile || normalizedDeclaredSourceType === undefined || normalizedDeclaredSourceType !== normalizedSelectedSourceType)
813
834
  return undefined;
814
835
  const connectionTargetDigest = effectiveConnectionTargetDigest(projectDir, normalizedSelectedSourceType);
815
836
  if (connectionTargetDigest === undefined)
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Reads a top-level `field: value` scalar from a `wren_project.yml`-shaped
3
+ * document and returns the YAML *value*, not the raw line remainder.
4
+ *
5
+ * `wren context init` scaffolds lines such as
6
+ * `data_source: # not set yet — ...`, and hand-edited projects keep trailing
7
+ * comments (`data_source: duckdb # change to your datasource type`). A raw
8
+ * remainder would hand that comment to callers as part of the value. This
9
+ * stays a single-purpose reader (no YAML dependency, matching the rest of
10
+ * `server/`); it only understands what a plain scalar line can contain:
11
+ *
12
+ * - an unquoted value ends at the first ` #` (a `#` preceded by whitespace);
13
+ * - a quoted value ends at its closing quote, so `"a#b"` stays `a#b` and
14
+ * anything after the closing quote is a comment;
15
+ * - a blank value, or one that is only a comment, reads as `""` (unset).
16
+ *
17
+ * Double-quoted escapes are NOT unescaped (`"a\"b"` yields `a\"b`): only plain
18
+ * identifiers (data source types, profile names) are ever read through here.
19
+ * A leading BOM is ignored.
20
+ *
21
+ * Returns `undefined` only when the field's key line is absent.
22
+ */
23
+ export declare function readYamlScalarField(content: string, field: string): string | undefined;
@@ -0,0 +1,62 @@
1
+ /**
2
+ * Reads a top-level `field: value` scalar from a `wren_project.yml`-shaped
3
+ * document and returns the YAML *value*, not the raw line remainder.
4
+ *
5
+ * `wren context init` scaffolds lines such as
6
+ * `data_source: # not set yet — ...`, and hand-edited projects keep trailing
7
+ * comments (`data_source: duckdb # change to your datasource type`). A raw
8
+ * remainder would hand that comment to callers as part of the value. This
9
+ * stays a single-purpose reader (no YAML dependency, matching the rest of
10
+ * `server/`); it only understands what a plain scalar line can contain:
11
+ *
12
+ * - an unquoted value ends at the first ` #` (a `#` preceded by whitespace);
13
+ * - a quoted value ends at its closing quote, so `"a#b"` stays `a#b` and
14
+ * anything after the closing quote is a comment;
15
+ * - a blank value, or one that is only a comment, reads as `""` (unset).
16
+ *
17
+ * Double-quoted escapes are NOT unescaped (`"a\"b"` yields `a\"b`): only plain
18
+ * identifiers (data source types, profile names) are ever read through here.
19
+ * A leading BOM is ignored.
20
+ *
21
+ * Returns `undefined` only when the field's key line is absent.
22
+ */
23
+ export function readYamlScalarField(content, field) {
24
+ const escaped = field.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
25
+ const text = content.charCodeAt(0) === 0xfeff ? content.slice(1) : content;
26
+ const match = new RegExp(`^${escaped}:[ \\t]*(.*)$`, "m").exec(text);
27
+ if (!match)
28
+ return undefined;
29
+ return parseScalarValue(match[1].replace(/\r$/, ""));
30
+ }
31
+ function parseScalarValue(rest) {
32
+ const quote = rest[0];
33
+ if (quote === '"' || quote === "'") {
34
+ const closing = findClosingQuote(rest, quote);
35
+ // An unterminated quote isn't a valid scalar; fall back to the plain-scalar rule rather than guessing.
36
+ if (closing !== -1)
37
+ return rest.slice(1, closing);
38
+ }
39
+ return stripPlainScalarComment(rest);
40
+ }
41
+ function findClosingQuote(rest, quote) {
42
+ for (let i = 1; i < rest.length; i++) {
43
+ if (quote === '"' && rest[i] === "\\") {
44
+ i++;
45
+ continue;
46
+ }
47
+ if (rest[i] === quote) {
48
+ if (quote === "'" && rest[i + 1] === "'") {
49
+ i++;
50
+ continue;
51
+ }
52
+ return i;
53
+ }
54
+ }
55
+ return -1;
56
+ }
57
+ function stripPlainScalarComment(rest) {
58
+ if (rest.startsWith("#"))
59
+ return "";
60
+ const comment = /\s#/.exec(rest);
61
+ return (comment ? rest.slice(0, comment.index) : rest).trim();
62
+ }
@@ -52,24 +52,8 @@
52
52
  import { execFile } from "node:child_process";
53
53
  import { existsSync, readFileSync, statSync, writeFileSync } from "node:fs";
54
54
  import path from "node:path";
55
+ import { readYamlScalarField } from "../harness/setup/yaml-scalar.js";
55
56
  import { listCandidateProfiles, loadProfileStore } from "./wren-profiles.js";
56
- /**
57
- * Matches a `wren_project.yml` top-level `field: value` scalar line — same
58
- * single-purpose-regex convention as `server/app.ts`'s `ENV_KEY_LINE`. Only
59
- * ever reads a handful of known scalar fields (`profile`, `data_source`);
60
- * never a general YAML parser (matches this repo's existing convention of
61
- * avoiding a YAML dependency for single-field reads).
62
- */
63
- function readYamlScalarField(content, field) {
64
- const re = new RegExp(`^${field}:\\s*(.+?)\\s*$`, "m");
65
- const match = re.exec(content);
66
- if (!match)
67
- return undefined;
68
- const raw = match[1];
69
- // Strip one layer of matching quotes (wren_project.yml sometimes quotes scalars, e.g. `version: '1.0'`) — never unescape further, this only ever reads plain identifiers.
70
- const unquoted = /^(['"])(.*)\1$/.exec(raw);
71
- return unquoted ? unquoted[2] : raw;
72
- }
73
57
  function execWren(args, cwd) {
74
58
  return new Promise((resolve) => {
75
59
  execFile("wren", args, { cwd, maxBuffer: 32 * 1024 * 1024 }, (error, stdout, stderr) => {
@@ -1,6 +1,7 @@
1
1
  import { Hono } from "hono";
2
2
  import type { InteractiveTerminalSession } from "./interactive-terminal.js";
3
3
  import type { TurnDeps } from "./turn.js";
4
+ export declare const GATEWAY_CATALOG_UNAVAILABLE = "gateway catalog unavailable: the optional dependency @earendil-works/pi-ai is not installed or Node >= 22.19 is required";
4
5
  /** Applies only the narrow, size-bounded native-terminal WS wire contract. */
5
6
  export declare function applyInteractiveTerminalFrame(session: Pick<InteractiveTerminalSession, "write" | "resize" | "close">, raw: string): void;
6
7
  export declare function createApp(deps: TurnDeps): Hono<import("hono/types").BlankEnv, import("hono/types").BlankSchema, "/">;
@@ -10,10 +10,11 @@ import path from "node:path";
10
10
  import { Hono } from "hono";
11
11
  import { streamSSE } from "hono/streaming";
12
12
  import { upgradeWebSocket } from "@hono/node-server";
13
+ import { conversationSocket } from "./conversation-socket.js";
13
14
  import { BUILD_CONTEXT_AGENT_ID, ComplianceError, CONNECT_SOURCE_AGENT_ID, contextLifecycleIdentityFingerprint, enforceCompliance, findLockedGatedCheck, resolveArtifactContent, resolveArtifactsDir } from "../harness/index.js";
14
15
  import { adoptWithChosenProfile, verifyAdoptProject } from "./adopt.js";
15
16
  import { mountSpaFallback } from "./spa.js";
16
- import { toAuthChoiceFromRuntimeSettings } from "./auth-choice.js";
17
+ import { isRuntimeAuthMode, runtimeSettingsFromAuthChoice, toAuthChoiceFromRuntimeSettings } from "./auth-choice.js";
17
18
  import { composeSetupPrompt, zeroMdlSchemaDiscoveryInstruction } from "./compose.js";
18
19
  import { mapContextShowToOverview } from "./context-map.js";
19
20
  import { buildContextFileTree, computeKnowledgeStatus } from "./context-files.js";
@@ -25,7 +26,10 @@ import { INTERACTIVE_TARGETS, InteractiveLaunchError } from "./interactive-termi
25
26
  import { nativeSessionLaunchErrorCode, nativeSessionLaunchFailure, nativeSessionLifecycle, NATIVE_PURPOSES, NATIVE_SETUP_RECOVERY_MCP_TOOL_NAME, NativeSetupRecoveryError } from "./native-sessions.js";
26
27
  import { NATIVE_MCP_PERSIST_ANSWER_TOOL_NAME, NATIVE_MCP_TOOL_NAME, NATIVE_PERSIST_ANSWER_CONTRACT, NATIVE_SAVE_DASHBOARD_CONTRACT, NativeArtifactError } from "./native-artifacts.js";
27
28
  import { sameNativeRuntimeBinding } from "./native-dispatch-registry.js";
28
- import { detectAdapterEnv } from "./env-detect.js";
29
+ import { ProfileRegistryError, toWarbleProfileDto } from "./profile-registry.js";
30
+ import { DEFAULT_CONVERSATION_PROFILE } from "./native-sessions.js";
31
+ import { detectAdapterEnv, GATEWAY_API_KEY_ENV } from "./env-detect.js";
32
+ import { listPiAiCatalog } from "../harness/providers/index.js";
29
33
  import { redactPublicSetupText, redactSetupText, sanitizePublicSetupWorklog } from "./fold.js";
30
34
  import { assertHarnessBundlePurpose, buildHarnessDto } from "./harness.js";
31
35
  import { requiredInProcessCredentialEnvVars, RuntimeBindingError, runtimeSettingsCorrection, validateRuntimeTierBindings } from "./runtime-binding.js";
@@ -34,6 +38,7 @@ import { ArtifactNotFoundError, effectiveRouteOptions, getAskSessionData, invali
34
38
  import { connectionVariableNames, loadSourceCatalog, supportedSourceTypes } from "./source-catalog.js";
35
39
  import { driverProvisionNote, provisionSourceDriver } from "./source-driver.js";
36
40
  import { annotateEnvFields } from "./env-field-hints.js";
41
+ export const GATEWAY_CATALOG_UNAVAILABLE = "gateway catalog unavailable: the optional dependency @earendil-works/pi-ai is not installed or Node >= 22.19 is required";
37
42
  /** Only explicit persisted settings can supersede the boot route or require repair. */
38
43
  function persistedRuntimeCorrection(deps) {
39
44
  return deps.store.hasExplicitRuntimeSettings() ? runtimeSettingsCorrection(deps.store.getRuntimeSettings()) : undefined;
@@ -1009,11 +1014,29 @@ export function createApp(deps) {
1009
1014
  // remains structured conversation storage and is never overloaded with PTYs.
1010
1015
  app.post("/api/native-sessions", async (c) => {
1011
1016
  const body = await c.req.json().catch(() => ({}));
1017
+ if (!body || typeof body !== "object" || Array.isArray(body))
1018
+ return c.json({ error: "native session launch request is invalid" }, 400);
1012
1019
  const intent = body.intent === undefined ? "open_existing" : body.intent;
1013
1020
  const requiresAction = intent === "start_separate" || intent === "resume";
1014
- const allowedKeys = intent === "start_separate" ? ["purpose", "intent", "idempotencyKey"] : intent === "resume" ? ["purpose", "intent", "sessionId", "idempotencyKey"] : ["purpose", "intent", "sessionId"];
1021
+ // `profile` names a registry id for a NEW analysis session only: a start-separate, or an
1022
+ // open-or-create with no sessionId. A resume or an explicit sessionId already carries its profile.
1023
+ const allowedKeys = intent === "start_separate" ? ["purpose", "intent", "idempotencyKey", "entryVerb", "profile"] : intent === "resume" ? ["purpose", "intent", "sessionId", "idempotencyKey"] : ["purpose", "intent", "sessionId", "profile"];
1015
1024
  if (!NATIVE_PURPOSES.includes(body.purpose) || (intent !== "open_existing" && intent !== "start_separate" && intent !== "resume") || !Object.keys(body).every((key) => allowedKeys.includes(key)) || (requiresAction && (typeof body.idempotencyKey !== "string" || !/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(body.idempotencyKey))) || ((intent === "resume" && (typeof body.sessionId !== "string" || !/^native-session-[0-9a-f-]{36}$/i.test(body.sessionId))) || (intent === "open_existing" && body.sessionId !== undefined && (typeof body.sessionId !== "string" || !/^native-session-[0-9a-f-]{36}$/i.test(body.sessionId)))))
1016
1025
  return c.json({ error: "native session launch request is invalid" }, 400);
1026
+ if (body.entryVerb !== undefined && (body.purpose !== "analysis" || typeof body.entryVerb !== "string" || !["answer_query", "generate_dashboard"].includes(body.entryVerb)))
1027
+ return c.json({ error: "native session entry is invalid" }, 400);
1028
+ if (body.profile !== undefined) {
1029
+ if (typeof body.profile !== "string" || !/^[a-z0-9][a-z0-9_-]{0,63}$/.test(body.profile))
1030
+ return c.json({ error: "native session profile is invalid" }, 400);
1031
+ if (body.purpose !== "analysis")
1032
+ return c.json({ error: "native session profile is only selectable for analysis" }, 400);
1033
+ if (intent === "open_existing" && body.sessionId !== undefined)
1034
+ return c.json({ error: "native session profile cannot be changed on an existing session" }, 400);
1035
+ // A selected profile's entry form was decided at admission; only the default takes an entry verb.
1036
+ if (body.profile !== DEFAULT_CONVERSATION_PROFILE && body.entryVerb !== undefined)
1037
+ return c.json({ error: "native session entry is invalid" }, 400);
1038
+ }
1039
+ const profile = typeof body.profile === "string" ? { profile: body.profile } : {};
1017
1040
  if (!deps.nativeSessions)
1018
1041
  return c.json({ error: "native sessions are not configured" }, 503);
1019
1042
  const runtimeCorrection = persistedRuntimeCorrection(deps);
@@ -1021,12 +1044,12 @@ export function createApp(deps) {
1021
1044
  return c.json({ error: runtimeCorrection, code: "runtime_correction_required" }, 409);
1022
1045
  try {
1023
1046
  const created = intent === "start_separate"
1024
- ? await deps.nativeSessions.startSeparate({ purpose: body.purpose, idempotencyKey: body.idempotencyKey })
1047
+ ? await deps.nativeSessions.startSeparate({ purpose: body.purpose, idempotencyKey: body.idempotencyKey, ...(body.entryVerb !== undefined ? { entryVerb: body.entryVerb } : {}), ...profile })
1025
1048
  : intent === "resume"
1026
1049
  ? await deps.nativeSessions.resume({ id: body.sessionId, idempotencyKey: body.idempotencyKey })
1027
1050
  : typeof body.sessionId === "string"
1028
1051
  ? await deps.nativeSessions.openExisting({ purpose: body.purpose, id: body.sessionId })
1029
- : await deps.nativeSessions.openOrCreate({ purpose: body.purpose });
1052
+ : await deps.nativeSessions.openOrCreate({ purpose: body.purpose, ...profile });
1030
1053
  return c.json({ session: { ...created.row, lifecycle: nativeSessionLifecycle(created.row, nativeResumeAvailability(created.row)) }, ...(created.capability ? { capability: created.capability } : {}), ...(created.recoveryCapability ? { recoveryCapability: created.recoveryCapability } : {}) }, 201);
1031
1054
  }
1032
1055
  catch (error) {
@@ -1076,8 +1099,16 @@ export function createApp(deps) {
1076
1099
  const body = await c.req.json().catch(() => ({}));
1077
1100
  if (!deps.nativeSessions || typeof body.capability !== "string")
1078
1101
  return c.json({ error: "native session unavailable" }, 404);
1079
- return deps.nativeSessions.stop(c.req.param("id") ?? "", body.capability) ? c.body(null, 204) : c.json({ error: "native session unavailable" }, 404);
1102
+ try {
1103
+ return await deps.nativeSessions.stopAndWait(c.req.param("id") ?? "", body.capability) ? c.body(null, 204) : c.json({ error: "native session unavailable" }, 404);
1104
+ }
1105
+ catch {
1106
+ return c.json({ error: "Native session cleanup could not be confirmed." }, 409);
1107
+ }
1080
1108
  });
1109
+ app.get("/api/structured-runtime/readiness", async (c) => deps.structuredRuntime
1110
+ ? c.json(await deps.structuredRuntime.readiness()) : c.json({ error: "Structured runtime is not configured." }, 503));
1111
+ app.get("/api/native-sessions/:id/conversation", upgradeWebSocket((c) => conversationSocket(deps.nativeSessions, c.req.param("id") ?? "", c.req.query("cap") ?? "", c.req.query("after") ?? "0")));
1081
1112
  app.get("/api/native-sessions/:id/attach", upgradeWebSocket((c) => {
1082
1113
  const id = c.req.param("id") ?? "";
1083
1114
  const capability = c.req.query("cap") ?? "";
@@ -1165,7 +1196,7 @@ export function createApp(deps) {
1165
1196
  };
1166
1197
  const artifactTool = {
1167
1198
  name: NATIVE_MCP_TOOL_NAME,
1168
- description: "Save a verified dashboard to Artifacts. Use a prior persist_answer reference, or select this session's latest retained answer when the opaque reference is no longer available, without recomputing. The existing payload form remains available for compatibility.",
1199
+ description: "Save a dashboard to Artifacts. Use a prior persist_answer reference, or select this session's latest retained answer when the opaque reference is no longer available, without recomputing. The host marks the artifact verified only when the referenced answer is one it grounded itself; a dashboard built from a payload you write is stored as unverified. The existing payload form remains available for compatibility.",
1169
1200
  inputSchema: NATIVE_SAVE_DASHBOARD_CONTRACT.inputSchema,
1170
1201
  };
1171
1202
  const persistAnswerTool = {
@@ -1196,11 +1227,15 @@ export function createApp(deps) {
1196
1227
  if (request.method === "tools/list")
1197
1228
  return isNotification ? invalidRequest() : c.json({
1198
1229
  jsonrpc: "2.0", id,
1199
- result: { tools: nativeToolsForPurpose(nativeSession.purpose) },
1230
+ result: { tools: [...nativeToolsForPurpose(nativeSession.purpose), ...(deps.nativeSessions?.componentTools(nativeSession.id) ?? [])] },
1200
1231
  });
1201
1232
  if (request.method !== "tools/call" || isNotification || !isRecord(request.params) || !Object.hasOwn(request.params, "arguments"))
1202
1233
  return methodNotFound();
1203
1234
  try {
1235
+ if (typeof request.params.name === "string" && request.params.name.startsWith("warble_run_") && deps.nativeSessions && id !== null) {
1236
+ const result = await deps.nativeSessions.callComponent(nativeSession.id, request.params.name, request.params.arguments, id, c.req.raw.signal);
1237
+ return c.json({ jsonrpc: "2.0", id, result: { isError: result.status !== "ok", content: [{ type: "text", text: JSON.stringify(result) }], structuredContent: result } });
1238
+ }
1204
1239
  if (nativeSession.purpose === "setup" && request.params.name === NATIVE_SETUP_RECOVERY_MCP_TOOL_NAME && deps.nativeSessions) {
1205
1240
  const recovery = deps.nativeSessions.reportSetupRecovery(nativeSession.id, request.params.arguments, isRecord(request.params.arguments) && request.params.arguments.state === "reported_complete" && validateNativeSetupCompletion(deps, request.params.arguments.phase === "context" ? "context" : "connect"));
1206
1241
  return c.json({ jsonrpc: "2.0", id, result: { content: [{ type: "text", text: JSON.stringify({ accepted: true, version: recovery.version }) }], structuredContent: { accepted: true, version: recovery.version } } });
@@ -1582,6 +1617,61 @@ export function createApp(deps) {
1582
1617
  }
1583
1618
  });
1584
1619
  // ---------------------------------------------------------------------
1620
+ // Warble profile registry
1621
+ // ---------------------------------------------------------------------
1622
+ //
1623
+ // Conversation profiles are selected from a server-held registry by id. The
1624
+ // registry owns the directory, runs admission and resolves the launch tuple;
1625
+ // a browser never submits a path for a session to run inside. Adding a
1626
+ // profile takes a directory path on this machine, the same shape the adopt
1627
+ // flow accepts for a project, and the registry copies and checks it.
1628
+ const profileRegistryFailure = (c, error) => {
1629
+ if (error instanceof ProfileRegistryError)
1630
+ return c.json({ error: error.message }, error.status);
1631
+ return c.json({ error: error instanceof Error ? error.message : String(error) }, 500);
1632
+ };
1633
+ app.get("/api/profiles", async (c) => {
1634
+ if (!deps.profileRegistry)
1635
+ return c.json({ error: "the profile registry is not configured on this BFF instance" }, 503);
1636
+ try {
1637
+ const rows = await deps.profileRegistry.list();
1638
+ const response = { profiles: rows.map(toWarbleProfileDto) };
1639
+ return c.json(response);
1640
+ }
1641
+ catch (error) {
1642
+ return profileRegistryFailure(c, error);
1643
+ }
1644
+ });
1645
+ app.post("/api/profiles", async (c) => {
1646
+ if (!deps.profileRegistry)
1647
+ return c.json({ error: "the profile registry is not configured on this BFF instance" }, 503);
1648
+ const body = await c.req.json().catch(() => undefined);
1649
+ if (typeof body !== "object" || body === null || Array.isArray(body) || !Object.keys(body).every((key) => key === "sourcePath")) {
1650
+ return c.json({ error: "profile registration request is invalid: expected { sourcePath }" }, 400);
1651
+ }
1652
+ try {
1653
+ const row = await deps.profileRegistry.add(body.sourcePath);
1654
+ return c.json({ profile: toWarbleProfileDto(row) }, 201);
1655
+ }
1656
+ catch (error) {
1657
+ return profileRegistryFailure(c, error);
1658
+ }
1659
+ });
1660
+ app.delete("/api/profiles/:id", async (c) => {
1661
+ if (!deps.profileRegistry)
1662
+ return c.json({ error: "the profile registry is not configured on this BFF instance" }, 503);
1663
+ const id = c.req.param("id");
1664
+ if (!/^[a-z0-9][a-z0-9_-]{0,63}$/.test(id))
1665
+ return c.json({ error: "profile id is invalid" }, 400);
1666
+ try {
1667
+ await deps.profileRegistry.remove(id);
1668
+ return c.body(null, 204);
1669
+ }
1670
+ catch (error) {
1671
+ return profileRegistryFailure(c, error);
1672
+ }
1673
+ });
1674
+ // ---------------------------------------------------------------------
1585
1675
  // Harness introspection
1586
1676
  // ---------------------------------------------------------------------
1587
1677
  app.get("/api/harness", async (c) => {
@@ -1590,16 +1680,44 @@ export function createApp(deps) {
1590
1680
  return c.json({ error: "harness purpose must be exactly one of: setup, analysis, context_enrichment" }, 400);
1591
1681
  }
1592
1682
  const purpose = purposes[0];
1683
+ // An optional registry id selects which conversation profile the analysis purpose describes.
1684
+ // The server resolves it; the browser never names a directory.
1685
+ const profiles = c.req.queries("profile") ?? [];
1686
+ if (profiles.length > 1)
1687
+ return c.json({ error: "harness profile must be given at most once" }, 400);
1688
+ const requestedProfile = profiles[0];
1689
+ if (requestedProfile !== undefined) {
1690
+ if (!/^[a-z0-9][a-z0-9_-]{0,63}$/.test(requestedProfile))
1691
+ return c.json({ error: "harness profile is invalid" }, 400);
1692
+ if (purpose !== "analysis")
1693
+ return c.json({ error: "harness profile is only selectable for analysis" }, 400);
1694
+ }
1695
+ const selectedProfile = requestedProfile !== undefined && requestedProfile !== DEFAULT_CONVERSATION_PROFILE ? requestedProfile : undefined;
1593
1696
  // Setup describes the raw bootstrap profile, before a project exists. The
1594
1697
  // other two profiles consume a bound project's context and stay gated.
1595
1698
  if (purpose !== "setup" && !isProjectBound(deps))
1596
1699
  return c.json({ error: PROJECT_NOT_BOUND_MESSAGE }, 409);
1597
1700
  if (!deps.describeHarnessBundle)
1598
1701
  return c.json({ error: "harness introspection is not configured" }, 500);
1702
+ let selectedSource;
1703
+ if (selectedProfile !== undefined) {
1704
+ if (!deps.profileRegistry)
1705
+ return c.json({ error: "the profile registry is not configured on this BFF instance" }, 503);
1706
+ try {
1707
+ selectedSource = (await deps.profileRegistry.resolveConversationProfileSource(selectedProfile)).sourceDir;
1708
+ }
1709
+ catch (error) {
1710
+ return profileRegistryFailure(c, error);
1711
+ }
1712
+ }
1599
1713
  try {
1600
1714
  const routeOptions = effectiveRouteOptions(deps, { allowUnbound: purpose === "setup" });
1601
- const bundle = await deps.describeHarnessBundle(purpose, routeOptions);
1602
- assertHarnessBundlePurpose(bundle, purpose);
1715
+ // Keep the two-argument call for the fixed profiles so existing describers and their tests see
1716
+ // exactly the shape they always did; only a selection adds the third argument.
1717
+ const bundle = selectedSource !== undefined
1718
+ ? await deps.describeHarnessBundle(purpose, routeOptions, selectedSource)
1719
+ : await deps.describeHarnessBundle(purpose, routeOptions);
1720
+ assertHarnessBundlePurpose(bundle, purpose, selectedProfile);
1603
1721
  const setupRuntimeCorrection = purpose === "setup" ? persistedRuntimeCorrection(deps) : undefined;
1604
1722
  const setupReadiness = purpose === "setup"
1605
1723
  ? setupRuntimeCorrection !== undefined
@@ -1616,7 +1734,7 @@ export function createApp(deps) {
1616
1734
  const nativeReadiness = purpose === "setup"
1617
1735
  ? await deps.nativeSessions?.readiness().catch(() => undefined)
1618
1736
  : await deps.nativeSessions?.readiness();
1619
- return c.json(buildHarnessDto(bundle, deps.store, routeOptions, purpose, nativeReadiness, setupReadiness));
1737
+ return c.json(buildHarnessDto(bundle, deps.store, routeOptions, purpose, nativeReadiness, setupReadiness, selectedProfile));
1620
1738
  }
1621
1739
  catch (err) {
1622
1740
  const message = err instanceof Error ? err.message : String(err);
@@ -1626,7 +1744,12 @@ export function createApp(deps) {
1626
1744
  // ---------------------------------------------------------------------
1627
1745
  // Config / setup pages
1628
1746
  // ---------------------------------------------------------------------
1629
- app.get("/api/config/runtime", (c) => c.json(deps.store.getRuntimeSettings()));
1747
+ // Until the user saves, the effective runtime is the boot auth env, so report that rather than
1748
+ // the seeded display default. Nothing here is persisted and no credential is read.
1749
+ app.get("/api/config/runtime", (c) => {
1750
+ const stored = deps.store.getRuntimeSettings();
1751
+ return c.json(deps.store.hasExplicitRuntimeSettings() ? stored : runtimeSettingsFromAuthChoice(deps.baseRouteOptions.authChoice, stored));
1752
+ });
1630
1753
  app.get("/api/config/runtime/readiness", (c) => {
1631
1754
  const correction = persistedRuntimeCorrection(deps);
1632
1755
  return c.json(correction ? { valid: false, correction } : { valid: true });
@@ -1673,9 +1796,31 @@ export function createApp(deps) {
1673
1796
  : { version: 1, status: "unavailable", provider, code: "runtime_unavailable", retryable: true };
1674
1797
  return c.json(result);
1675
1798
  });
1799
+ /** pi-ai's built-in catalog (offline, no credentials) for the gateway option's suggestions. */
1800
+ app.get("/api/harness/gateway-models", async (c) => {
1801
+ const provider = c.req.query("provider")?.trim() || undefined;
1802
+ try {
1803
+ const catalog = await listPiAiCatalog(provider);
1804
+ return c.json({
1805
+ status: "ready",
1806
+ providers: catalog.providers,
1807
+ models: catalog.models.map((model) => ({ model: model.id, displayName: model.name })),
1808
+ });
1809
+ }
1810
+ catch {
1811
+ // A fixed reason: the load error is Node's module-resolution text, which can name local paths.
1812
+ return c.json({ status: "unavailable", reason: GATEWAY_CATALOG_UNAVAILABLE });
1813
+ }
1814
+ });
1676
1815
  app.put("/api/config/runtime", async (c) => {
1677
1816
  const patch = await c.req.json().catch(() => ({}));
1678
- const updated = { ...deps.store.getRuntimeSettings(), ...patch };
1817
+ // Seed the merge from what GET reports, so a first save patches the effective (boot) runtime.
1818
+ const stored = deps.store.getRuntimeSettings();
1819
+ const effective = deps.store.hasExplicitRuntimeSettings() ? stored : runtimeSettingsFromAuthChoice(deps.baseRouteOptions.authChoice, stored);
1820
+ const updated = { ...effective, ...patch };
1821
+ if (!isRuntimeAuthMode(updated.authMode)) {
1822
+ return c.json({ error: "authMode must be one of subscription, byo, local, gateway" }, 400);
1823
+ }
1679
1824
  // Derive the candidate live AuthChoice from the merged (not-yet-persisted)
1680
1825
  // settings and validate it BEFORE any mutation — a rejected save must leave
1681
1826
  // both the persisted settings and the live auth binding untouched.
@@ -1712,7 +1857,9 @@ export function createApp(deps) {
1712
1857
  if (candidateAuthChoice.mode !== "subscription") {
1713
1858
  const envStatus = detectAdapterEnv();
1714
1859
  for (const envVar of requiredInProcessCredentialEnvVars(updated)) {
1715
- const present = envVar === "OPENAI_API_KEY" ? envStatus.openaiCompatible : envStatus.anthropic;
1860
+ const present = envVar === "OPENAI_API_KEY" ? envStatus.openaiCompatible
1861
+ : envVar === GATEWAY_API_KEY_ENV ? envStatus.gateway === true
1862
+ : envStatus.anthropic;
1716
1863
  if (!present)
1717
1864
  return c.json({ error: `${envVar} is not set on the server — required by a configured tier adapter` }, 400);
1718
1865
  }
@@ -1728,6 +1875,7 @@ export function createApp(deps) {
1728
1875
  }
1729
1876
  const revokedNativeSessionIds = deps.store.setRuntimeSettingsAndRevokeIncompatibleNativeSessions(updated);
1730
1877
  deps.nativeSessions?.revokeRuntimeCapabilities(revokedNativeSessionIds);
1878
+ deps.structuredRuntime?.revoke();
1731
1879
  deps.revokeInteractiveTerminals?.();
1732
1880
  deps.setAuthChoice?.(candidateAuthChoice);
1733
1881
  // describeBundle branches its target on authChoice.mode, so a cached
@@ -1909,6 +2057,7 @@ export function createApp(deps) {
1909
2057
  }
1910
2058
  const revokedNativeSessionIds = deps.store.resetSetup();
1911
2059
  deps.nativeSessions?.revokeRuntimeCapabilities(revokedNativeSessionIds);
2060
+ deps.structuredRuntime?.revoke();
1912
2061
  deps.revokeInteractiveTerminals?.();
1913
2062
  deps.unbindProject?.();
1914
2063
  // Reset also revokes the explicit runtime override. Restore the boot auth
@@ -3,14 +3,26 @@ import type { RuntimeSettings } from "./wire-types.js";
3
3
  /**
4
4
  * Maps the wizard's persisted `RuntimeSettings` to the harness-level `AuthChoice` that should
5
5
  * actually be dispatched with. `RuntimeSettings.authMode` is the wire/persisted vocabulary
6
- * (`"subscription" | "byo" | "local"`); `AuthChoice.mode` is the harness vocabulary
6
+ * (`"subscription" | "byo" | "local" | "gateway"`); `AuthChoice.mode` is the harness vocabulary
7
7
  * (`"subscription" | "api-key" | "local" | "gateway"`) — `"byo"` maps to `"api-key"`.
8
8
  *
9
9
  * For the `openai-compatible` adapter, the BFF reads `OPENAI_API_KEY` from its own process env
10
10
  * and injects it explicitly as `config.apiKey` — that adapter has no env self-read of its own.
11
11
  * For `anthropic`, `config.apiKey` is deliberately omitted so the adapter self-reads
12
12
  * `ANTHROPIC_API_KEY` from the process env itself (see `harness/providers/adapters/anthropic.ts`).
13
+ * For `gateway` (the `pi-ai` adapter), the BFF reads `GENBI_GATEWAY_API_KEY` and injects it as
14
+ * `config.apiKey` the same way — pi-ai never reads env vars or credential files on its own.
13
15
  * The key value is never read into `RuntimeSettings`, SQLite, or any log — only referenced here,
14
16
  * transiently, to build the in-memory `AuthChoice` passed straight to the adapter factory.
15
17
  */
16
18
  export declare function toAuthChoiceFromRuntimeSettings(settings: RuntimeSettings): AuthChoice;
19
+ /** True for the wire `authMode` values `toAuthChoiceFromRuntimeSettings` can map. */
20
+ export declare function isRuntimeAuthMode(value: unknown): value is RuntimeSettings["authMode"];
21
+ /**
22
+ * The inverse of `toAuthChoiceFromRuntimeSettings`, for reporting the auth the BFF booted with
23
+ * (`WREN_HARNESS_MODE` / `MODEL` / `ENDPOINT` / flags) before the user has saved a runtime.
24
+ * Overlays only the auth-selection fields onto `base`; tier rows, deployment and hybrid stay the
25
+ * store's. Credentials never appear in the result. A boot adapter the wizard cannot represent
26
+ * leaves `base` untouched.
27
+ */
28
+ export declare function runtimeSettingsFromAuthChoice(choice: AuthChoice, base: RuntimeSettings): RuntimeSettings;