harness-dispatch 0.4.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +152 -0
- package/README.md +232 -87
- package/config.default.yaml +416 -397
- package/dist/auth.d.ts.map +1 -1
- package/dist/auth.js +2 -2
- package/dist/auth.js.map +1 -1
- package/dist/billing.d.ts +35 -4
- package/dist/billing.d.ts.map +1 -1
- package/dist/billing.js +48 -17
- package/dist/billing.js.map +1 -1
- package/dist/bin.d.ts.map +1 -1
- package/dist/bin.js +147 -91
- package/dist/bin.js.map +1 -1
- package/dist/breaker-store.d.ts +60 -9
- package/dist/breaker-store.d.ts.map +1 -1
- package/dist/breaker-store.js +246 -50
- package/dist/breaker-store.js.map +1 -1
- package/dist/circuit-breaker.d.ts +35 -0
- package/dist/circuit-breaker.d.ts.map +1 -1
- package/dist/circuit-breaker.js +68 -11
- package/dist/circuit-breaker.js.map +1 -1
- package/dist/config/coercions.d.ts +31 -0
- package/dist/config/coercions.d.ts.map +1 -0
- package/dist/config/coercions.js +169 -0
- package/dist/config/coercions.js.map +1 -0
- package/dist/config/env-interpolation.d.ts +36 -0
- package/dist/config/env-interpolation.d.ts.map +1 -0
- package/dist/config/env-interpolation.js +73 -0
- package/dist/config/env-interpolation.js.map +1 -0
- package/dist/config/protocol.d.ts +51 -0
- package/dist/config/protocol.d.ts.map +1 -0
- package/dist/config/protocol.js +300 -0
- package/dist/config/protocol.js.map +1 -0
- package/dist/config/route-fields.d.ts +72 -0
- package/dist/config/route-fields.d.ts.map +1 -0
- package/dist/config/route-fields.js +128 -0
- package/dist/config/route-fields.js.map +1 -0
- package/dist/config/validation.d.ts +72 -0
- package/dist/config/validation.d.ts.map +1 -0
- package/dist/config/validation.js +189 -0
- package/dist/config/validation.js.map +1 -0
- package/dist/config.d.ts +19 -2
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +214 -635
- package/dist/config.js.map +1 -1
- package/dist/configure-yaml.d.ts +23 -0
- package/dist/configure-yaml.d.ts.map +1 -0
- package/dist/configure-yaml.js +193 -0
- package/dist/configure-yaml.js.map +1 -0
- package/dist/dispatch-log.d.ts.map +1 -1
- package/dist/dispatch-log.js +2 -3
- package/dist/dispatch-log.js.map +1 -1
- package/dist/dispatchers/base.d.ts +3 -0
- package/dist/dispatchers/base.d.ts.map +1 -1
- package/dist/dispatchers/base.js.map +1 -1
- package/dist/dispatchers/generic-cli.d.ts +15 -1
- package/dist/dispatchers/generic-cli.d.ts.map +1 -1
- package/dist/dispatchers/generic-cli.js +69 -5
- package/dist/dispatchers/generic-cli.js.map +1 -1
- package/dist/dispatchers/openai-compatible.d.ts.map +1 -1
- package/dist/dispatchers/openai-compatible.js +70 -2
- package/dist/dispatchers/openai-compatible.js.map +1 -1
- package/dist/dispatchers/shared/kill-tree.d.ts +17 -4
- package/dist/dispatchers/shared/kill-tree.d.ts.map +1 -1
- package/dist/dispatchers/shared/kill-tree.js +28 -4
- package/dist/dispatchers/shared/kill-tree.js.map +1 -1
- package/dist/dispatchers/shared/rate-limit-headers.d.ts +17 -0
- package/dist/dispatchers/shared/rate-limit-headers.d.ts.map +1 -1
- package/dist/dispatchers/shared/rate-limit-headers.js +27 -0
- package/dist/dispatchers/shared/rate-limit-headers.js.map +1 -1
- package/dist/dispatchers/shared/stream-subprocess.d.ts +11 -0
- package/dist/dispatchers/shared/stream-subprocess.d.ts.map +1 -1
- package/dist/dispatchers/shared/stream-subprocess.js +62 -35
- package/dist/dispatchers/shared/stream-subprocess.js.map +1 -1
- package/dist/dispatchers/shared/subprocess.d.ts.map +1 -1
- package/dist/dispatchers/shared/subprocess.js +3 -0
- package/dist/dispatchers/shared/subprocess.js.map +1 -1
- package/dist/dispatchers/shared/which-available.d.ts.map +1 -1
- package/dist/dispatchers/shared/which-available.js +16 -2
- package/dist/dispatchers/shared/which-available.js.map +1 -1
- package/dist/file-lock.d.ts +17 -0
- package/dist/file-lock.d.ts.map +1 -0
- package/dist/file-lock.js +143 -0
- package/dist/file-lock.js.map +1 -0
- package/dist/http/parse.d.ts +64 -0
- package/dist/http/parse.d.ts.map +1 -0
- package/dist/http/parse.js +230 -0
- package/dist/http/parse.js.map +1 -0
- package/dist/http/server.d.ts +2 -0
- package/dist/http/server.d.ts.map +1 -1
- package/dist/http/server.js +249 -286
- package/dist/http/server.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -2
- package/dist/index.js.map +1 -1
- package/dist/job-runner.js +23 -5
- package/dist/job-runner.js.map +1 -1
- package/dist/jobs/context.d.ts +17 -0
- package/dist/jobs/context.d.ts.map +1 -0
- package/dist/jobs/context.js +84 -0
- package/dist/jobs/context.js.map +1 -0
- package/dist/jobs/store.d.ts +80 -0
- package/dist/jobs/store.d.ts.map +1 -0
- package/dist/jobs/store.js +279 -0
- package/dist/jobs/store.js.map +1 -0
- package/dist/jobs/types.d.ts +135 -0
- package/dist/jobs/types.d.ts.map +1 -0
- package/dist/jobs/types.js +11 -0
- package/dist/jobs/types.js.map +1 -0
- package/dist/jobs.d.ts +165 -85
- package/dist/jobs.d.ts.map +1 -1
- package/dist/jobs.js +824 -208
- package/dist/jobs.js.map +1 -1
- package/dist/leaderboard.d.ts +24 -1
- package/dist/leaderboard.d.ts.map +1 -1
- package/dist/leaderboard.js +94 -2
- package/dist/leaderboard.js.map +1 -1
- package/dist/mcp/config-hot-reload.d.ts.map +1 -1
- package/dist/mcp/config-hot-reload.js +1 -1
- package/dist/mcp/config-hot-reload.js.map +1 -1
- package/dist/mcp/dispatcher-factory.d.ts +16 -1
- package/dist/mcp/dispatcher-factory.d.ts.map +1 -1
- package/dist/mcp/dispatcher-factory.js +41 -3
- package/dist/mcp/dispatcher-factory.js.map +1 -1
- package/dist/mcp/tool-schemas.d.ts +194 -0
- package/dist/mcp/tool-schemas.d.ts.map +1 -0
- package/dist/mcp/tool-schemas.js +311 -0
- package/dist/mcp/tool-schemas.js.map +1 -0
- package/dist/mcp/tools.d.ts +26 -51
- package/dist/mcp/tools.d.ts.map +1 -1
- package/dist/mcp/tools.js +122 -151
- package/dist/mcp/tools.js.map +1 -1
- package/dist/observability/index.d.ts +16 -3
- package/dist/observability/index.d.ts.map +1 -1
- package/dist/observability/index.js +23 -7
- package/dist/observability/index.js.map +1 -1
- package/dist/observability/spans.d.ts.map +1 -1
- package/dist/observability/spans.js +4 -1
- package/dist/observability/spans.js.map +1 -1
- package/dist/quota.d.ts +106 -22
- package/dist/quota.d.ts.map +1 -1
- package/dist/quota.js +198 -58
- package/dist/quota.js.map +1 -1
- package/dist/route-policy.d.ts.map +1 -1
- package/dist/route-policy.js +13 -3
- package/dist/route-policy.js.map +1 -1
- package/dist/router.d.ts +29 -1
- package/dist/router.d.ts.map +1 -1
- package/dist/router.js +102 -40
- package/dist/router.js.map +1 -1
- package/dist/safety.d.ts.map +1 -1
- package/dist/safety.js +11 -2
- package/dist/safety.js.map +1 -1
- package/dist/state-dir.d.ts +17 -0
- package/dist/state-dir.d.ts.map +1 -0
- package/dist/state-dir.js +21 -0
- package/dist/state-dir.js.map +1 -0
- package/dist/status.d.ts +49 -0
- package/dist/status.d.ts.map +1 -1
- package/dist/status.js +67 -4
- package/dist/status.js.map +1 -1
- package/dist/types.d.ts +74 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/working-dir.d.ts +13 -0
- package/dist/working-dir.d.ts.map +1 -1
- package/dist/working-dir.js +27 -0
- package/dist/working-dir.js.map +1 -1
- package/dist/workspace-lock.d.ts +40 -0
- package/dist/workspace-lock.d.ts.map +1 -0
- package/dist/workspace-lock.js +258 -0
- package/dist/workspace-lock.js.map +1 -0
- package/dist/workspace-resolve.d.ts +98 -0
- package/dist/workspace-resolve.d.ts.map +1 -0
- package/dist/workspace-resolve.js +382 -0
- package/dist/workspace-resolve.js.map +1 -0
- package/dist/workspaces.d.ts +16 -0
- package/dist/workspaces.d.ts.map +1 -1
- package/dist/workspaces.js +166 -22
- package/dist/workspaces.js.map +1 -1
- package/package.json +99 -96
- package/dist/dashboard/live.d.ts +0 -49
- package/dist/dashboard/live.d.ts.map +0 -1
- package/dist/dashboard/live.js +0 -149
- package/dist/dashboard/live.js.map +0 -1
package/dist/config.js
CHANGED
|
@@ -17,7 +17,42 @@ import path from "node:path";
|
|
|
17
17
|
import { fileURLToPath } from "node:url";
|
|
18
18
|
import yaml from "js-yaml";
|
|
19
19
|
import which from "which";
|
|
20
|
-
import {
|
|
20
|
+
import { inferredPaidUsagePossible } from "./billing.js";
|
|
21
|
+
import { authSourceFrom, billingKindFrom, bool, endpointModeFrom, endpointProviderFrom, inferEndpointProvider, int, num, providerFrom, str, surfaceFrom, wireProtocolFrom, } from "./config/coercions.js";
|
|
22
|
+
import { warnUnknownRouteKeys, warnUnknownSafetyEnums, warnUnknownTopLevelKeys, } from "./config/validation.js";
|
|
23
|
+
import { parseProtocolFields, protocolFrom, stringArrayFrom } from "./config/protocol.js";
|
|
24
|
+
import { effectiveSafetyFrom, resolveSharedRouteFields, } from "./config/route-fields.js";
|
|
25
|
+
import { interpolateTree } from "./config/env-interpolation.js";
|
|
26
|
+
/**
|
|
27
|
+
* PATH lookups, memoised for the life of the process.
|
|
28
|
+
*
|
|
29
|
+
* loadConfig() runs on every CLI invocation and on every config reload, and
|
|
30
|
+
* each lookup is a real filesystem walk — ~2-3s per harness on Windows. A CLI
|
|
31
|
+
* that resolves once will resolve the same way a second later, and a
|
|
32
|
+
* long-running server re-reads config on hot reload where re-probing bought
|
|
33
|
+
* nothing.
|
|
34
|
+
*
|
|
35
|
+
* Deliberately NOT persisted across processes: installing a harness should
|
|
36
|
+
* take effect on the next command, not after a cache expiry someone has to
|
|
37
|
+
* discover.
|
|
38
|
+
*/
|
|
39
|
+
const whichCache = new Map();
|
|
40
|
+
const defaultWhich = async (cmd) => {
|
|
41
|
+
const cached = whichCache.get(cmd);
|
|
42
|
+
if (cached !== undefined)
|
|
43
|
+
return cached;
|
|
44
|
+
const lookup = (async () => {
|
|
45
|
+
try {
|
|
46
|
+
const r = await which(cmd, { nothrow: true });
|
|
47
|
+
return r ?? null;
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
})();
|
|
53
|
+
whichCache.set(cmd, lookup);
|
|
54
|
+
return lookup;
|
|
55
|
+
};
|
|
21
56
|
// ---------------------------------------------------------------------------
|
|
22
57
|
// Built-in harness defaults — loaded from the package's own bundled
|
|
23
58
|
// config.default.yaml's `clis:` list, NOT hardcoded here. Claude Code,
|
|
@@ -30,15 +65,6 @@ import { normalizeSafetyProfile, } from "./safety.js";
|
|
|
30
65
|
// doesn't matter to JS).
|
|
31
66
|
// ---------------------------------------------------------------------------
|
|
32
67
|
const SAFETY_PROFILES = ["read_only", "workspace_edit", "full_auto"];
|
|
33
|
-
/** The full set of `{{name}}` tokens expandToken() in generic-cli.ts understands. */
|
|
34
|
-
const KNOWN_ARG_PLACEHOLDERS = new Set([
|
|
35
|
-
"{{prompt}}",
|
|
36
|
-
"{{model}}",
|
|
37
|
-
"{{safety}}",
|
|
38
|
-
"{{working_dir}}",
|
|
39
|
-
"{{file_dirs}}",
|
|
40
|
-
"{{native_args}}",
|
|
41
|
-
]);
|
|
42
68
|
/**
|
|
43
69
|
* `harness: generic` is not a harness definition — it's the escape hatch a
|
|
44
70
|
* user's own entry selects when adding a wholly new CLI, so its "defaults"
|
|
@@ -71,6 +97,19 @@ export const PROTOCOL_PRESETS = Object.fromEntries(Object.entries(CLI_DEFAULTS)
|
|
|
71
97
|
// billing.ts/safety.ts/router.ts) and must not change. This mapping only
|
|
72
98
|
// controls what shows up as the service/route name, so it can follow the
|
|
73
99
|
// same `*_cli` convention `endpoints:` uses for `*_api` (e.g. gemini_api).
|
|
100
|
+
/**
|
|
101
|
+
* Commands auto-detect probes on PATH, exported so `doctor` can name them.
|
|
102
|
+
*
|
|
103
|
+
* A zero-route install used to report "0 ready route(s)" and stop, which tells
|
|
104
|
+
* a new user nothing about what was looked for or what to install. Keyed the
|
|
105
|
+
* same way as AUTO_DETECT_NAME so the two cannot drift.
|
|
106
|
+
*/
|
|
107
|
+
export const AUTO_DETECT_COMMANDS = {
|
|
108
|
+
claude_code_cli: "claude",
|
|
109
|
+
codex_cli: "codex",
|
|
110
|
+
cursor_cli: "cursor-agent",
|
|
111
|
+
antigravity_cli: "agy",
|
|
112
|
+
};
|
|
74
113
|
const AUTO_DETECT_NAME = {
|
|
75
114
|
claude_code: "claude_code_cli",
|
|
76
115
|
codex: "codex_cli",
|
|
@@ -80,208 +119,6 @@ const AUTO_DETECT_NAME = {
|
|
|
80
119
|
// ---------------------------------------------------------------------------
|
|
81
120
|
// Env var interpolation (${VAR_NAME})
|
|
82
121
|
// ---------------------------------------------------------------------------
|
|
83
|
-
const ENV_VAR_RE = /^\$\{([A-Za-z_][A-Za-z0-9_]*)\}$/;
|
|
84
|
-
/**
|
|
85
|
-
* `unsetVars` collects the names of ${VAR} references that resolved to
|
|
86
|
-
* nothing because the env var isn't set at all — distinct from a var
|
|
87
|
-
* deliberately set to "". Without this, a typo'd or forgotten env var
|
|
88
|
-
* (${ANTHROPIC_API_KEY} when the real name is ${ANTHROPIC_KEY}) silently
|
|
89
|
-
* becomes an empty string: `str()` then drops the field entirely, a route
|
|
90
|
-
* that needed an api_key loses it with zero feedback, and doctor reports
|
|
91
|
-
* the route "ready" right up until the first real call 401s.
|
|
92
|
-
*/
|
|
93
|
-
function interpolateEnv(value, unsetVars) {
|
|
94
|
-
const m = ENV_VAR_RE.exec(value);
|
|
95
|
-
if (!m)
|
|
96
|
-
return value;
|
|
97
|
-
const name = m[1];
|
|
98
|
-
if (!(name in process.env))
|
|
99
|
-
unsetVars.add(name);
|
|
100
|
-
return process.env[name] ?? "";
|
|
101
|
-
}
|
|
102
|
-
/** Walk an object tree and replace any "${VAR}" string leaves with env values. */
|
|
103
|
-
function interpolateTree(node, unsetVars) {
|
|
104
|
-
if (typeof node === "string") {
|
|
105
|
-
return interpolateEnv(node, unsetVars);
|
|
106
|
-
}
|
|
107
|
-
if (Array.isArray(node)) {
|
|
108
|
-
return node.map((v) => interpolateTree(v, unsetVars));
|
|
109
|
-
}
|
|
110
|
-
if (node !== null && typeof node === "object") {
|
|
111
|
-
const out = {};
|
|
112
|
-
for (const [k, v] of Object.entries(node)) {
|
|
113
|
-
out[k] = interpolateTree(v, unsetVars);
|
|
114
|
-
}
|
|
115
|
-
return out;
|
|
116
|
-
}
|
|
117
|
-
return node;
|
|
118
|
-
}
|
|
119
|
-
const defaultWhich = async (cmd) => {
|
|
120
|
-
try {
|
|
121
|
-
const r = await which(cmd, { nothrow: true });
|
|
122
|
-
return r ?? null;
|
|
123
|
-
}
|
|
124
|
-
catch {
|
|
125
|
-
return null;
|
|
126
|
-
}
|
|
127
|
-
};
|
|
128
|
-
// ---------------------------------------------------------------------------
|
|
129
|
-
// Normalization helpers
|
|
130
|
-
// ---------------------------------------------------------------------------
|
|
131
|
-
function num(v, def) {
|
|
132
|
-
if (typeof v === "number")
|
|
133
|
-
return v;
|
|
134
|
-
if (typeof v === "string" && v !== "") {
|
|
135
|
-
const n = Number(v);
|
|
136
|
-
if (!Number.isNaN(n))
|
|
137
|
-
return n;
|
|
138
|
-
}
|
|
139
|
-
return def;
|
|
140
|
-
}
|
|
141
|
-
function int(v, def) {
|
|
142
|
-
return Math.trunc(num(v, def));
|
|
143
|
-
}
|
|
144
|
-
function bool(v, def) {
|
|
145
|
-
if (typeof v === "boolean")
|
|
146
|
-
return v;
|
|
147
|
-
if (typeof v === "string") {
|
|
148
|
-
if (v === "true")
|
|
149
|
-
return true;
|
|
150
|
-
if (v === "false")
|
|
151
|
-
return false;
|
|
152
|
-
}
|
|
153
|
-
return def;
|
|
154
|
-
}
|
|
155
|
-
function str(v) {
|
|
156
|
-
if (typeof v !== "string")
|
|
157
|
-
return undefined;
|
|
158
|
-
if (v === "")
|
|
159
|
-
return undefined;
|
|
160
|
-
return v;
|
|
161
|
-
}
|
|
162
|
-
function thinkingFrom(v) {
|
|
163
|
-
if (v === "low" || v === "medium" || v === "high")
|
|
164
|
-
return v;
|
|
165
|
-
return undefined;
|
|
166
|
-
}
|
|
167
|
-
function providerFrom(v) {
|
|
168
|
-
if (v === "anthropic" ||
|
|
169
|
-
v === "openai" ||
|
|
170
|
-
v === "cursor" ||
|
|
171
|
-
v === "google" ||
|
|
172
|
-
v === "local" ||
|
|
173
|
-
v === "custom") {
|
|
174
|
-
return v;
|
|
175
|
-
}
|
|
176
|
-
return undefined;
|
|
177
|
-
}
|
|
178
|
-
function surfaceFrom(v) {
|
|
179
|
-
if (v === "claude_code" ||
|
|
180
|
-
v === "claude_agent_sdk" ||
|
|
181
|
-
v === "anthropic_api" ||
|
|
182
|
-
v === "codex_cli" ||
|
|
183
|
-
v === "codex_sdk" ||
|
|
184
|
-
v === "openai_api" ||
|
|
185
|
-
v === "cursor_agent_cli" ||
|
|
186
|
-
v === "antigravity_cli" ||
|
|
187
|
-
v === "gemini_api" ||
|
|
188
|
-
v === "vertex_ai" ||
|
|
189
|
-
v === "openai_compatible" ||
|
|
190
|
-
v === "local_endpoint" ||
|
|
191
|
-
v === "custom") {
|
|
192
|
-
return v;
|
|
193
|
-
}
|
|
194
|
-
return undefined;
|
|
195
|
-
}
|
|
196
|
-
function authSourceFrom(v) {
|
|
197
|
-
if (v === "product_login" ||
|
|
198
|
-
v === "api_key" ||
|
|
199
|
-
v === "oauth_session" ||
|
|
200
|
-
v === "local_network" ||
|
|
201
|
-
v === "configured_endpoint" ||
|
|
202
|
-
v === "unknown") {
|
|
203
|
-
return v;
|
|
204
|
-
}
|
|
205
|
-
return undefined;
|
|
206
|
-
}
|
|
207
|
-
function billingKindFrom(v) {
|
|
208
|
-
if (v === "local_compute" ||
|
|
209
|
-
v === "included_plan_usage" ||
|
|
210
|
-
v === "included_plan_then_flexible_credits" ||
|
|
211
|
-
v === "included_credit_then_optional_overage" ||
|
|
212
|
-
v === "included_usage_then_on_demand" ||
|
|
213
|
-
v === "metered_api" ||
|
|
214
|
-
v === "free_quota" ||
|
|
215
|
-
v === "unknown") {
|
|
216
|
-
return v;
|
|
217
|
-
}
|
|
218
|
-
return undefined;
|
|
219
|
-
}
|
|
220
|
-
function confidenceFrom(v) {
|
|
221
|
-
if (v === "documented" || v === "inferred" || v === "unknown" || v === "unsupported") {
|
|
222
|
-
return v;
|
|
223
|
-
}
|
|
224
|
-
return undefined;
|
|
225
|
-
}
|
|
226
|
-
function endpointModeFrom(v) {
|
|
227
|
-
if (v === "provider_cloud" ||
|
|
228
|
-
v === "direct_openai_compatible" ||
|
|
229
|
-
v === "harness_native_endpoint") {
|
|
230
|
-
return v;
|
|
231
|
-
}
|
|
232
|
-
return undefined;
|
|
233
|
-
}
|
|
234
|
-
function endpointProviderFrom(v) {
|
|
235
|
-
if (v === "ollama" ||
|
|
236
|
-
v === "lmstudio" ||
|
|
237
|
-
v === "openai_compatible" ||
|
|
238
|
-
v === "anthropic_gateway" ||
|
|
239
|
-
v === "gemini_proxy" ||
|
|
240
|
-
v === "custom") {
|
|
241
|
-
return v;
|
|
242
|
-
}
|
|
243
|
-
return undefined;
|
|
244
|
-
}
|
|
245
|
-
function wireProtocolFrom(v) {
|
|
246
|
-
if (v === "openai_chat_completions" ||
|
|
247
|
-
v === "anthropic_messages" ||
|
|
248
|
-
v === "gemini_generate_content" ||
|
|
249
|
-
v === "provider_native" ||
|
|
250
|
-
v === "unknown") {
|
|
251
|
-
return v;
|
|
252
|
-
}
|
|
253
|
-
return undefined;
|
|
254
|
-
}
|
|
255
|
-
function workspacePolicyFrom(v) {
|
|
256
|
-
if (v === "shared" || v === "shared_locked" || v === "git_worktree" || v === "copy") {
|
|
257
|
-
return v;
|
|
258
|
-
}
|
|
259
|
-
return undefined;
|
|
260
|
-
}
|
|
261
|
-
function inferEndpointProvider(baseUrl) {
|
|
262
|
-
if (!baseUrl)
|
|
263
|
-
return "custom";
|
|
264
|
-
try {
|
|
265
|
-
const url = new URL(baseUrl);
|
|
266
|
-
const host = url.hostname.toLowerCase();
|
|
267
|
-
const port = url.port;
|
|
268
|
-
if ((host === "localhost" || host === "127.0.0.1" || host === "::1") && port === "11434") {
|
|
269
|
-
return "ollama";
|
|
270
|
-
}
|
|
271
|
-
if ((host === "localhost" || host === "127.0.0.1" || host === "::1") && port === "1234") {
|
|
272
|
-
return "lmstudio";
|
|
273
|
-
}
|
|
274
|
-
}
|
|
275
|
-
catch {
|
|
276
|
-
// Fall through to substring checks for partial or nonstandard URLs.
|
|
277
|
-
}
|
|
278
|
-
const lower = baseUrl.toLowerCase();
|
|
279
|
-
if (lower.includes("ollama"))
|
|
280
|
-
return "ollama";
|
|
281
|
-
if (lower.includes("lmstudio") || lower.includes("lm-studio"))
|
|
282
|
-
return "lmstudio";
|
|
283
|
-
return "custom";
|
|
284
|
-
}
|
|
285
122
|
function endpointFields(raw, type, baseUrl) {
|
|
286
123
|
const endpointMode = endpointModeFrom(raw.endpoint_mode) ??
|
|
287
124
|
(type === "openai_compatible" ? "direct_openai_compatible" : undefined);
|
|
@@ -306,7 +143,7 @@ function endpointFields(raw, type, baseUrl) {
|
|
|
306
143
|
* `retention:`. Parsed once here so legacy `services:` configs and modern
|
|
307
144
|
* `clis:`/`endpoints:` configs behave identically.
|
|
308
145
|
*/
|
|
309
|
-
function topLevelSettings(raw) {
|
|
146
|
+
function topLevelSettings(raw, policyWarnings = []) {
|
|
310
147
|
const out = {};
|
|
311
148
|
const telemetryRaw = raw.telemetry;
|
|
312
149
|
if (telemetryRaw !== null && typeof telemetryRaw === "object") {
|
|
@@ -320,16 +157,47 @@ function topLevelSettings(raw) {
|
|
|
320
157
|
if (Number.isFinite(days) && days >= 0)
|
|
321
158
|
out.retention = { jobsDays: days };
|
|
322
159
|
}
|
|
160
|
+
const leaderboardRaw = raw.leaderboard;
|
|
161
|
+
if (leaderboardRaw !== null && typeof leaderboardRaw === "object") {
|
|
162
|
+
const enabled = leaderboardRaw.enabled;
|
|
163
|
+
if (typeof enabled === "boolean")
|
|
164
|
+
out.leaderboard = { enabled };
|
|
165
|
+
}
|
|
166
|
+
const maxRuns = num(raw.max_concurrent_runs, Number.NaN);
|
|
167
|
+
if (Number.isFinite(maxRuns) && maxRuns >= 0)
|
|
168
|
+
out.maxConcurrentRuns = Math.floor(maxRuns);
|
|
169
|
+
else if (raw.max_concurrent_runs !== undefined) {
|
|
170
|
+
// Present but unusable. Silently falling back to the default meant a
|
|
171
|
+
// caller who set a concurrency bound got a different one and was never
|
|
172
|
+
// told — and this value governs how many agent CLIs run at once.
|
|
173
|
+
policyWarnings.push(`max_concurrent_runs: ${JSON.stringify(raw.max_concurrent_runs)} is not a ` +
|
|
174
|
+
`non-negative number — IGNORED, the default applies instead.`);
|
|
175
|
+
}
|
|
323
176
|
return out;
|
|
324
177
|
}
|
|
178
|
+
/**
|
|
179
|
+
* `effective_safety` as either one profile or a per-request map.
|
|
180
|
+
*
|
|
181
|
+
* An unrecognised value is dropped rather than guessed at, and an unrecognised
|
|
182
|
+
* KEY or value inside the map is dropped individually — a typo must not
|
|
183
|
+
* silently widen the floor for a request it was meant to restrict.
|
|
184
|
+
*/
|
|
185
|
+
/**
|
|
186
|
+
* Billing IDENTITY for a route — the part that genuinely differs per shape.
|
|
187
|
+
*
|
|
188
|
+
* Everything that means the same thing everywhere (safety_profile,
|
|
189
|
+
* effective_safety, workspace_policy, billing_notes, models, …) now comes from
|
|
190
|
+
* resolveSharedRouteFields instead. What is left here is the set the caller
|
|
191
|
+
* has already inferred per shape: endpoints derive provider/surface/kind from
|
|
192
|
+
* the base URL, CLI routes derive them from whether an api_key is present.
|
|
193
|
+
* This function only reads what it is handed.
|
|
194
|
+
*/
|
|
325
195
|
function billingFields(raw) {
|
|
326
196
|
const out = {};
|
|
327
197
|
const provider = providerFrom(raw.provider);
|
|
328
198
|
const surface = surfaceFrom(raw.surface);
|
|
329
199
|
const authSource = authSourceFrom(raw.auth_source);
|
|
330
200
|
const billingKind = billingKindFrom(raw.billing_kind);
|
|
331
|
-
const billingConfidence = confidenceFrom(raw.billing_confidence);
|
|
332
|
-
const safetyProfile = normalizeSafetyProfile(raw.safety_profile);
|
|
333
201
|
if (provider !== undefined)
|
|
334
202
|
out.provider = provider;
|
|
335
203
|
if (surface !== undefined)
|
|
@@ -338,21 +206,9 @@ function billingFields(raw) {
|
|
|
338
206
|
out.authSource = authSource;
|
|
339
207
|
if (billingKind !== undefined)
|
|
340
208
|
out.billingKind = billingKind;
|
|
341
|
-
if (billingConfidence !== undefined)
|
|
342
|
-
out.billingConfidence = billingConfidence;
|
|
343
209
|
if (typeof raw.paid_usage_possible === "boolean") {
|
|
344
210
|
out.paidUsagePossible = raw.paid_usage_possible;
|
|
345
211
|
}
|
|
346
|
-
if (typeof raw.allow_paid_usage === "boolean")
|
|
347
|
-
out.allowPaidUsage = raw.allow_paid_usage;
|
|
348
|
-
const notes = str(raw.billing_notes);
|
|
349
|
-
if (notes !== undefined)
|
|
350
|
-
out.billingNotes = notes;
|
|
351
|
-
if (safetyProfile !== undefined)
|
|
352
|
-
out.safetyProfile = safetyProfile;
|
|
353
|
-
const workspacePolicy = workspacePolicyFrom(raw.workspace_policy);
|
|
354
|
-
if (workspacePolicy !== undefined)
|
|
355
|
-
out.workspacePolicy = workspacePolicy;
|
|
356
212
|
return out;
|
|
357
213
|
}
|
|
358
214
|
function capsFrom(raw) {
|
|
@@ -374,276 +230,6 @@ function escalateOnFrom(raw) {
|
|
|
374
230
|
}
|
|
375
231
|
return out.length > 0 ? out : ["plan", "review"];
|
|
376
232
|
}
|
|
377
|
-
function stringArrayFrom(raw) {
|
|
378
|
-
if (!Array.isArray(raw))
|
|
379
|
-
return undefined;
|
|
380
|
-
const out = raw.filter((v) => typeof v === "string");
|
|
381
|
-
return out.length > 0 ? out : undefined;
|
|
382
|
-
}
|
|
383
|
-
/**
|
|
384
|
-
* Parse `protocol:` for any route (see CliProtocolConfig in types.ts).
|
|
385
|
-
* Three shapes:
|
|
386
|
-
* - A string ("cursor", "codex", ...) — looked up in PROTOCOL_PRESETS by
|
|
387
|
-
* name. Lets a config select a known protocol without retyping it, and
|
|
388
|
-
* is the extension point for "others add further protocols" — a new
|
|
389
|
-
* named entry in the shipped config.default.yaml is immediately selectable here,
|
|
390
|
-
* no code changes.
|
|
391
|
-
* - An object with `extends: <preset name>` — starts from that preset and
|
|
392
|
-
* overrides only the fields present, for the common "95% the same, one
|
|
393
|
-
* flag different" case. safety merges per-profile (overriding just
|
|
394
|
-
* full_auto doesn't erase read_only/workspace_edit from the preset).
|
|
395
|
-
* - A plain object — the full protocol, no preset involved (unchanged
|
|
396
|
-
* behavior from before presets existed).
|
|
397
|
-
*
|
|
398
|
-
* Returns undefined — with a warning — for anything malformed, so a broken
|
|
399
|
-
* block degrades to "route unusable" (isAvailable() checks for a missing
|
|
400
|
-
* protocol) rather than a half-built dispatcher silently doing the wrong
|
|
401
|
-
* thing.
|
|
402
|
-
*/
|
|
403
|
-
function protocolFrom(raw, routeLabel, warnings) {
|
|
404
|
-
if (typeof raw === "string") {
|
|
405
|
-
const preset = PROTOCOL_PRESETS[raw];
|
|
406
|
-
if (!preset) {
|
|
407
|
-
warnings.push(`${routeLabel}: protocol "${raw}" is not a known preset (expected one of: ` +
|
|
408
|
-
`${Object.keys(PROTOCOL_PRESETS).join(", ")}) — entry ignored.`);
|
|
409
|
-
return undefined;
|
|
410
|
-
}
|
|
411
|
-
return preset;
|
|
412
|
-
}
|
|
413
|
-
if (raw === null || typeof raw !== "object")
|
|
414
|
-
return undefined;
|
|
415
|
-
const r = raw;
|
|
416
|
-
let base;
|
|
417
|
-
if (typeof r.extends === "string") {
|
|
418
|
-
base = PROTOCOL_PRESETS[r.extends];
|
|
419
|
-
if (!base) {
|
|
420
|
-
warnings.push(`${routeLabel}: protocol.extends "${r.extends}" is not a known preset (expected one of: ` +
|
|
421
|
-
`${Object.keys(PROTOCOL_PRESETS).join(", ")}) — entry ignored.`);
|
|
422
|
-
return undefined;
|
|
423
|
-
}
|
|
424
|
-
}
|
|
425
|
-
return parseProtocolFields(r, routeLabel, warnings, base);
|
|
426
|
-
}
|
|
427
|
-
function parseProtocolFields(r, routeLabel, warnings, base) {
|
|
428
|
-
const args = stringArrayFrom(r.args) ?? base?.args;
|
|
429
|
-
if (args === undefined) {
|
|
430
|
-
warnings.push(`${routeLabel}: protocol.args is required — entry ignored.`);
|
|
431
|
-
return undefined;
|
|
432
|
-
}
|
|
433
|
-
const outputRaw = r.output;
|
|
434
|
-
let output = base?.output;
|
|
435
|
-
if (outputRaw !== undefined) {
|
|
436
|
-
if (outputRaw === null || typeof outputRaw !== "object") {
|
|
437
|
-
warnings.push(`${routeLabel}: protocol.output must be an object — entry ignored.`);
|
|
438
|
-
return undefined;
|
|
439
|
-
}
|
|
440
|
-
const o = outputRaw;
|
|
441
|
-
if (o.mode !== "text" && o.mode !== "json_field" && o.mode !== "jsonl_stream") {
|
|
442
|
-
warnings.push(`${routeLabel}: protocol.output.mode must be one of text | json_field | jsonl_stream — entry ignored.`);
|
|
443
|
-
return undefined;
|
|
444
|
-
}
|
|
445
|
-
output = { mode: o.mode };
|
|
446
|
-
const fields = stringArrayFrom(o.fields);
|
|
447
|
-
if (fields !== undefined)
|
|
448
|
-
output.fields = fields;
|
|
449
|
-
const usageRaw = o.usage;
|
|
450
|
-
if (usageRaw !== null && typeof usageRaw === "object") {
|
|
451
|
-
const u = usageRaw;
|
|
452
|
-
const input = stringArrayFrom(u.input);
|
|
453
|
-
const outputTokens = stringArrayFrom(u.output);
|
|
454
|
-
if (input !== undefined && outputTokens !== undefined)
|
|
455
|
-
output.usage = { input, output: outputTokens };
|
|
456
|
-
}
|
|
457
|
-
const eventRulesRaw = o.event_rules;
|
|
458
|
-
if (Array.isArray(eventRulesRaw)) {
|
|
459
|
-
const eventRules = [];
|
|
460
|
-
for (const [i, ruleRaw] of eventRulesRaw.entries()) {
|
|
461
|
-
const rule = eventRuleFrom(ruleRaw, `${routeLabel}: protocol.output.event_rules[${i}]`, warnings);
|
|
462
|
-
if (rule)
|
|
463
|
-
eventRules.push(rule);
|
|
464
|
-
}
|
|
465
|
-
if (eventRules.length > 0)
|
|
466
|
-
output.eventRules = eventRules;
|
|
467
|
-
}
|
|
468
|
-
const errorRaw = o.error;
|
|
469
|
-
if (errorRaw !== null && typeof errorRaw === "object") {
|
|
470
|
-
const e = errorRaw;
|
|
471
|
-
const field = str(e.field);
|
|
472
|
-
if (field !== undefined) {
|
|
473
|
-
const messageFields = stringArrayFrom(e.message_fields);
|
|
474
|
-
output.error = messageFields !== undefined ? { field, messageFields } : { field };
|
|
475
|
-
}
|
|
476
|
-
else {
|
|
477
|
-
warnings.push(`${routeLabel}: protocol.output.error.field is required — error detection ignored.`);
|
|
478
|
-
}
|
|
479
|
-
}
|
|
480
|
-
}
|
|
481
|
-
if (output === undefined) {
|
|
482
|
-
warnings.push(`${routeLabel}: protocol.output is required — entry ignored.`);
|
|
483
|
-
return undefined;
|
|
484
|
-
}
|
|
485
|
-
const protocol = { args, output };
|
|
486
|
-
const stdin = typeof r.stdin === "boolean" ? r.stdin : base?.stdin;
|
|
487
|
-
if (stdin !== undefined)
|
|
488
|
-
protocol.stdin = stdin;
|
|
489
|
-
const modelRaw = r.model;
|
|
490
|
-
if (modelRaw !== undefined && modelRaw !== null) {
|
|
491
|
-
const m = modelRaw;
|
|
492
|
-
if (typeof m.flag === "string" && m.flag) {
|
|
493
|
-
protocol.model = { flag: m.flag };
|
|
494
|
-
}
|
|
495
|
-
else {
|
|
496
|
-
warnings.push(`${routeLabel}: protocol.model set but missing a "flag" string — ignored.`);
|
|
497
|
-
}
|
|
498
|
-
}
|
|
499
|
-
else if (base?.model) {
|
|
500
|
-
protocol.model = base.model;
|
|
501
|
-
}
|
|
502
|
-
const workingDirRaw = r.working_dir;
|
|
503
|
-
if (workingDirRaw !== undefined && workingDirRaw !== null) {
|
|
504
|
-
const wd = workingDirRaw;
|
|
505
|
-
if (typeof wd.flag === "string" && wd.flag) {
|
|
506
|
-
protocol.workingDir = { flag: wd.flag };
|
|
507
|
-
const extraArgsWhenSet = stringArrayFrom(wd.extra_args_when_set);
|
|
508
|
-
if (extraArgsWhenSet !== undefined)
|
|
509
|
-
protocol.workingDir.extraArgsWhenSet = extraArgsWhenSet;
|
|
510
|
-
if (wd.fallback === "home")
|
|
511
|
-
protocol.workingDir.fallback = "home";
|
|
512
|
-
}
|
|
513
|
-
else {
|
|
514
|
-
warnings.push(`${routeLabel}: protocol.working_dir set but missing a "flag" string — ignored.`);
|
|
515
|
-
}
|
|
516
|
-
}
|
|
517
|
-
else if (base?.workingDir) {
|
|
518
|
-
protocol.workingDir = base.workingDir;
|
|
519
|
-
}
|
|
520
|
-
const fileDirsRaw = r.file_dirs;
|
|
521
|
-
if (fileDirsRaw !== undefined && fileDirsRaw !== null) {
|
|
522
|
-
const fd = fileDirsRaw;
|
|
523
|
-
if (typeof fd.flag === "string" && fd.flag) {
|
|
524
|
-
protocol.fileDirs = { flag: fd.flag };
|
|
525
|
-
}
|
|
526
|
-
else {
|
|
527
|
-
warnings.push(`${routeLabel}: protocol.file_dirs set but missing a "flag" string — ignored.`);
|
|
528
|
-
}
|
|
529
|
-
}
|
|
530
|
-
else if (base?.fileDirs) {
|
|
531
|
-
protocol.fileDirs = base.fileDirs;
|
|
532
|
-
}
|
|
533
|
-
const fileListHeader = str(r.file_list_header) ?? base?.fileListHeader;
|
|
534
|
-
if (fileListHeader !== undefined)
|
|
535
|
-
protocol.fileListHeader = fileListHeader;
|
|
536
|
-
const fileListBullet = (typeof r.file_list_bullet === "string" ? r.file_list_bullet : undefined) ?? base?.fileListBullet;
|
|
537
|
-
if (fileListBullet !== undefined)
|
|
538
|
-
protocol.fileListBullet = fileListBullet;
|
|
539
|
-
const apiKeyEnvVar = str(r.api_key_env_var) ?? base?.apiKeyEnvVar;
|
|
540
|
-
if (apiKeyEnvVar !== undefined)
|
|
541
|
-
protocol.apiKeyEnvVar = apiKeyEnvVar;
|
|
542
|
-
const safetyRaw = r.safety;
|
|
543
|
-
const safety = { ...base?.safety };
|
|
544
|
-
if (safetyRaw !== null && typeof safetyRaw === "object") {
|
|
545
|
-
for (const profile of SAFETY_PROFILES) {
|
|
546
|
-
const profileArgs = stringArrayFrom(safetyRaw[profile]);
|
|
547
|
-
if (profileArgs !== undefined)
|
|
548
|
-
safety[profile] = profileArgs;
|
|
549
|
-
}
|
|
550
|
-
}
|
|
551
|
-
if (Object.keys(safety).length > 0)
|
|
552
|
-
protocol.safety = safety;
|
|
553
|
-
if (typeof r.success_requires_output === "boolean") {
|
|
554
|
-
protocol.successRequiresOutput = r.success_requires_output;
|
|
555
|
-
}
|
|
556
|
-
else if (base?.successRequiresOutput !== undefined) {
|
|
557
|
-
protocol.successRequiresOutput = base.successRequiresOutput;
|
|
558
|
-
}
|
|
559
|
-
const endpointNativeArgsRaw = r.endpoint_native_args;
|
|
560
|
-
if (endpointNativeArgsRaw !== null && typeof endpointNativeArgsRaw === "object") {
|
|
561
|
-
const ena = {};
|
|
562
|
-
for (const [k, v] of Object.entries(endpointNativeArgsRaw)) {
|
|
563
|
-
const args2 = stringArrayFrom(v);
|
|
564
|
-
if (args2 !== undefined)
|
|
565
|
-
ena[k] = args2;
|
|
566
|
-
}
|
|
567
|
-
if (Object.keys(ena).length > 0)
|
|
568
|
-
protocol.endpointNativeArgs = ena;
|
|
569
|
-
}
|
|
570
|
-
else if (base?.endpointNativeArgs) {
|
|
571
|
-
protocol.endpointNativeArgs = base.endpointNativeArgs;
|
|
572
|
-
}
|
|
573
|
-
// Placeholder sanity checks on the FINAL merged args (so `extends:` results
|
|
574
|
-
// are covered too). A typo'd placeholder is the most likely user error in a
|
|
575
|
-
// hand-written protocol, and without these warnings it doesn't just fail
|
|
576
|
-
// silently — it "succeeds": the CLI receives the literal "{{promt}}" token,
|
|
577
|
-
// never receives the prompt, exits 0, and the run reports ok.
|
|
578
|
-
// Matches embedded forms too ("--flag={{prompt}}"), not just whole-token
|
|
579
|
-
// typos — expansion only ever substitutes a token that IS a placeholder,
|
|
580
|
-
// so anything merely containing one goes through literally.
|
|
581
|
-
for (const token of protocol.args) {
|
|
582
|
-
if (token.includes("{{") && !KNOWN_ARG_PLACEHOLDERS.has(token)) {
|
|
583
|
-
warnings.push(`${routeLabel}: protocol.args contains unrecognized placeholder "${token}" — it will be ` +
|
|
584
|
-
`passed to the CLI as a literal argument, not substituted. Placeholders only work as ` +
|
|
585
|
-
`a whole standalone argument. Known placeholders: ` +
|
|
586
|
-
`${[...KNOWN_ARG_PLACEHOLDERS].join(", ")}.`);
|
|
587
|
-
}
|
|
588
|
-
}
|
|
589
|
-
if (!protocol.stdin && !protocol.args.includes("{{prompt}}")) {
|
|
590
|
-
warnings.push(`${routeLabel}: protocol.args has no {{prompt}} placeholder and stdin is not true — ` +
|
|
591
|
-
`the prompt is never sent to the CLI. Add "{{prompt}}" to args, or set stdin: true.`);
|
|
592
|
-
}
|
|
593
|
-
return protocol;
|
|
594
|
-
}
|
|
595
|
-
function eventRuleFrom(raw, label, warnings) {
|
|
596
|
-
if (raw === null || typeof raw !== "object") {
|
|
597
|
-
warnings.push(`${label}: must be an object — ignored.`);
|
|
598
|
-
return undefined;
|
|
599
|
-
}
|
|
600
|
-
const r = raw;
|
|
601
|
-
// "when" is optional — omitted or {} means "matches every line" (e.g. a
|
|
602
|
-
// usage rule that should fire regardless of event type, matching Codex's
|
|
603
|
-
// original unconditional `if (event.usage) {...}` check).
|
|
604
|
-
const whenRaw = r.when ?? {};
|
|
605
|
-
if (whenRaw === null || typeof whenRaw !== "object" || Array.isArray(whenRaw)) {
|
|
606
|
-
warnings.push(`${label}: "when" must be a {field: value} map — ignored.`);
|
|
607
|
-
return undefined;
|
|
608
|
-
}
|
|
609
|
-
const when = {};
|
|
610
|
-
for (const [k, v] of Object.entries(whenRaw)) {
|
|
611
|
-
if (typeof v === "string")
|
|
612
|
-
when[k] = v;
|
|
613
|
-
}
|
|
614
|
-
const emit = r.emit;
|
|
615
|
-
if (emit !== "text" &&
|
|
616
|
-
emit !== "tool_use" &&
|
|
617
|
-
emit !== "thinking" &&
|
|
618
|
-
emit !== "usage" &&
|
|
619
|
-
emit !== "error") {
|
|
620
|
-
warnings.push(`${label}: "emit" must be one of text | tool_use | thinking | usage | error — ignored.`);
|
|
621
|
-
return undefined;
|
|
622
|
-
}
|
|
623
|
-
const rule = { when, emit };
|
|
624
|
-
const textField = str(r.text_field);
|
|
625
|
-
if (textField !== undefined)
|
|
626
|
-
rule.textField = textField;
|
|
627
|
-
const nameField = str(r.name_field);
|
|
628
|
-
if (nameField !== undefined)
|
|
629
|
-
rule.nameField = nameField;
|
|
630
|
-
const inputField = str(r.input_field);
|
|
631
|
-
if (inputField !== undefined)
|
|
632
|
-
rule.inputField = inputField;
|
|
633
|
-
const chunkField = str(r.chunk_field);
|
|
634
|
-
if (chunkField !== undefined)
|
|
635
|
-
rule.chunkField = chunkField;
|
|
636
|
-
const inputTokenFields = stringArrayFrom(r.input_token_fields);
|
|
637
|
-
if (inputTokenFields !== undefined)
|
|
638
|
-
rule.inputTokenFields = inputTokenFields;
|
|
639
|
-
const outputTokenFields = stringArrayFrom(r.output_token_fields);
|
|
640
|
-
if (outputTokenFields !== undefined)
|
|
641
|
-
rule.outputTokenFields = outputTokenFields;
|
|
642
|
-
const messageField = str(r.message_field);
|
|
643
|
-
if (messageField !== undefined)
|
|
644
|
-
rule.messageField = messageField;
|
|
645
|
-
return rule;
|
|
646
|
-
}
|
|
647
233
|
// ---------------------------------------------------------------------------
|
|
648
234
|
// Legacy full-format parser (YAML with top-level `services:` key)
|
|
649
235
|
// ---------------------------------------------------------------------------
|
|
@@ -685,31 +271,11 @@ function buildLegacyConfig(raw) {
|
|
|
685
271
|
tier: int(svc.tier, 1),
|
|
686
272
|
weight: num(svc.weight, 1.0),
|
|
687
273
|
cliCapability: num(svc.cli_capability, 1.0),
|
|
688
|
-
...(str(svc.leaderboard_model) !== undefined
|
|
689
|
-
? { leaderboardModel: str(svc.leaderboard_model) }
|
|
690
|
-
: {}),
|
|
691
|
-
...(() => {
|
|
692
|
-
const t = thinkingFrom(svc.thinking_level);
|
|
693
|
-
return t !== undefined ? { thinkingLevel: t } : {};
|
|
694
|
-
})(),
|
|
695
|
-
...(str(svc.escalate_model) !== undefined
|
|
696
|
-
? { escalateModel: str(svc.escalate_model) }
|
|
697
|
-
: {}),
|
|
698
274
|
escalateOn: escalateOnFrom(svc.escalate_on),
|
|
699
275
|
capabilities: capsFrom(svc.capabilities),
|
|
700
276
|
...(() => {
|
|
701
|
-
//
|
|
702
|
-
//
|
|
703
|
-
// a known harness (e.g. antigravity_cli's 2M context) must still
|
|
704
|
-
// inherit them — otherwise the boost silently becomes a no-op.
|
|
705
|
-
const v = typeof svc.max_output_tokens === "number" ? svc.max_output_tokens : harnessDefaults?.maxOutputTokens;
|
|
706
|
-
return v !== undefined ? { maxOutputTokens: v } : {};
|
|
707
|
-
})(),
|
|
708
|
-
...(() => {
|
|
709
|
-
const v = typeof svc.max_input_tokens === "number" ? svc.max_input_tokens : harnessDefaults?.maxInputTokens;
|
|
710
|
-
return v !== undefined ? { maxInputTokens: v } : {};
|
|
711
|
-
})(),
|
|
712
|
-
...(() => {
|
|
277
|
+
// Billing IDENTITY only — the rest of the named harness's defaults
|
|
278
|
+
// reach this entry through resolveSharedRouteFields below.
|
|
713
279
|
if (!harnessDefaults)
|
|
714
280
|
return {};
|
|
715
281
|
return {
|
|
@@ -722,37 +288,26 @@ function buildLegacyConfig(raw) {
|
|
|
722
288
|
...(harnessDefaults.paidUsagePossible !== undefined
|
|
723
289
|
? { paidUsagePossible: harnessDefaults.paidUsagePossible }
|
|
724
290
|
: {}),
|
|
725
|
-
...(harnessDefaults.effectiveSafety !== undefined
|
|
726
|
-
? { effectiveSafety: harnessDefaults.effectiveSafety }
|
|
727
|
-
: {}),
|
|
728
|
-
...(harnessDefaults.models !== undefined ? { models: harnessDefaults.models } : {}),
|
|
729
|
-
...(harnessDefaults.modelHint !== undefined
|
|
730
|
-
? { modelHint: harnessDefaults.modelHint }
|
|
731
|
-
: {}),
|
|
732
291
|
};
|
|
733
292
|
})(),
|
|
734
293
|
...billingFields(svc),
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
...(
|
|
744
|
-
const modelHint = str(svc.model_hint);
|
|
745
|
-
return modelHint !== undefined ? { modelHint } : {};
|
|
746
|
-
})(),
|
|
294
|
+
// The same table the `clis:` and `endpoints:` builders use. This shape
|
|
295
|
+
// previously read the shared keys through billingFields() plus four
|
|
296
|
+
// hand-written blocks, which is how it ended up with a DIFFERENT set
|
|
297
|
+
// again: it inherited maxInput/OutputTokens, effectiveSafety, models and
|
|
298
|
+
// modelHint from the named harness but not leaderboardModel or
|
|
299
|
+
// thinkingLevel, so `services: { x: { harness: antigravity_cli } }`
|
|
300
|
+
// silently lost its scoring key and its thinking level. Passing the
|
|
301
|
+
// defaults through one table fixes that too.
|
|
302
|
+
...resolveSharedRouteFields(svc, harnessDefaults),
|
|
747
303
|
...endpointFields(svc, type, str(svc.base_url)),
|
|
748
|
-
...(typeof svc.timeout_ms === "number" ? { timeoutMs: svc.timeout_ms } : {}),
|
|
749
304
|
...(() => {
|
|
750
305
|
// Falls back to the named harness's built-in default protocol (if
|
|
751
306
|
// any) when this legacy-format entry doesn't declare its own —
|
|
752
307
|
// same behavior as clis: entries, so `harness: claude_code` here
|
|
753
308
|
// works without repeating the whole protocol block.
|
|
754
309
|
const harnessDefaults = CLI_DEFAULTS[str(svc.harness) ?? ""];
|
|
755
|
-
const protocol = protocolFrom(svc.protocol, `services "${name}"`, warnings) ?? harnessDefaults?.protocol;
|
|
310
|
+
const protocol = protocolFrom(svc.protocol, `services "${name}"`, warnings, PROTOCOL_PRESETS) ?? harnessDefaults?.protocol;
|
|
756
311
|
return protocol !== undefined ? { protocol } : {};
|
|
757
312
|
})(),
|
|
758
313
|
};
|
|
@@ -763,7 +318,7 @@ function buildLegacyConfig(raw) {
|
|
|
763
318
|
...(Array.isArray(raw.disabled)
|
|
764
319
|
? { disabled: raw.disabled.slice() }
|
|
765
320
|
: {}),
|
|
766
|
-
...topLevelSettings(raw),
|
|
321
|
+
...topLevelSettings(raw, warnings),
|
|
767
322
|
...(warnings.length > 0 ? { configWarnings: warnings } : {}),
|
|
768
323
|
};
|
|
769
324
|
return cfg;
|
|
@@ -801,31 +356,13 @@ function buildCliServiceConfig(name, defaults, override, apiKeys, warnings = [])
|
|
|
801
356
|
weight: num(override.weight, 1.0),
|
|
802
357
|
tier: int(override.tier, defaults.tier),
|
|
803
358
|
cliCapability: num(override.cli_capability, defaults.cliCapability),
|
|
804
|
-
leaderboardModel: str(override.leaderboard_model) ?? defaults.leaderboardModel,
|
|
805
|
-
...(() => {
|
|
806
|
-
const overrideThinking = thinkingFrom(override.thinking_level);
|
|
807
|
-
if (overrideThinking !== undefined)
|
|
808
|
-
return { thinkingLevel: overrideThinking };
|
|
809
|
-
if (defaults.thinkingLevel !== undefined) {
|
|
810
|
-
return { thinkingLevel: defaults.thinkingLevel };
|
|
811
|
-
}
|
|
812
|
-
return {};
|
|
813
|
-
})(),
|
|
814
|
-
...(str(override.escalate_model) !== undefined
|
|
815
|
-
? { escalateModel: str(override.escalate_model) }
|
|
816
|
-
: {}),
|
|
817
359
|
escalateOn: escalateOnFrom(override.escalate_on),
|
|
818
360
|
capabilities: caps,
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
...(() => {
|
|
825
|
-
const m = override.max_input_tokens;
|
|
826
|
-
const v = typeof m === "number" ? m : defaults.maxInputTokens;
|
|
827
|
-
return v !== undefined ? { maxInputTokens: v } : {};
|
|
828
|
-
})(),
|
|
361
|
+
// Every field that means the same thing on every route shape, resolved
|
|
362
|
+
// from ONE table against this harness's shipped defaults. Spread early so
|
|
363
|
+
// the shape-specific resolutions below (billing identity, which depends on
|
|
364
|
+
// whether an api_key is present) still win.
|
|
365
|
+
...resolveSharedRouteFields(override, defaults),
|
|
829
366
|
provider: providerFrom(override.provider) ?? defaults.provider,
|
|
830
367
|
surface: surfaceFrom(override.surface) ?? defaults.surface,
|
|
831
368
|
authSource: authSourceFrom(override.auth_source) ?? (apiKey ? "api_key" : defaults.authSource),
|
|
@@ -834,48 +371,36 @@ function buildCliServiceConfig(name, defaults, override, apiKeys, warnings = [])
|
|
|
834
371
|
return billingKind !== undefined ? { billingKind } : {};
|
|
835
372
|
})(),
|
|
836
373
|
...(() => {
|
|
374
|
+
// A DECLARED billing_kind beats the harness default.
|
|
375
|
+
//
|
|
376
|
+
// `harness: generic` defaults paidUsagePossible to true (correctly — an
|
|
377
|
+
// unknown command might cost money). But a route declaring
|
|
378
|
+
// `billing_kind: local_compute` has said it cannot, and the default
|
|
379
|
+
// still won: status showed `billing=local_compute paid=possible`, two
|
|
380
|
+
// fields of the same record contradicting each other, and the route was
|
|
381
|
+
// skipped by billing policy. Nothing about a declared non-paid kind
|
|
382
|
+
// should leave the paid flag set by a fallback.
|
|
383
|
+
//
|
|
384
|
+
// An explicit paid_usage_possible still wins over both, and an api_key
|
|
385
|
+
// still forces true — a key means a metered account exists regardless of
|
|
386
|
+
// what the kind claims.
|
|
387
|
+
const declaredKind = billingKindFrom(override.billing_kind);
|
|
837
388
|
const paidUsagePossible = typeof override.paid_usage_possible === "boolean"
|
|
838
389
|
? override.paid_usage_possible
|
|
839
390
|
: apiKey
|
|
840
391
|
? true
|
|
841
|
-
:
|
|
392
|
+
: declaredKind !== undefined
|
|
393
|
+
? inferredPaidUsagePossible(declaredKind)
|
|
394
|
+
: defaults.paidUsagePossible;
|
|
842
395
|
return paidUsagePossible !== undefined ? { paidUsagePossible } : {};
|
|
843
396
|
})(),
|
|
844
|
-
...(typeof override.allow_paid_usage === "boolean"
|
|
845
|
-
? { allowPaidUsage: override.allow_paid_usage }
|
|
846
|
-
: {}),
|
|
847
|
-
...(() => {
|
|
848
|
-
const c = confidenceFrom(override.billing_confidence);
|
|
849
|
-
return c !== undefined ? { billingConfidence: c } : {};
|
|
850
|
-
})(),
|
|
851
|
-
...(() => {
|
|
852
|
-
const notes = str(override.billing_notes);
|
|
853
|
-
return notes !== undefined ? { billingNotes: notes } : {};
|
|
854
|
-
})(),
|
|
855
|
-
...(() => {
|
|
856
|
-
const safetyProfile = normalizeSafetyProfile(override.safety_profile);
|
|
857
|
-
return safetyProfile !== undefined ? { safetyProfile } : {};
|
|
858
|
-
})(),
|
|
859
|
-
...(() => {
|
|
860
|
-
const effectiveSafety = normalizeSafetyProfile(override.effective_safety) ?? defaults.effectiveSafety;
|
|
861
|
-
return effectiveSafety !== undefined ? { effectiveSafety } : {};
|
|
862
|
-
})(),
|
|
863
|
-
...(() => {
|
|
864
|
-
const models = stringArrayFrom(override.models) ?? defaults.models;
|
|
865
|
-
return models !== undefined ? { models } : {};
|
|
866
|
-
})(),
|
|
867
|
-
...(() => {
|
|
868
|
-
const modelHint = str(override.model_hint) ?? defaults.modelHint;
|
|
869
|
-
return modelHint !== undefined ? { modelHint } : {};
|
|
870
|
-
})(),
|
|
871
397
|
...endpointFields(override, "cli", str(override.base_url)),
|
|
872
|
-
...(typeof override.timeout_ms === "number" ? { timeoutMs: override.timeout_ms } : {}),
|
|
873
398
|
...(() => {
|
|
874
399
|
// Falls back to this harness's built-in default (if any) when no
|
|
875
400
|
// override is given, or when the override is malformed — protocolFrom
|
|
876
401
|
// already warned in the latter case; failing the whole route over a
|
|
877
402
|
// typo'd override would be worse than keeping the known-good default.
|
|
878
|
-
const protocol = protocolFrom(override.protocol, `clis "${name}"`, warnings) ?? defaults.protocol;
|
|
403
|
+
const protocol = protocolFrom(override.protocol, `clis "${name}"`, warnings, PROTOCOL_PRESETS) ?? defaults.protocol;
|
|
879
404
|
return protocol !== undefined ? { protocol } : {};
|
|
880
405
|
})(),
|
|
881
406
|
};
|
|
@@ -883,19 +408,32 @@ function buildCliServiceConfig(name, defaults, override, apiKeys, warnings = [])
|
|
|
883
408
|
async function detectServices(disabled, apiKeys, overrides, whichFn) {
|
|
884
409
|
const services = {};
|
|
885
410
|
const disabledSet = new Set(disabled);
|
|
886
|
-
|
|
411
|
+
// Probe every harness AT ONCE.
|
|
412
|
+
//
|
|
413
|
+
// This was a sequential await per harness. Each `which` costs real time on
|
|
414
|
+
// Windows — measured 2.8s / 3.3s / 2.7s / 2.1s for claude / codex /
|
|
415
|
+
// cursor-agent / agy on a machine where all four are installed, so ~11s per
|
|
416
|
+
// loadConfig() call, and loadConfig runs on every CLI invocation. `status`
|
|
417
|
+
// and `doctor` took ~17s, and the test suite went red with timeouts on any
|
|
418
|
+
// developer machine that actually has the harnesses installed — it passed in
|
|
419
|
+
// CI only because CI is bare. The probes are independent, so there was never
|
|
420
|
+
// a reason to serialise them.
|
|
421
|
+
const candidates = Object.entries(CLI_DEFAULTS)
|
|
887
422
|
// "generic" has no installable binary of its own — it exists only for
|
|
888
423
|
// explicit clis: entries (addClis), never auto-detection.
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
424
|
+
.filter(([harness]) => harness !== "generic")
|
|
425
|
+
.map(([harness, defaults]) => ({
|
|
426
|
+
harness,
|
|
427
|
+
defaults,
|
|
428
|
+
name: AUTO_DETECT_NAME[harness] ?? harness,
|
|
429
|
+
}))
|
|
430
|
+
.filter(({ name }) => !disabledSet.has(name));
|
|
431
|
+
const found = await Promise.all(candidates.map(async (c) => ((await whichFn(c.defaults.command)) ? c : undefined)));
|
|
432
|
+
for (const c of found) {
|
|
433
|
+
if (!c)
|
|
896
434
|
continue;
|
|
897
|
-
const override = overrides[name] ?? {};
|
|
898
|
-
services[name] = buildCliServiceConfig(name, defaults, override, apiKeys);
|
|
435
|
+
const override = overrides[c.name] ?? {};
|
|
436
|
+
services[c.name] = buildCliServiceConfig(c.name, c.defaults, override, apiKeys);
|
|
899
437
|
}
|
|
900
438
|
return services;
|
|
901
439
|
}
|
|
@@ -917,6 +455,7 @@ function addClis(services, raw, apiKeys, warnings) {
|
|
|
917
455
|
warnings.push(`clis[${index}]: missing required "name" and/or "harness" — entry ignored.`);
|
|
918
456
|
continue;
|
|
919
457
|
}
|
|
458
|
+
warnUnknownRouteKeys(entry, `clis[${index}] "${name}"`, warnings);
|
|
920
459
|
const defaults = CLI_DEFAULTS[harness];
|
|
921
460
|
if (!defaults) {
|
|
922
461
|
warnings.push(`clis[${index}] "${name}": unrecognized harness "${harness}" (expected one of: ` +
|
|
@@ -943,7 +482,7 @@ function addClis(services, raw, apiKeys, warnings) {
|
|
|
943
482
|
// once, from here, since the entry gets skipped before that second
|
|
944
483
|
// parse ever runs.
|
|
945
484
|
const validation = [];
|
|
946
|
-
if (protocolFrom(entry.protocol, `clis[${index}] "${name}"`, validation) === undefined) {
|
|
485
|
+
if (protocolFrom(entry.protocol, `clis[${index}] "${name}"`, validation, PROTOCOL_PRESETS) === undefined) {
|
|
947
486
|
warnings.push(...validation);
|
|
948
487
|
continue;
|
|
949
488
|
}
|
|
@@ -968,16 +507,29 @@ function collectApiKeys(raw) {
|
|
|
968
507
|
}
|
|
969
508
|
return apiKeys;
|
|
970
509
|
}
|
|
971
|
-
function addEndpoints(services, raw) {
|
|
510
|
+
function addEndpoints(services, raw, apiKeys, warnings = []) {
|
|
972
511
|
const endpoints = Array.isArray(raw.endpoints)
|
|
973
512
|
? raw.endpoints
|
|
974
513
|
: [];
|
|
975
|
-
for (const ep of endpoints) {
|
|
514
|
+
for (const [index, ep] of endpoints.entries()) {
|
|
976
515
|
const name = str(ep.name);
|
|
516
|
+
warnUnknownRouteKeys(ep, `endpoints[${index}] "${name ?? "?"}"`, warnings);
|
|
977
517
|
const baseUrl = str(ep.base_url);
|
|
978
518
|
const model = str(ep.model);
|
|
979
|
-
if (!name || !baseUrl || !model)
|
|
519
|
+
if (!name || !baseUrl || !model) {
|
|
520
|
+
// Silently dropping the entry was the same class this file keeps
|
|
521
|
+
// producing: the equivalent `clis:` mistake warns loudly, this one left
|
|
522
|
+
// `doctor` reporting "ok config-warnings" while three endpoints had
|
|
523
|
+
// vanished.
|
|
524
|
+
const missing = [
|
|
525
|
+
!name ? "name" : undefined,
|
|
526
|
+
!baseUrl ? "base_url" : undefined,
|
|
527
|
+
!model ? "model" : undefined,
|
|
528
|
+
].filter((v) => v !== undefined);
|
|
529
|
+
warnings.push(`endpoints[${index}]${name ? ` "${name}"` : ""}: missing required ` +
|
|
530
|
+
`${missing.join(", ")} — entry ignored.`);
|
|
980
531
|
continue;
|
|
532
|
+
}
|
|
981
533
|
const svc = {
|
|
982
534
|
name,
|
|
983
535
|
enabled: bool(ep.enabled, true),
|
|
@@ -985,13 +537,20 @@ function addEndpoints(services, raw) {
|
|
|
985
537
|
baseUrl,
|
|
986
538
|
model,
|
|
987
539
|
command: "",
|
|
988
|
-
|
|
540
|
+
// The top-level `api_keys:` block was honoured for `clis:` (see
|
|
541
|
+
// buildCliServiceConfig) and silently ignored here, so an endpoint whose
|
|
542
|
+
// credential lived there had NO key at runtime — and `configure` could
|
|
543
|
+
// not round-trip a reference that had never reached the service. Same
|
|
544
|
+
// class as workspace_policy: a documented key read for one route shape
|
|
545
|
+
// and dropped for another.
|
|
546
|
+
...(str(ep.api_key) !== undefined
|
|
547
|
+
? { apiKey: str(ep.api_key) }
|
|
548
|
+
: apiKeys[name]
|
|
549
|
+
? { apiKey: apiKeys[name] }
|
|
550
|
+
: {}),
|
|
989
551
|
weight: num(ep.weight, 0.6),
|
|
990
552
|
tier: int(ep.tier, 3),
|
|
991
553
|
cliCapability: num(ep.cli_capability, 1.0),
|
|
992
|
-
...(str(ep.leaderboard_model) !== undefined
|
|
993
|
-
? { leaderboardModel: str(ep.leaderboard_model) }
|
|
994
|
-
: {}),
|
|
995
554
|
escalateOn: escalateOnFrom(ep.escalate_on),
|
|
996
555
|
capabilities: capsFrom(ep.capabilities),
|
|
997
556
|
...billingFields({
|
|
@@ -1008,21 +567,10 @@ function addEndpoints(services, raw) {
|
|
|
1008
567
|
billing_confidence: ep.billing_confidence ?? (inferEndpointProvider(baseUrl) === "custom" ? undefined : "documented"),
|
|
1009
568
|
}),
|
|
1010
569
|
...endpointFields(ep, "openai_compatible", baseUrl),
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
...(
|
|
1015
|
-
const t = thinkingFrom(ep.thinking_level);
|
|
1016
|
-
return t !== undefined ? { thinkingLevel: t } : {};
|
|
1017
|
-
})(),
|
|
1018
|
-
...(() => {
|
|
1019
|
-
const models = stringArrayFrom(ep.models);
|
|
1020
|
-
return models !== undefined ? { models } : {};
|
|
1021
|
-
})(),
|
|
1022
|
-
...(() => {
|
|
1023
|
-
const modelHint = str(ep.model_hint);
|
|
1024
|
-
return modelHint !== undefined ? { modelHint } : {};
|
|
1025
|
-
})(),
|
|
570
|
+
// No defaults argument: an endpoint has no harness whose shipped
|
|
571
|
+
// defaults it could fall back to. That absence is exactly why the CLI
|
|
572
|
+
// path could not simply reuse billingFields() — see route-fields.ts.
|
|
573
|
+
...resolveSharedRouteFields(ep),
|
|
1026
574
|
};
|
|
1027
575
|
services[name] = svc;
|
|
1028
576
|
}
|
|
@@ -1040,19 +588,41 @@ export async function loadConfig(path, opts = {}) {
|
|
|
1040
588
|
const whichFn = opts.whichFn ?? defaultWhich;
|
|
1041
589
|
let raw = {};
|
|
1042
590
|
const unsetEnvVars = new Set();
|
|
591
|
+
const envRefs = new Map();
|
|
1043
592
|
if (path) {
|
|
1044
593
|
try {
|
|
1045
594
|
const text = await fs.readFile(path, "utf-8");
|
|
1046
595
|
const parsed = yaml.load(text);
|
|
1047
596
|
if (parsed && typeof parsed === "object") {
|
|
1048
|
-
raw = interpolateTree(parsed, unsetEnvVars);
|
|
597
|
+
raw = interpolateTree(parsed, unsetEnvVars, envRefs);
|
|
1049
598
|
}
|
|
1050
599
|
}
|
|
1051
600
|
catch (err) {
|
|
1052
|
-
// File not found -> auto-detect mode. Any other error -> rethrow.
|
|
1053
601
|
const e = err;
|
|
1054
|
-
if (e.code
|
|
602
|
+
if (e.code === "ENOENT") {
|
|
603
|
+
// configure names an OUTPUT path, so a file that is not there yet is
|
|
604
|
+
// its normal first run — it alone passes allowMissing.
|
|
605
|
+
if (opts.allowMissing === true) {
|
|
606
|
+
// Fall through to auto-detect with an empty `raw`.
|
|
607
|
+
}
|
|
608
|
+
else {
|
|
609
|
+
// Otherwise an explicit --config that does not exist is a typo, not
|
|
610
|
+
// a request for auto-detection. Continuing printed a confident,
|
|
611
|
+
// healthy route table built from defaults, so a mistyped path looked
|
|
612
|
+
// like a working config. The implicit fallback (no path given at
|
|
613
|
+
// all) never reaches here and is unchanged.
|
|
614
|
+
throw new Error(`config file not found: ${path}. Check the path, or omit --config to ` +
|
|
615
|
+
`auto-detect installed harness CLIs.`);
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
else if (err instanceof yaml.YAMLException) {
|
|
619
|
+
// A YAML syntax error used to escape as a raw js-yaml stack trace that
|
|
620
|
+
// never named the file it came from.
|
|
621
|
+
throw new Error(`config file ${path} is not valid YAML: ${err.message}`);
|
|
622
|
+
}
|
|
623
|
+
else {
|
|
1055
624
|
throw err;
|
|
625
|
+
}
|
|
1056
626
|
}
|
|
1057
627
|
}
|
|
1058
628
|
const envVarWarning = unsetEnvVars.size > 0
|
|
@@ -1063,10 +633,16 @@ export async function loadConfig(path, opts = {}) {
|
|
|
1063
633
|
// Legacy full format: has a `services:` key -> use as-is.
|
|
1064
634
|
if (raw.services && typeof raw.services === "object") {
|
|
1065
635
|
const legacyCfg = buildLegacyConfig(raw);
|
|
636
|
+
const enumWarnings = [];
|
|
637
|
+
warnUnknownSafetyEnums(raw, enumWarnings);
|
|
638
|
+
if (enumWarnings.length > 0) {
|
|
639
|
+
legacyCfg.configWarnings = [...(legacyCfg.configWarnings ?? []), ...enumWarnings];
|
|
640
|
+
}
|
|
641
|
+
const withRefs = envRefs.size > 0 ? { ...legacyCfg, envRefs } : legacyCfg;
|
|
1066
642
|
if (envVarWarning !== undefined) {
|
|
1067
|
-
return { ...
|
|
643
|
+
return { ...withRefs, configWarnings: [...(withRefs.configWarnings ?? []), envVarWarning] };
|
|
1068
644
|
}
|
|
1069
|
-
return
|
|
645
|
+
return withRefs;
|
|
1070
646
|
}
|
|
1071
647
|
const disabled = Array.isArray(raw.disabled)
|
|
1072
648
|
? raw.disabled.slice()
|
|
@@ -1090,13 +666,16 @@ export async function loadConfig(path, opts = {}) {
|
|
|
1090
666
|
const apiKeys = collectApiKeys(raw);
|
|
1091
667
|
const services = await detectServices(disabled, apiKeys, overrides, whichFn);
|
|
1092
668
|
addClis(services, raw, apiKeys, warnings);
|
|
1093
|
-
addEndpoints(services, raw);
|
|
669
|
+
addEndpoints(services, raw, apiKeys, warnings);
|
|
670
|
+
warnUnknownSafetyEnums(raw, warnings);
|
|
671
|
+
warnUnknownTopLevelKeys(raw, warnings);
|
|
1094
672
|
if (envVarWarning !== undefined)
|
|
1095
673
|
warnings.push(envVarWarning);
|
|
1096
674
|
const cfg = {
|
|
1097
675
|
services,
|
|
1098
676
|
disabled,
|
|
1099
|
-
...topLevelSettings(raw),
|
|
677
|
+
...topLevelSettings(raw, warnings),
|
|
678
|
+
...(envRefs.size > 0 ? { envRefs } : {}),
|
|
1100
679
|
...(warnings.length > 0 ? { configWarnings: warnings } : {}),
|
|
1101
680
|
};
|
|
1102
681
|
return cfg;
|
|
@@ -1196,7 +775,7 @@ function cliDefaultsFrom(raw, warnings) {
|
|
|
1196
775
|
...(billingKind !== undefined ? { billingKind } : {}),
|
|
1197
776
|
...(typeof raw.paid_usage_possible === "boolean" ? { paidUsagePossible: raw.paid_usage_possible } : {}),
|
|
1198
777
|
...(() => {
|
|
1199
|
-
const effectiveSafety =
|
|
778
|
+
const effectiveSafety = effectiveSafetyFrom(raw.effective_safety);
|
|
1200
779
|
return effectiveSafety !== undefined ? { effectiveSafety } : {};
|
|
1201
780
|
})(),
|
|
1202
781
|
...(() => {
|