@wrenai/genbi 0.0.4 → 0.0.6

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 (234) hide show
  1. package/README.md +65 -3
  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 +43 -45
  93. package/dist-server/harness/route/codex-local-manifest.d.ts +9 -1
  94. package/dist-server/harness/route/codex-local-manifest.js +96 -15
  95. package/dist-server/harness/route/describe.js +45 -6
  96. package/dist-server/harness/route/dispatched.d.ts +20 -0
  97. package/dist-server/harness/route/dispatched.js +122 -24
  98. package/dist-server/harness/route/host-query.d.ts +93 -0
  99. package/dist-server/harness/route/host-query.js +257 -0
  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 +8 -2
  103. package/dist-server/harness/route/types.d.ts +47 -1
  104. package/dist-server/harness/route/wren-host-proxy.d.ts +33 -0
  105. package/dist-server/harness/route/wren-host-proxy.js +228 -0
  106. package/dist-server/harness/session/run.js +30 -10
  107. package/dist-server/harness/setup/runner.d.ts +17 -3
  108. package/dist-server/harness/setup/runner.js +272 -89
  109. package/dist-server/harness/setup/yaml-scalar.d.ts +23 -0
  110. package/dist-server/harness/setup/yaml-scalar.js +62 -0
  111. package/dist-server/harness/tools/setup-native.d.ts +7 -0
  112. package/dist-server/harness/tools/setup-native.js +7 -4
  113. package/dist-server/server/adopt.js +1 -17
  114. package/dist-server/server/app.d.ts +1 -0
  115. package/dist-server/server/app.js +175 -17
  116. package/dist-server/server/auth-choice.d.ts +13 -1
  117. package/dist-server/server/auth-choice.js +66 -1
  118. package/dist-server/server/bin.js +185 -25
  119. package/dist-server/server/codex-native-session.d.ts +25 -0
  120. package/dist-server/server/codex-native-session.js +1 -0
  121. package/dist-server/server/codex-runtime-composition.d.ts +34 -0
  122. package/dist-server/server/codex-runtime-composition.js +250 -0
  123. package/dist-server/server/codex-structured-ask.d.ts +23 -0
  124. package/dist-server/server/codex-structured-ask.js +102 -0
  125. package/dist-server/server/conn-config.js +4 -16
  126. package/dist-server/server/conversation-socket.d.ts +16 -0
  127. package/dist-server/server/conversation-socket.js +65 -0
  128. package/dist-server/server/db.d.ts +46 -0
  129. package/dist-server/server/db.js +88 -9
  130. package/dist-server/server/env-detect.d.ts +6 -0
  131. package/dist-server/server/env-detect.js +7 -0
  132. package/dist-server/server/fold.d.ts +23 -4
  133. package/dist-server/server/fold.js +42 -5
  134. package/dist-server/server/harness.d.ts +2 -2
  135. package/dist-server/server/harness.js +33 -7
  136. package/dist-server/server/host-evidence-grounding.d.ts +21 -0
  137. package/dist-server/server/host-evidence-grounding.js +93 -0
  138. package/dist-server/server/interactive-terminal.d.ts +10 -6
  139. package/dist-server/server/interactive-terminal.js +33 -8
  140. package/dist-server/server/managed-wren-runtime.d.ts +116 -0
  141. package/dist-server/server/managed-wren-runtime.js +670 -0
  142. package/dist-server/server/native-artifacts.d.ts +112 -7
  143. package/dist-server/server/native-artifacts.js +194 -14
  144. package/dist-server/server/native-component-context.d.ts +19 -0
  145. package/dist-server/server/native-component-context.js +76 -0
  146. package/dist-server/server/native-component-preparation.d.ts +20 -0
  147. package/dist-server/server/native-component-preparation.js +129 -0
  148. package/dist-server/server/native-component-runtime.d.ts +36 -0
  149. package/dist-server/server/native-component-runtime.js +92 -0
  150. package/dist-server/server/native-components.d.ts +84 -0
  151. package/dist-server/server/native-components.js +160 -0
  152. package/dist-server/server/native-dispatch-process.d.ts +8 -0
  153. package/dist-server/server/native-dispatch-process.js +65 -0
  154. package/dist-server/server/native-runtime-spec.d.ts +89 -0
  155. package/dist-server/server/native-runtime-spec.js +169 -0
  156. package/dist-server/server/native-sessions.d.ts +172 -19
  157. package/dist-server/server/native-sessions.js +839 -244
  158. package/dist-server/server/native-shutdown.d.ts +6 -0
  159. package/dist-server/server/native-shutdown.js +28 -0
  160. package/dist-server/server/native-wren-home.d.ts +18 -0
  161. package/dist-server/server/native-wren-home.js +262 -0
  162. package/dist-server/server/node-pty-host.d.ts +2 -1
  163. package/dist-server/server/node-pty-host.js +3 -2
  164. package/dist-server/server/profile-registry.d.ts +139 -0
  165. package/dist-server/server/profile-registry.js +620 -0
  166. package/dist-server/server/runtime-binding.d.ts +4 -1
  167. package/dist-server/server/runtime-binding.js +50 -2
  168. package/dist-server/server/runtime-host/claude-component-step.d.ts +25 -0
  169. package/dist-server/server/runtime-host/claude-component-step.js +186 -0
  170. package/dist-server/server/runtime-host/codex-app-server.d.ts +75 -0
  171. package/dist-server/server/runtime-host/codex-app-server.js +239 -0
  172. package/dist-server/server/runtime-host/codex-compatibility.d.ts +110 -0
  173. package/dist-server/server/runtime-host/codex-compatibility.js +162 -0
  174. package/dist-server/server/runtime-host/codex-component-step.d.ts +19 -0
  175. package/dist-server/server/runtime-host/codex-component-step.js +308 -0
  176. package/dist-server/server/runtime-host/codex-conversation.d.ts +109 -0
  177. package/dist-server/server/runtime-host/codex-conversation.js +295 -0
  178. package/dist-server/server/runtime-host/codex-direct-tools.d.ts +23 -0
  179. package/dist-server/server/runtime-host/codex-direct-tools.js +59 -0
  180. package/dist-server/server/runtime-host/codex-events.d.ts +647 -0
  181. package/dist-server/server/runtime-host/codex-events.js +66 -0
  182. package/dist-server/server/runtime-host/codex-policy.d.ts +16 -0
  183. package/dist-server/server/runtime-host/codex-policy.js +151 -0
  184. package/dist-server/server/runtime-host/codex-process.d.ts +8 -0
  185. package/dist-server/server/runtime-host/codex-process.js +96 -0
  186. package/dist-server/server/runtime-host/codex-rpc.d.ts +47 -0
  187. package/dist-server/server/runtime-host/codex-rpc.js +182 -0
  188. package/dist-server/server/runtime-host/codex-session.d.ts +74 -0
  189. package/dist-server/server/runtime-host/codex-session.js +492 -0
  190. package/dist-server/server/runtime-host/codex-terminal-protocol.d.ts +35 -0
  191. package/dist-server/server/runtime-host/codex-terminal-protocol.js +243 -0
  192. package/dist-server/server/runtime-host/codex-terminal.d.ts +16 -0
  193. package/dist-server/server/runtime-host/codex-terminal.js +120 -0
  194. package/dist-server/server/runtime-host/index.d.ts +5 -0
  195. package/dist-server/server/runtime-host/index.js +3 -0
  196. package/dist-server/server/runtime-host/local.d.ts +29 -0
  197. package/dist-server/server/runtime-host/local.js +90 -0
  198. package/dist-server/server/runtime-host/policy.d.ts +8 -0
  199. package/dist-server/server/runtime-host/policy.js +78 -0
  200. package/dist-server/server/runtime-host/types.d.ts +52 -0
  201. package/dist-server/server/runtime-host/types.js +62 -0
  202. package/dist-server/server/structured-query-evidence.d.ts +7 -0
  203. package/dist-server/server/structured-query-evidence.js +47 -0
  204. package/dist-server/server/structured-runtime.d.ts +52 -0
  205. package/dist-server/server/structured-runtime.js +130 -0
  206. package/dist-server/server/structured-scope.d.ts +4 -0
  207. package/dist-server/server/structured-scope.js +12 -0
  208. package/dist-server/server/turn.d.ts +6 -1
  209. package/dist-server/server/turn.js +86 -26
  210. package/dist-server/server/wire-types.d.ts +67 -3
  211. package/dist-server/server/wren-profiles.js +1 -1
  212. package/managed-wren/README.md +60 -0
  213. package/managed-wren/dependency-wheels.json +282 -0
  214. package/managed-wren/manifest.json +373 -0
  215. package/managed-wren/release-inputs.json +45 -0
  216. package/managed-wren/runtime-tree.cjs +115 -0
  217. package/managed-wren/source-inputs.json +40 -0
  218. package/package.json +15 -6
  219. package/profiles/genbi-default/context/binding.yml +17 -7
  220. package/profiles/genbi-default/context/context.json +146 -0
  221. package/profiles/genbi-default/ir.golden.json +39 -43
  222. package/profiles/genbi-default/profile.yml +31 -0
  223. package/profiles/genbi-enrich-context/context/binding.yml +14 -3
  224. package/profiles/genbi-enrich-context/context/context.json +146 -0
  225. package/profiles/genbi-enrich-context/ir.golden.json +4 -5
  226. package/profiles/genbi-monitor/context/binding.yml +13 -5
  227. package/profiles/genbi-monitor/context/context.json +146 -0
  228. package/profiles/genbi-monitor/ir.golden.json +13 -7
  229. package/profiles/genbi-report/context/binding.yml +19 -0
  230. package/profiles/genbi-report/context/context.json +223 -0
  231. package/profiles/genbi-report/ir.golden.json +328 -0
  232. package/profiles/genbi-report/profile.yml +38 -0
  233. package/profiles/genbi-setup/ir.golden.json +3 -1
  234. package/dist/assets/index-Xs4q7pNo.css +0 -1
@@ -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
+ }
@@ -17,6 +17,13 @@ export interface SetupExecutionToolOptions {
17
17
  * never broaden either its cwd or its write scope beyond it.
18
18
  */
19
19
  readonly workspaceRoot: string;
20
+ /**
21
+ * The bound project directory of a project-bound setup step. When set and the
22
+ * directory exists, an `exec` without an explicit `cwd` runs there instead of
23
+ * at `workspaceRoot`; an explicit `cwd` still wins. Must lie within
24
+ * `workspaceRoot` (same containment check as an explicit `cwd`).
25
+ */
26
+ readonly defaultExecCwd?: string;
20
27
  }
21
28
  /**
22
29
  * A single flat top-level object, NOT a `z.discriminatedUnion` — a
@@ -109,7 +109,7 @@ export const setupExecutionInputSchema = z.object({
109
109
  cwd: z
110
110
  .string()
111
111
  .optional()
112
- .describe('Optional for both actions: use this directory instead of the workspace root as the command cwd or the base for a relative write path (e.g. the project directory). Absolute or relative to the workspace root, and must resolve within it. Defaults to the workspace root when omitted. For exec, use this instead of "cd <dir> && ..." chaining.'),
112
+ .describe('Optional for both actions: use this directory instead of the workspace root as the command cwd or the base for a relative write path (e.g. the project directory). Absolute or relative to the workspace root, and must resolve within it. Defaults to the workspace root when omitted (for exec, the bound project directory once the step has one). For exec, use this instead of "cd <dir> && ..." chaining.'),
113
113
  path: z
114
114
  .string()
115
115
  .optional()
@@ -291,8 +291,11 @@ function resolveWritePath(workspaceRoot, requestedPath, requestedCwd) {
291
291
  * exec cwd to any directory within `workspaceRoot` (e.g. the project
292
292
  * directory), but never broaden it beyond that root.
293
293
  */
294
- function resolveExecCwd(workspaceRoot, requestedCwd) {
294
+ function resolveExecCwd(workspaceRoot, requestedCwd, defaultCwd) {
295
295
  if (requestedCwd === undefined) {
296
+ if (defaultCwd !== undefined && existsSync(defaultCwd)) {
297
+ return assertWithinScope(workspaceRoot, defaultCwd, (requested, scope) => new SetupExecCwdScopeError(requested, scope));
298
+ }
296
299
  return workspaceRoot;
297
300
  }
298
301
  return assertWithinScope(workspaceRoot, requestedCwd, (requestedCwd, scope) => new SetupExecCwdScopeError(requestedCwd, scope));
@@ -319,7 +322,7 @@ export async function executeSetupExecution(input, options) {
319
322
  throw new SetupExecutionInputError("exec", "command");
320
323
  }
321
324
  assertCommandAllowed(input.command);
322
- const cwd = resolveExecCwd(workspaceRoot, input.cwd);
325
+ const cwd = resolveExecCwd(workspaceRoot, input.cwd, options.defaultExecCwd);
323
326
  const result = await env.exec({ mode: "write", command: "/bin/sh", args: ["-c", input.command], cwd, timeoutMs: SETUP_EXEC_TIMEOUT_MS }, policy);
324
327
  const { stdout, stderr } = redactSetupExecutionOutput(input.command, result);
325
328
  return {
@@ -343,7 +346,7 @@ export function createSetupExecutionTool(options) {
343
346
  return tool({
344
347
  description: "Run a shell command or write a file within the setup workspace, subject to a destructive/redirection " +
345
348
  'command denylist and a workspace-root scope. Two call shapes: {action: "exec", command, cwd?} runs a ' +
346
- "shell command (command is required; cwd is optional, defaults to the workspace root, and must resolve " +
349
+ "shell command (command is required; cwd is optional, defaults to the bound project directory for a project step once it exists (otherwise the workspace root), and must resolve " +
347
350
  'within it — path/content are unused); {action: "write", path, content, cwd?} writes a file (path and content ' +
348
351
  "are both required; a relative path uses cwd when supplied, otherwise the workspace root; command is unused). A call missing the field(s) its action requires, or " +
349
352
  "supplying a cwd/path that escapes the workspace root, is rejected before anything runs or is written.",
@@ -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";
@@ -23,9 +24,12 @@ import { verifyProposal } from "./enrichment-verify.js";
23
24
  import { canonicalizeProposal, hashEnrichmentOperation, NATIVE_ENRICHMENT_SUBMIT_INPUT_SCHEMA, NATIVE_ENRICHMENT_SUBMIT_MCP_TOOL_NAME, requiresApproval, resolveEnrichmentBinding, sameEnrichmentBinding, EnrichmentContractError } from "./enrichment.js";
24
25
  import { INTERACTIVE_TARGETS, InteractiveLaunchError } from "./interactive-terminal.js";
25
26
  import { nativeSessionLaunchErrorCode, nativeSessionLaunchFailure, nativeSessionLifecycle, NATIVE_PURPOSES, NATIVE_SETUP_RECOVERY_MCP_TOOL_NAME, NativeSetupRecoveryError } from "./native-sessions.js";
26
- 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
+ import { NATIVE_MCP_PERSIST_ANSWER_TOOL_NAME, NATIVE_MCP_QUERY_TOOL_NAME, NATIVE_MCP_TOOL_NAME, NATIVE_PERSIST_ANSWER_CONTRACT, NATIVE_QUERY_INPUT_SCHEMA, 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,14 +1196,19 @@ 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, or when a payload you write holds only table and definition blocks (plus an optional summary) and every definition cites a query this session ran through the query tool (by its query_id, else its SQL); its tables and definitions are then rebuilt from those query results and your summary is kept. A payload with any other block (narrative, chart, KPI) or an estimate flag is stored intact and unverified, as is any other payload. The existing payload form remains available for compatibility.",
1169
1200
  inputSchema: NATIVE_SAVE_DASHBOARD_CONTRACT.inputSchema,
1170
1201
  };
1171
1202
  const persistAnswerTool = {
1172
1203
  name: NATIVE_MCP_PERSIST_ANSWER_TOOL_NAME,
1173
- description: "Persist this answer's exact typed table and optional definition result before conversational presentation. idempotency_key is retry authority only; the host returns canonical provenance. On success, later save_dashboard calls should use its opaque answer_ref, with the session-scoped latest selector available if that ref leaves the conversation; on failure, present the answer but report that reference saving is unavailable.",
1204
+ description: "Persist this answer's exact typed table and optional definition result before conversational presentation. idempotency_key is retry authority only; the host returns canonical provenance. On success, later save_dashboard calls should use its opaque answer_ref, with the session-scoped latest selector available if that ref leaves the conversation; on failure, present the answer but report that reference saving is unavailable. The host records the answer as verified only when its definition cites a query this session ran through the query tool (by its query_id, else its SQL); its table and definition are then rebuilt from that query's result. Any other answer is stored as written and unverified.",
1174
1205
  inputSchema: NATIVE_PERSIST_ANSWER_CONTRACT.inputSchema,
1175
1206
  };
1207
+ const queryTool = {
1208
+ name: NATIVE_MCP_QUERY_TOOL_NAME,
1209
+ description: "Run one read-only SQL query through the host and return its rows plus a query_id. Cite that query_id in the definition block of the answer you persist or save; only answers whose every definition cites a query this session ran here can be recorded as verified.",
1210
+ inputSchema: NATIVE_QUERY_INPUT_SCHEMA,
1211
+ };
1176
1212
  const enrichmentSubmitTool = {
1177
1213
  name: NATIVE_ENRICHMENT_SUBMIT_MCP_TOOL_NAME,
1178
1214
  description: "Submit a drafted context enrichment proposal for host verification. The host re-checks the project binding, canonicalizes the proposal, and runs it through a shadow-copy verification ladder (grammar, sink, validate, build, dry-run) against a throwaway copy of the project -- the bound project itself is never written to. Only a verified proposal is recorded, into the same enrichment ledger a human reviews through accept/edit/skip. A refuted proposal is never recorded; the refuting step and reason are returned so the draft can be revised.",
@@ -1185,7 +1221,7 @@ export function createApp(deps) {
1185
1221
  const nativeToolsForPurpose = (purpose) => {
1186
1222
  switch (purpose) {
1187
1223
  case "setup": return [setupTool];
1188
- case "analysis": return [persistAnswerTool, artifactTool];
1224
+ case "analysis": return deps.nativeArtifacts?.queryAvailable() ? [queryTool, persistAnswerTool, artifactTool] : [persistAnswerTool, artifactTool];
1189
1225
  case "context_enrichment": return [enrichmentSubmitTool];
1190
1226
  default: {
1191
1227
  const exhaustive = purpose;
@@ -1196,11 +1232,15 @@ export function createApp(deps) {
1196
1232
  if (request.method === "tools/list")
1197
1233
  return isNotification ? invalidRequest() : c.json({
1198
1234
  jsonrpc: "2.0", id,
1199
- result: { tools: nativeToolsForPurpose(nativeSession.purpose) },
1235
+ result: { tools: [...nativeToolsForPurpose(nativeSession.purpose), ...(deps.nativeSessions?.componentTools(nativeSession.id) ?? [])] },
1200
1236
  });
1201
1237
  if (request.method !== "tools/call" || isNotification || !isRecord(request.params) || !Object.hasOwn(request.params, "arguments"))
1202
1238
  return methodNotFound();
1203
1239
  try {
1240
+ if (typeof request.params.name === "string" && request.params.name.startsWith("warble_run_") && deps.nativeSessions && id !== null) {
1241
+ const result = await deps.nativeSessions.callComponent(nativeSession.id, request.params.name, request.params.arguments, id, c.req.raw.signal);
1242
+ return c.json({ jsonrpc: "2.0", id, result: { isError: result.status !== "ok", content: [{ type: "text", text: JSON.stringify(result) }], structuredContent: result } });
1243
+ }
1204
1244
  if (nativeSession.purpose === "setup" && request.params.name === NATIVE_SETUP_RECOVERY_MCP_TOOL_NAME && deps.nativeSessions) {
1205
1245
  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
1246
  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 } } });
@@ -1209,6 +1249,10 @@ export function createApp(deps) {
1209
1249
  const saved = deps.nativeArtifacts.save(credential, request.params.arguments);
1210
1250
  return c.json({ jsonrpc: "2.0", id, result: { content: [{ type: "text", text: JSON.stringify(saved) }], structuredContent: saved } });
1211
1251
  }
1252
+ if (nativeSession.purpose === "analysis" && request.params.name === NATIVE_MCP_QUERY_TOOL_NAME && deps.nativeArtifacts.queryAvailable()) {
1253
+ const queried = await deps.nativeArtifacts.query(credential, request.params.arguments);
1254
+ return c.json({ jsonrpc: "2.0", id, result: { isError: queried.isError, content: [{ type: "text", text: queried.text }], ...(queried.result !== undefined ? { structuredContent: queried.result } : {}) } });
1255
+ }
1212
1256
  if (nativeSession.purpose === "analysis" && request.params.name === NATIVE_MCP_PERSIST_ANSWER_TOOL_NAME) {
1213
1257
  const persisted = deps.nativeArtifacts.persistAnswer(credential, request.params.arguments);
1214
1258
  return c.json({ jsonrpc: "2.0", id, result: { content: [{ type: "text", text: JSON.stringify(persisted) }], structuredContent: persisted } });
@@ -1582,6 +1626,61 @@ export function createApp(deps) {
1582
1626
  }
1583
1627
  });
1584
1628
  // ---------------------------------------------------------------------
1629
+ // Warble profile registry
1630
+ // ---------------------------------------------------------------------
1631
+ //
1632
+ // Conversation profiles are selected from a server-held registry by id. The
1633
+ // registry owns the directory, runs admission and resolves the launch tuple;
1634
+ // a browser never submits a path for a session to run inside. Adding a
1635
+ // profile takes a directory path on this machine, the same shape the adopt
1636
+ // flow accepts for a project, and the registry copies and checks it.
1637
+ const profileRegistryFailure = (c, error) => {
1638
+ if (error instanceof ProfileRegistryError)
1639
+ return c.json({ error: error.message }, error.status);
1640
+ return c.json({ error: error instanceof Error ? error.message : String(error) }, 500);
1641
+ };
1642
+ app.get("/api/profiles", async (c) => {
1643
+ if (!deps.profileRegistry)
1644
+ return c.json({ error: "the profile registry is not configured on this BFF instance" }, 503);
1645
+ try {
1646
+ const rows = await deps.profileRegistry.list();
1647
+ const response = { profiles: rows.map(toWarbleProfileDto) };
1648
+ return c.json(response);
1649
+ }
1650
+ catch (error) {
1651
+ return profileRegistryFailure(c, error);
1652
+ }
1653
+ });
1654
+ app.post("/api/profiles", async (c) => {
1655
+ if (!deps.profileRegistry)
1656
+ return c.json({ error: "the profile registry is not configured on this BFF instance" }, 503);
1657
+ const body = await c.req.json().catch(() => undefined);
1658
+ if (typeof body !== "object" || body === null || Array.isArray(body) || !Object.keys(body).every((key) => key === "sourcePath")) {
1659
+ return c.json({ error: "profile registration request is invalid: expected { sourcePath }" }, 400);
1660
+ }
1661
+ try {
1662
+ const row = await deps.profileRegistry.add(body.sourcePath);
1663
+ return c.json({ profile: toWarbleProfileDto(row) }, 201);
1664
+ }
1665
+ catch (error) {
1666
+ return profileRegistryFailure(c, error);
1667
+ }
1668
+ });
1669
+ app.delete("/api/profiles/:id", async (c) => {
1670
+ if (!deps.profileRegistry)
1671
+ return c.json({ error: "the profile registry is not configured on this BFF instance" }, 503);
1672
+ const id = c.req.param("id");
1673
+ if (!/^[a-z0-9][a-z0-9_-]{0,63}$/.test(id))
1674
+ return c.json({ error: "profile id is invalid" }, 400);
1675
+ try {
1676
+ await deps.profileRegistry.remove(id);
1677
+ return c.body(null, 204);
1678
+ }
1679
+ catch (error) {
1680
+ return profileRegistryFailure(c, error);
1681
+ }
1682
+ });
1683
+ // ---------------------------------------------------------------------
1585
1684
  // Harness introspection
1586
1685
  // ---------------------------------------------------------------------
1587
1686
  app.get("/api/harness", async (c) => {
@@ -1590,16 +1689,44 @@ export function createApp(deps) {
1590
1689
  return c.json({ error: "harness purpose must be exactly one of: setup, analysis, context_enrichment" }, 400);
1591
1690
  }
1592
1691
  const purpose = purposes[0];
1692
+ // An optional registry id selects which conversation profile the analysis purpose describes.
1693
+ // The server resolves it; the browser never names a directory.
1694
+ const profiles = c.req.queries("profile") ?? [];
1695
+ if (profiles.length > 1)
1696
+ return c.json({ error: "harness profile must be given at most once" }, 400);
1697
+ const requestedProfile = profiles[0];
1698
+ if (requestedProfile !== undefined) {
1699
+ if (!/^[a-z0-9][a-z0-9_-]{0,63}$/.test(requestedProfile))
1700
+ return c.json({ error: "harness profile is invalid" }, 400);
1701
+ if (purpose !== "analysis")
1702
+ return c.json({ error: "harness profile is only selectable for analysis" }, 400);
1703
+ }
1704
+ const selectedProfile = requestedProfile !== undefined && requestedProfile !== DEFAULT_CONVERSATION_PROFILE ? requestedProfile : undefined;
1593
1705
  // Setup describes the raw bootstrap profile, before a project exists. The
1594
1706
  // other two profiles consume a bound project's context and stay gated.
1595
1707
  if (purpose !== "setup" && !isProjectBound(deps))
1596
1708
  return c.json({ error: PROJECT_NOT_BOUND_MESSAGE }, 409);
1597
1709
  if (!deps.describeHarnessBundle)
1598
1710
  return c.json({ error: "harness introspection is not configured" }, 500);
1711
+ let selectedSource;
1712
+ if (selectedProfile !== undefined) {
1713
+ if (!deps.profileRegistry)
1714
+ return c.json({ error: "the profile registry is not configured on this BFF instance" }, 503);
1715
+ try {
1716
+ selectedSource = (await deps.profileRegistry.resolveConversationProfileSource(selectedProfile)).sourceDir;
1717
+ }
1718
+ catch (error) {
1719
+ return profileRegistryFailure(c, error);
1720
+ }
1721
+ }
1599
1722
  try {
1600
1723
  const routeOptions = effectiveRouteOptions(deps, { allowUnbound: purpose === "setup" });
1601
- const bundle = await deps.describeHarnessBundle(purpose, routeOptions);
1602
- assertHarnessBundlePurpose(bundle, purpose);
1724
+ // Keep the two-argument call for the fixed profiles so existing describers and their tests see
1725
+ // exactly the shape they always did; only a selection adds the third argument.
1726
+ const bundle = selectedSource !== undefined
1727
+ ? await deps.describeHarnessBundle(purpose, routeOptions, selectedSource)
1728
+ : await deps.describeHarnessBundle(purpose, routeOptions);
1729
+ assertHarnessBundlePurpose(bundle, purpose, selectedProfile);
1603
1730
  const setupRuntimeCorrection = purpose === "setup" ? persistedRuntimeCorrection(deps) : undefined;
1604
1731
  const setupReadiness = purpose === "setup"
1605
1732
  ? setupRuntimeCorrection !== undefined
@@ -1616,7 +1743,7 @@ export function createApp(deps) {
1616
1743
  const nativeReadiness = purpose === "setup"
1617
1744
  ? await deps.nativeSessions?.readiness().catch(() => undefined)
1618
1745
  : await deps.nativeSessions?.readiness();
1619
- return c.json(buildHarnessDto(bundle, deps.store, routeOptions, purpose, nativeReadiness, setupReadiness));
1746
+ return c.json(buildHarnessDto(bundle, deps.store, routeOptions, purpose, nativeReadiness, setupReadiness, selectedProfile));
1620
1747
  }
1621
1748
  catch (err) {
1622
1749
  const message = err instanceof Error ? err.message : String(err);
@@ -1626,7 +1753,12 @@ export function createApp(deps) {
1626
1753
  // ---------------------------------------------------------------------
1627
1754
  // Config / setup pages
1628
1755
  // ---------------------------------------------------------------------
1629
- app.get("/api/config/runtime", (c) => c.json(deps.store.getRuntimeSettings()));
1756
+ // Until the user saves, the effective runtime is the boot auth env, so report that rather than
1757
+ // the seeded display default. Nothing here is persisted and no credential is read.
1758
+ app.get("/api/config/runtime", (c) => {
1759
+ const stored = deps.store.getRuntimeSettings();
1760
+ return c.json(deps.store.hasExplicitRuntimeSettings() ? stored : runtimeSettingsFromAuthChoice(deps.baseRouteOptions.authChoice, stored));
1761
+ });
1630
1762
  app.get("/api/config/runtime/readiness", (c) => {
1631
1763
  const correction = persistedRuntimeCorrection(deps);
1632
1764
  return c.json(correction ? { valid: false, correction } : { valid: true });
@@ -1673,9 +1805,31 @@ export function createApp(deps) {
1673
1805
  : { version: 1, status: "unavailable", provider, code: "runtime_unavailable", retryable: true };
1674
1806
  return c.json(result);
1675
1807
  });
1808
+ /** pi-ai's built-in catalog (offline, no credentials) for the gateway option's suggestions. */
1809
+ app.get("/api/harness/gateway-models", async (c) => {
1810
+ const provider = c.req.query("provider")?.trim() || undefined;
1811
+ try {
1812
+ const catalog = await listPiAiCatalog(provider);
1813
+ return c.json({
1814
+ status: "ready",
1815
+ providers: catalog.providers,
1816
+ models: catalog.models.map((model) => ({ model: model.id, displayName: model.name })),
1817
+ });
1818
+ }
1819
+ catch {
1820
+ // A fixed reason: the load error is Node's module-resolution text, which can name local paths.
1821
+ return c.json({ status: "unavailable", reason: GATEWAY_CATALOG_UNAVAILABLE });
1822
+ }
1823
+ });
1676
1824
  app.put("/api/config/runtime", async (c) => {
1677
1825
  const patch = await c.req.json().catch(() => ({}));
1678
- const updated = { ...deps.store.getRuntimeSettings(), ...patch };
1826
+ // Seed the merge from what GET reports, so a first save patches the effective (boot) runtime.
1827
+ const stored = deps.store.getRuntimeSettings();
1828
+ const effective = deps.store.hasExplicitRuntimeSettings() ? stored : runtimeSettingsFromAuthChoice(deps.baseRouteOptions.authChoice, stored);
1829
+ const updated = { ...effective, ...patch };
1830
+ if (!isRuntimeAuthMode(updated.authMode)) {
1831
+ return c.json({ error: "authMode must be one of subscription, byo, local, gateway" }, 400);
1832
+ }
1679
1833
  // Derive the candidate live AuthChoice from the merged (not-yet-persisted)
1680
1834
  // settings and validate it BEFORE any mutation — a rejected save must leave
1681
1835
  // both the persisted settings and the live auth binding untouched.
@@ -1712,7 +1866,9 @@ export function createApp(deps) {
1712
1866
  if (candidateAuthChoice.mode !== "subscription") {
1713
1867
  const envStatus = detectAdapterEnv();
1714
1868
  for (const envVar of requiredInProcessCredentialEnvVars(updated)) {
1715
- const present = envVar === "OPENAI_API_KEY" ? envStatus.openaiCompatible : envStatus.anthropic;
1869
+ const present = envVar === "OPENAI_API_KEY" ? envStatus.openaiCompatible
1870
+ : envVar === GATEWAY_API_KEY_ENV ? envStatus.gateway === true
1871
+ : envStatus.anthropic;
1716
1872
  if (!present)
1717
1873
  return c.json({ error: `${envVar} is not set on the server — required by a configured tier adapter` }, 400);
1718
1874
  }
@@ -1728,6 +1884,7 @@ export function createApp(deps) {
1728
1884
  }
1729
1885
  const revokedNativeSessionIds = deps.store.setRuntimeSettingsAndRevokeIncompatibleNativeSessions(updated);
1730
1886
  deps.nativeSessions?.revokeRuntimeCapabilities(revokedNativeSessionIds);
1887
+ deps.structuredRuntime?.revoke();
1731
1888
  deps.revokeInteractiveTerminals?.();
1732
1889
  deps.setAuthChoice?.(candidateAuthChoice);
1733
1890
  // describeBundle branches its target on authChoice.mode, so a cached
@@ -1909,6 +2066,7 @@ export function createApp(deps) {
1909
2066
  }
1910
2067
  const revokedNativeSessionIds = deps.store.resetSetup();
1911
2068
  deps.nativeSessions?.revokeRuntimeCapabilities(revokedNativeSessionIds);
2069
+ deps.structuredRuntime?.revoke();
1912
2070
  deps.revokeInteractiveTerminals?.();
1913
2071
  deps.unbindProject?.();
1914
2072
  // 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;
@@ -1,13 +1,16 @@
1
+ import { GATEWAY_API_KEY_ENV } from "./env-detect.js";
1
2
  /**
2
3
  * Maps the wizard's persisted `RuntimeSettings` to the harness-level `AuthChoice` that should
3
4
  * actually be dispatched with. `RuntimeSettings.authMode` is the wire/persisted vocabulary
4
- * (`"subscription" | "byo" | "local"`); `AuthChoice.mode` is the harness vocabulary
5
+ * (`"subscription" | "byo" | "local" | "gateway"`); `AuthChoice.mode` is the harness vocabulary
5
6
  * (`"subscription" | "api-key" | "local" | "gateway"`) — `"byo"` maps to `"api-key"`.
6
7
  *
7
8
  * For the `openai-compatible` adapter, the BFF reads `OPENAI_API_KEY` from its own process env
8
9
  * and injects it explicitly as `config.apiKey` — that adapter has no env self-read of its own.
9
10
  * For `anthropic`, `config.apiKey` is deliberately omitted so the adapter self-reads
10
11
  * `ANTHROPIC_API_KEY` from the process env itself (see `harness/providers/adapters/anthropic.ts`).
12
+ * For `gateway` (the `pi-ai` adapter), the BFF reads `GENBI_GATEWAY_API_KEY` and injects it as
13
+ * `config.apiKey` the same way — pi-ai never reads env vars or credential files on its own.
11
14
  * The key value is never read into `RuntimeSettings`, SQLite, or any log — only referenced here,
12
15
  * transiently, to build the in-memory `AuthChoice` passed straight to the adapter factory.
13
16
  */
@@ -17,6 +20,19 @@ export function toAuthChoiceFromRuntimeSettings(settings) {
17
20
  return { mode: "subscription", provider: settings.subscriptionProvider ?? "claude" };
18
21
  case "local":
19
22
  return { mode: "local" };
23
+ case "gateway": {
24
+ const config = {};
25
+ if (settings.gatewayProvider !== undefined)
26
+ config["provider"] = settings.gatewayProvider;
27
+ if (settings.gatewayModel !== undefined)
28
+ config["model"] = settings.gatewayModel;
29
+ if (settings.gatewayBaseURL?.trim())
30
+ config["baseUrl"] = settings.gatewayBaseURL;
31
+ const apiKey = process.env[GATEWAY_API_KEY_ENV];
32
+ if (apiKey?.trim())
33
+ config["apiKey"] = apiKey;
34
+ return { mode: "gateway", config };
35
+ }
20
36
  case "byo": {
21
37
  const adapter = settings.apiKeyAdapter ?? "anthropic";
22
38
  if (adapter === "openai-compatible") {
@@ -34,3 +50,52 @@ export function toAuthChoiceFromRuntimeSettings(settings) {
34
50
  }
35
51
  }
36
52
  }
53
+ const RUNTIME_AUTH_MODES = new Set(["subscription", "byo", "local", "gateway"]);
54
+ /** True for the wire `authMode` values `toAuthChoiceFromRuntimeSettings` can map. */
55
+ export function isRuntimeAuthMode(value) {
56
+ return typeof value === "string" && RUNTIME_AUTH_MODES.has(value);
57
+ }
58
+ function stringField(config, key) {
59
+ const value = config?.[key];
60
+ return typeof value === "string" && value.trim().length > 0 ? value : undefined;
61
+ }
62
+ /**
63
+ * The inverse of `toAuthChoiceFromRuntimeSettings`, for reporting the auth the BFF booted with
64
+ * (`WREN_HARNESS_MODE` / `MODEL` / `ENDPOINT` / flags) before the user has saved a runtime.
65
+ * Overlays only the auth-selection fields onto `base`; tier rows, deployment and hybrid stay the
66
+ * store's. Credentials never appear in the result. A boot adapter the wizard cannot represent
67
+ * leaves `base` untouched.
68
+ */
69
+ export function runtimeSettingsFromAuthChoice(choice, base) {
70
+ switch (choice.mode) {
71
+ case "subscription":
72
+ return { ...base, authMode: "subscription", subscriptionProvider: choice.provider };
73
+ case "local":
74
+ return { ...base, authMode: "local" };
75
+ case "gateway": {
76
+ const provider = stringField(choice.config, "provider");
77
+ const model = stringField(choice.config, "model");
78
+ const baseURL = stringField(choice.config, "baseUrl");
79
+ return {
80
+ ...base,
81
+ authMode: "gateway",
82
+ ...(provider !== undefined ? { gatewayProvider: provider } : {}),
83
+ ...(model !== undefined ? { gatewayModel: model } : {}),
84
+ ...(baseURL !== undefined ? { gatewayBaseURL: baseURL } : {}),
85
+ };
86
+ }
87
+ case "api-key": {
88
+ if (choice.adapter !== "anthropic" && choice.adapter !== "openai-compatible")
89
+ return base;
90
+ const model = stringField(choice.config, "model");
91
+ const baseURL = choice.adapter === "openai-compatible" ? stringField(choice.config, "baseURL") : undefined;
92
+ return {
93
+ ...base,
94
+ authMode: "byo",
95
+ apiKeyAdapter: choice.adapter,
96
+ ...(model !== undefined ? { apiKeyModel: model } : {}),
97
+ ...(baseURL !== undefined ? { apiKeyBaseURL: baseURL } : {}),
98
+ };
99
+ }
100
+ }
101
+ }