@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.
- package/README.md +65 -3
- package/dist/assets/{echarts-LTFC-ysd.js → echarts-DYM4hjvk.js} +2 -2
- package/dist/assets/index-A_Wrq7Ey.css +1 -0
- package/dist/assets/{index-Bo--QMUN.js → index-DsH129tV.js} +18 -17
- package/dist/index.html +2 -2
- package/dist-server/harness/bundle/schema.d.ts +38 -0
- package/dist-server/harness/bundle/schema.js +18 -10
- package/dist-server/harness/bundle/version.js +1 -1
- package/dist-server/harness/cli-args.d.ts +10 -2
- package/dist-server/harness/cli-args.js +24 -8
- package/dist-server/harness/cli.js +75 -9
- package/dist-server/harness/compile/cache.js +4 -4
- package/dist-server/harness/compile/compose-profile.d.ts +51 -14
- package/dist-server/harness/compile/compose-profile.js +85 -19
- package/dist-server/harness/compile/context-loader.d.ts +56 -0
- package/dist-server/harness/compile/context-loader.js +237 -0
- package/dist-server/harness/compile/errors.d.ts +19 -0
- package/dist-server/harness/compile/errors.js +35 -0
- package/dist-server/harness/compile/index.d.ts +4 -3
- package/dist-server/harness/compile/index.js +4 -3
- package/dist-server/harness/compile/pipeline.d.ts +15 -7
- package/dist-server/harness/compile/pipeline.js +62 -14
- package/dist-server/harness/compile/resolve-binary.d.ts +2 -2
- package/dist-server/harness/compile/resolve-binary.js +3 -3
- package/dist-server/harness/compile/types.d.ts +29 -0
- package/dist-server/harness/compile/warble-identity.d.ts +11 -0
- package/dist-server/harness/compile/warble-identity.js +13 -0
- package/dist-server/harness/components/ai-step.d.ts +4 -0
- package/dist-server/harness/components/ai-step.js +40 -0
- package/dist-server/harness/components/broker.d.ts +60 -0
- package/dist-server/harness/components/broker.js +117 -0
- package/dist-server/harness/components/capability-card.d.ts +98 -0
- package/dist-server/harness/components/capability-card.js +169 -0
- package/dist-server/harness/components/context.d.ts +2 -0
- package/dist-server/harness/components/context.js +30 -0
- package/dist-server/harness/components/display.d.ts +6 -0
- package/dist-server/harness/components/display.js +29 -0
- package/dist-server/harness/components/egress-judge.d.ts +12 -0
- package/dist-server/harness/components/egress-judge.js +27 -0
- package/dist-server/harness/components/egress.d.ts +142 -0
- package/dist-server/harness/components/egress.js +469 -0
- package/dist-server/harness/components/in-process.d.ts +7 -0
- package/dist-server/harness/components/in-process.js +245 -0
- package/dist-server/harness/components/normalize.d.ts +35 -0
- package/dist-server/harness/components/normalize.js +245 -0
- package/dist-server/harness/components/plan.d.ts +31 -0
- package/dist-server/harness/components/plan.js +184 -0
- package/dist-server/harness/components/report.d.ts +207 -0
- package/dist-server/harness/components/report.js +309 -0
- package/dist-server/harness/components/runner.d.ts +193 -0
- package/dist-server/harness/components/runner.js +492 -0
- package/dist-server/harness/components/semantic-guards.d.ts +20 -0
- package/dist-server/harness/components/semantic-guards.js +42 -0
- package/dist-server/harness/components/terminal-json.d.ts +18 -0
- package/dist-server/harness/components/terminal-json.js +82 -0
- package/dist-server/harness/components/usage.d.ts +14 -0
- package/dist-server/harness/components/usage.js +19 -0
- package/dist-server/harness/components/wren-access.d.ts +29 -0
- package/dist-server/harness/components/wren-access.js +233 -0
- package/dist-server/harness/components/zone-dry-run.d.ts +43 -0
- package/dist-server/harness/components/zone-dry-run.js +107 -0
- package/dist-server/harness/components/zone-gate.d.ts +76 -0
- package/dist-server/harness/components/zone-gate.js +144 -0
- package/dist-server/harness/events/index.d.ts +1 -1
- package/dist-server/harness/events/types.d.ts +52 -0
- package/dist-server/harness/index.d.ts +9 -2
- package/dist-server/harness/index.js +5 -1
- package/dist-server/harness/loop/executor.d.ts +1 -0
- package/dist-server/harness/loop/executor.js +37 -15
- package/dist-server/harness/npm-package-resolve.d.ts +2 -0
- package/dist-server/harness/npm-package-resolve.js +16 -4
- package/dist-server/harness/providers/adapters/openai-compatible.d.ts +13 -0
- package/dist-server/harness/providers/adapters/openai-compatible.js +17 -0
- package/dist-server/harness/providers/adapters/openai.d.ts +7 -0
- package/dist-server/harness/providers/adapters/openai.js +6 -0
- package/dist-server/harness/providers/adapters/pi-ai.d.ts +48 -0
- package/dist-server/harness/providers/adapters/pi-ai.js +518 -0
- package/dist-server/harness/providers/binding.d.ts +45 -4
- package/dist-server/harness/providers/binding.js +44 -8
- package/dist-server/harness/providers/index.d.ts +9 -3
- package/dist-server/harness/providers/index.js +10 -3
- package/dist-server/harness/providers/zone.d.ts +96 -0
- package/dist-server/harness/providers/zone.js +112 -0
- package/dist-server/harness/render/answering-query.d.ts +29 -0
- package/dist-server/harness/render/answering-query.js +62 -0
- package/dist-server/harness/render/envelope.d.ts +6 -0
- package/dist-server/harness/render/envelope.js +10 -3
- package/dist-server/harness/route/adapter-spec.d.ts +8 -9
- package/dist-server/harness/route/adapter-spec.js +13 -14
- package/dist-server/harness/route/codex-ask-events.js +2 -1
- package/dist-server/harness/route/codex-ask.d.ts +4 -0
- package/dist-server/harness/route/codex-ask.js +43 -45
- package/dist-server/harness/route/codex-local-manifest.d.ts +9 -1
- package/dist-server/harness/route/codex-local-manifest.js +96 -15
- package/dist-server/harness/route/describe.js +45 -6
- package/dist-server/harness/route/dispatched.d.ts +20 -0
- package/dist-server/harness/route/dispatched.js +122 -24
- package/dist-server/harness/route/host-query.d.ts +93 -0
- package/dist-server/harness/route/host-query.js +257 -0
- package/dist-server/harness/route/in-process.d.ts +22 -1
- package/dist-server/harness/route/in-process.js +54 -4
- package/dist-server/harness/route/route.js +8 -2
- package/dist-server/harness/route/types.d.ts +47 -1
- package/dist-server/harness/route/wren-host-proxy.d.ts +33 -0
- package/dist-server/harness/route/wren-host-proxy.js +228 -0
- package/dist-server/harness/session/run.js +30 -10
- package/dist-server/harness/setup/runner.d.ts +17 -3
- package/dist-server/harness/setup/runner.js +272 -89
- package/dist-server/harness/setup/yaml-scalar.d.ts +23 -0
- package/dist-server/harness/setup/yaml-scalar.js +62 -0
- package/dist-server/harness/tools/setup-native.d.ts +7 -0
- package/dist-server/harness/tools/setup-native.js +7 -4
- package/dist-server/server/adopt.js +1 -17
- package/dist-server/server/app.d.ts +1 -0
- package/dist-server/server/app.js +175 -17
- package/dist-server/server/auth-choice.d.ts +13 -1
- package/dist-server/server/auth-choice.js +66 -1
- package/dist-server/server/bin.js +185 -25
- package/dist-server/server/codex-native-session.d.ts +25 -0
- package/dist-server/server/codex-native-session.js +1 -0
- package/dist-server/server/codex-runtime-composition.d.ts +34 -0
- package/dist-server/server/codex-runtime-composition.js +250 -0
- package/dist-server/server/codex-structured-ask.d.ts +23 -0
- package/dist-server/server/codex-structured-ask.js +102 -0
- package/dist-server/server/conn-config.js +4 -16
- package/dist-server/server/conversation-socket.d.ts +16 -0
- package/dist-server/server/conversation-socket.js +65 -0
- package/dist-server/server/db.d.ts +46 -0
- package/dist-server/server/db.js +88 -9
- package/dist-server/server/env-detect.d.ts +6 -0
- package/dist-server/server/env-detect.js +7 -0
- package/dist-server/server/fold.d.ts +23 -4
- package/dist-server/server/fold.js +42 -5
- package/dist-server/server/harness.d.ts +2 -2
- package/dist-server/server/harness.js +33 -7
- package/dist-server/server/host-evidence-grounding.d.ts +21 -0
- package/dist-server/server/host-evidence-grounding.js +93 -0
- package/dist-server/server/interactive-terminal.d.ts +10 -6
- package/dist-server/server/interactive-terminal.js +33 -8
- package/dist-server/server/managed-wren-runtime.d.ts +116 -0
- package/dist-server/server/managed-wren-runtime.js +670 -0
- package/dist-server/server/native-artifacts.d.ts +112 -7
- package/dist-server/server/native-artifacts.js +194 -14
- package/dist-server/server/native-component-context.d.ts +19 -0
- package/dist-server/server/native-component-context.js +76 -0
- package/dist-server/server/native-component-preparation.d.ts +20 -0
- package/dist-server/server/native-component-preparation.js +129 -0
- package/dist-server/server/native-component-runtime.d.ts +36 -0
- package/dist-server/server/native-component-runtime.js +92 -0
- package/dist-server/server/native-components.d.ts +84 -0
- package/dist-server/server/native-components.js +160 -0
- package/dist-server/server/native-dispatch-process.d.ts +8 -0
- package/dist-server/server/native-dispatch-process.js +65 -0
- package/dist-server/server/native-runtime-spec.d.ts +89 -0
- package/dist-server/server/native-runtime-spec.js +169 -0
- package/dist-server/server/native-sessions.d.ts +172 -19
- package/dist-server/server/native-sessions.js +839 -244
- package/dist-server/server/native-shutdown.d.ts +6 -0
- package/dist-server/server/native-shutdown.js +28 -0
- package/dist-server/server/native-wren-home.d.ts +18 -0
- package/dist-server/server/native-wren-home.js +262 -0
- package/dist-server/server/node-pty-host.d.ts +2 -1
- package/dist-server/server/node-pty-host.js +3 -2
- package/dist-server/server/profile-registry.d.ts +139 -0
- package/dist-server/server/profile-registry.js +620 -0
- package/dist-server/server/runtime-binding.d.ts +4 -1
- package/dist-server/server/runtime-binding.js +50 -2
- package/dist-server/server/runtime-host/claude-component-step.d.ts +25 -0
- package/dist-server/server/runtime-host/claude-component-step.js +186 -0
- package/dist-server/server/runtime-host/codex-app-server.d.ts +75 -0
- package/dist-server/server/runtime-host/codex-app-server.js +239 -0
- package/dist-server/server/runtime-host/codex-compatibility.d.ts +110 -0
- package/dist-server/server/runtime-host/codex-compatibility.js +162 -0
- package/dist-server/server/runtime-host/codex-component-step.d.ts +19 -0
- package/dist-server/server/runtime-host/codex-component-step.js +308 -0
- package/dist-server/server/runtime-host/codex-conversation.d.ts +109 -0
- package/dist-server/server/runtime-host/codex-conversation.js +295 -0
- package/dist-server/server/runtime-host/codex-direct-tools.d.ts +23 -0
- package/dist-server/server/runtime-host/codex-direct-tools.js +59 -0
- package/dist-server/server/runtime-host/codex-events.d.ts +647 -0
- package/dist-server/server/runtime-host/codex-events.js +66 -0
- package/dist-server/server/runtime-host/codex-policy.d.ts +16 -0
- package/dist-server/server/runtime-host/codex-policy.js +151 -0
- package/dist-server/server/runtime-host/codex-process.d.ts +8 -0
- package/dist-server/server/runtime-host/codex-process.js +96 -0
- package/dist-server/server/runtime-host/codex-rpc.d.ts +47 -0
- package/dist-server/server/runtime-host/codex-rpc.js +182 -0
- package/dist-server/server/runtime-host/codex-session.d.ts +74 -0
- package/dist-server/server/runtime-host/codex-session.js +492 -0
- package/dist-server/server/runtime-host/codex-terminal-protocol.d.ts +35 -0
- package/dist-server/server/runtime-host/codex-terminal-protocol.js +243 -0
- package/dist-server/server/runtime-host/codex-terminal.d.ts +16 -0
- package/dist-server/server/runtime-host/codex-terminal.js +120 -0
- package/dist-server/server/runtime-host/index.d.ts +5 -0
- package/dist-server/server/runtime-host/index.js +3 -0
- package/dist-server/server/runtime-host/local.d.ts +29 -0
- package/dist-server/server/runtime-host/local.js +90 -0
- package/dist-server/server/runtime-host/policy.d.ts +8 -0
- package/dist-server/server/runtime-host/policy.js +78 -0
- package/dist-server/server/runtime-host/types.d.ts +52 -0
- package/dist-server/server/runtime-host/types.js +62 -0
- package/dist-server/server/structured-query-evidence.d.ts +7 -0
- package/dist-server/server/structured-query-evidence.js +47 -0
- package/dist-server/server/structured-runtime.d.ts +52 -0
- package/dist-server/server/structured-runtime.js +130 -0
- package/dist-server/server/structured-scope.d.ts +4 -0
- package/dist-server/server/structured-scope.js +12 -0
- package/dist-server/server/turn.d.ts +6 -1
- package/dist-server/server/turn.js +86 -26
- package/dist-server/server/wire-types.d.ts +67 -3
- package/dist-server/server/wren-profiles.js +1 -1
- package/managed-wren/README.md +60 -0
- package/managed-wren/dependency-wheels.json +282 -0
- package/managed-wren/manifest.json +373 -0
- package/managed-wren/release-inputs.json +45 -0
- package/managed-wren/runtime-tree.cjs +115 -0
- package/managed-wren/source-inputs.json +40 -0
- package/package.json +15 -6
- package/profiles/genbi-default/context/binding.yml +17 -7
- package/profiles/genbi-default/context/context.json +146 -0
- package/profiles/genbi-default/ir.golden.json +39 -43
- package/profiles/genbi-default/profile.yml +31 -0
- package/profiles/genbi-enrich-context/context/binding.yml +14 -3
- package/profiles/genbi-enrich-context/context/context.json +146 -0
- package/profiles/genbi-enrich-context/ir.golden.json +4 -5
- package/profiles/genbi-monitor/context/binding.yml +13 -5
- package/profiles/genbi-monitor/context/context.json +146 -0
- package/profiles/genbi-monitor/ir.golden.json +13 -7
- package/profiles/genbi-report/context/binding.yml +19 -0
- package/profiles/genbi-report/context/context.json +223 -0
- package/profiles/genbi-report/ir.golden.json +328 -0
- package/profiles/genbi-report/profile.yml +38 -0
- package/profiles/genbi-setup/ir.golden.json +3 -1
- 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 {
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
1602
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
+
}
|