harness-dispatch 0.4.0 → 0.5.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/README.md +228 -86
- 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 +253 -23
- 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/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 +229 -511
- package/dist/config.js.map +1 -1
- 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/generic-cli.d.ts +15 -1
- package/dist/dispatchers/generic-cli.d.ts.map +1 -1
- package/dist/dispatchers/generic-cli.js +65 -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 +52 -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.map +1 -1
- package/dist/dispatchers/shared/stream-subprocess.js +5 -0
- 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/server.d.ts +11 -0
- package/dist/http/server.d.ts.map +1 -1
- package/dist/http/server.js +331 -162
- package/dist/http/server.js.map +1 -1
- package/dist/job-runner.js +23 -5
- package/dist/job-runner.js.map +1 -1
- package/dist/jobs.d.ts +91 -1
- package/dist/jobs.d.ts.map +1 -1
- package/dist/jobs.js +647 -36
- 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/tools.d.ts +23 -4
- package/dist/mcp/tools.d.ts.map +1 -1
- package/dist/mcp/tools.js +163 -16
- 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 +88 -22
- package/dist/quota.d.ts.map +1 -1
- package/dist/quota.js +163 -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 +25 -1
- package/dist/router.d.ts.map +1 -1
- package/dist/router.js +94 -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 +36 -0
- package/dist/status.d.ts.map +1 -1
- package/dist/status.js +61 -4
- package/dist/status.js.map +1 -1
- package/dist/types.d.ts +50 -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/workspaces.d.ts +15 -0
- package/dist/workspaces.d.ts.map +1 -1
- package/dist/workspaces.js +104 -12
- package/dist/workspaces.js.map +1 -1
- package/package.json +98 -96
package/dist/auth.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../src/auth.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../src/auth.ts"],"names":[],"mappings":"AASA,wBAAgB,OAAO,IAAI,MAAM,CAEhC;AAED,wBAAgB,SAAS,IAAI,MAAM,CAElC;AAED,wBAAgB,iBAAiB,IAAI,MAAM,CAE1C;AAED,wBAAsB,aAAa,IAAI,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAS5D;AAED,wBAAsB,eAAe,IAAI,OAAO,CAAC,MAAM,CAAC,CAOvD;AAED,wBAAsB,eAAe,IAAI,OAAO,CAAC,MAAM,CAAC,CAKvD;AAmBD,wBAAgB,YAAY,CAC1B,mBAAmB,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,SAAS,EAClD,KAAK,EAAE,MAAM,GAAG,IAAI,GACnB,OAAO,CAOT;AAED,wBAAgB,SAAS,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAG/C"}
|
package/dist/auth.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { randomBytes, timingSafeEqual } from "node:crypto";
|
|
2
2
|
import { promises as fs } from "node:fs";
|
|
3
|
-
import os from "node:os";
|
|
4
3
|
import path from "node:path";
|
|
4
|
+
import { stateRoot } from "./state-dir.js";
|
|
5
5
|
const TOKEN_ENV = "HARNESS_DISPATCH_HTTP_TOKEN";
|
|
6
6
|
export function authDir() {
|
|
7
|
-
return process.env.HARNESS_DISPATCH_HOME ??
|
|
7
|
+
return process.env.HARNESS_DISPATCH_HOME ?? stateRoot();
|
|
8
8
|
}
|
|
9
9
|
export function tokenPath() {
|
|
10
10
|
return path.join(authDir(), "http-token");
|
package/dist/auth.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"auth.js","sourceRoot":"","sources":["../src/auth.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAC3D,OAAO,EAAE,QAAQ,IAAI,EAAE,EAAE,MAAM,SAAS,CAAC;
|
|
1
|
+
{"version":3,"file":"auth.js","sourceRoot":"","sources":["../src/auth.ts"],"names":[],"mappings":"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"}
|
package/dist/billing.d.ts
CHANGED
|
@@ -1,7 +1,38 @@
|
|
|
1
|
-
import type { RouteBilling, ServiceConfig } from "./types.js";
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
import type { BillingKind, RouteBilling, ServiceConfig } from "./types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Whether a route can incur a REAL charge with no further action from the
|
|
4
|
+
* user, by default — i.e. whether harness-dispatch should block it until
|
|
5
|
+
* explicitly allowed.
|
|
6
|
+
*
|
|
7
|
+
* "included_X_then_optional_Y" kinds (Codex flexible credits, Claude usage
|
|
8
|
+
* credits, Cursor on-demand) are NOT blocked by default: researched across
|
|
9
|
+
* Anthropic, OpenAI, and Cursor (2026-07), all three hard-stop at the
|
|
10
|
+
* included cap by default — continuing past it requires the user to have
|
|
11
|
+
* ALREADY completed a separate, deliberate opt-in on the PROVIDER's own
|
|
12
|
+
* side (enabling usage credits/flexible pricing/on-demand billing, usually
|
|
13
|
+
* with its own payment method and spend limit). harness-dispatch blocking
|
|
14
|
+
* these by default would just be re-gating something the provider already
|
|
15
|
+
* gates, and asking every user to prove a negative ("I haven't opted into
|
|
16
|
+
* my provider's overage") for a state that's off by default anyway.
|
|
17
|
+
*
|
|
18
|
+
* A user who HAS enabled provider-side overage can still restore the block
|
|
19
|
+
* by setting `paid_usage_possible: true` explicitly in that route's config
|
|
20
|
+
* — the override in buildRouteBilling() takes precedence over this
|
|
21
|
+
* function. Only kinds with no such provider-side backstop — metered_api
|
|
22
|
+
* (raw API keys bill from the first token, no included pool at all) and
|
|
23
|
+
* unknown (no data to reason about) — are blocked by default here.
|
|
24
|
+
*/
|
|
25
|
+
export declare function inferredPaidUsagePossible(kind: BillingKind): boolean;
|
|
26
|
+
/**
|
|
27
|
+
* Classification is date-independent by construction.
|
|
28
|
+
*
|
|
29
|
+
* This took an `opts.now` that was threaded into inferredKind/defaultNotes
|
|
30
|
+
* and used by neither — a leftover from when the Agent SDK credit split was
|
|
31
|
+
* expected to change classification on 2026-06-15. Anthropic paused that on
|
|
32
|
+
* launch day, the date logic was removed, and the parameter stayed behind
|
|
33
|
+
* implying a time dependency that no longer exists.
|
|
34
|
+
*/
|
|
35
|
+
export declare function buildRouteBilling(svc: ServiceConfig): RouteBilling;
|
|
5
36
|
export declare function billingIsUnknown(billing: RouteBilling): boolean;
|
|
6
37
|
export declare function billingIsBlocked(billing: RouteBilling): boolean;
|
|
7
38
|
//# sourceMappingURL=billing.d.ts.map
|
package/dist/billing.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"billing.d.ts","sourceRoot":"","sources":["../src/billing.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,
|
|
1
|
+
{"version":3,"file":"billing.d.ts","sourceRoot":"","sources":["../src/billing.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAGV,WAAW,EAGX,YAAY,EACZ,aAAa,EACd,MAAM,YAAY,CAAC;AA0IpB;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,wBAAgB,yBAAyB,CAAC,IAAI,EAAE,WAAW,GAAG,OAAO,CAapE;AAuBD;;;;;;;;GAQG;AACH,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,aAAa,GAAG,YAAY,CAoBlE;AAED,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,YAAY,GAAG,OAAO,CAE/D;AAED,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,YAAY,GAAG,OAAO,CAI/D"}
|
package/dist/billing.js
CHANGED
|
@@ -1,17 +1,40 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Parse a base_url into (hostname, port), or undefined if it isn't a URL.
|
|
3
|
+
*
|
|
4
|
+
* Both predicates below used `String.includes`, which is not a host check:
|
|
5
|
+
* `https://evil.example.com/proxy?upstream=localhost:11434/v1` contains
|
|
6
|
+
* "localhost:11434" and so classified as free local compute — provider
|
|
7
|
+
* "local", kind "local_compute", paidUsagePossible false. That also exempted
|
|
8
|
+
* it from the caller-supplied `local_only` and `approval_required` policies,
|
|
9
|
+
* because route-policy.ts's isLocalRoute ORs those same four fields.
|
|
10
|
+
*
|
|
11
|
+
* Requires a hostile or mistaken config entry, so it is not remotely
|
|
12
|
+
* triggerable — but "is this host local" is exactly the question a substring
|
|
13
|
+
* cannot answer. config.ts:inferEndpointProvider already did this correctly;
|
|
14
|
+
* this is the same approach applied to the two predicates that did not.
|
|
15
|
+
*/
|
|
16
|
+
function hostOf(baseUrl) {
|
|
2
17
|
if (!baseUrl)
|
|
3
|
-
return
|
|
4
|
-
|
|
5
|
-
baseUrl
|
|
6
|
-
|
|
18
|
+
return undefined;
|
|
19
|
+
try {
|
|
20
|
+
const url = new URL(baseUrl);
|
|
21
|
+
return { host: url.hostname.toLowerCase(), port: url.port };
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
return undefined;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
const LOOPBACK_HOSTS = new Set(["localhost", "127.0.0.1", "::1", "[::1]"]);
|
|
28
|
+
function isLoopback(baseUrl) {
|
|
29
|
+
const parsed = hostOf(baseUrl);
|
|
30
|
+
return parsed !== undefined && LOOPBACK_HOSTS.has(parsed.host);
|
|
7
31
|
}
|
|
32
|
+
/** Ollama (11434) and LM Studio (1234) on loopback — the two runtimes we can name. */
|
|
8
33
|
function isKnownLocalRuntime(baseUrl) {
|
|
9
|
-
|
|
34
|
+
const parsed = hostOf(baseUrl);
|
|
35
|
+
if (parsed === undefined || !LOOPBACK_HOSTS.has(parsed.host))
|
|
10
36
|
return false;
|
|
11
|
-
return
|
|
12
|
-
baseUrl.includes("127.0.0.1:11434") ||
|
|
13
|
-
baseUrl.includes("localhost:1234") ||
|
|
14
|
-
baseUrl.includes("127.0.0.1:1234"));
|
|
37
|
+
return parsed.port === "11434" || parsed.port === "1234";
|
|
15
38
|
}
|
|
16
39
|
// NOTE: no harness-name special cases here. Built-in harnesses declare
|
|
17
40
|
// provider/surface/auth_source/billing_kind in the shipped config.default.yaml,
|
|
@@ -58,7 +81,7 @@ function authSourceFromService(svc, surface) {
|
|
|
58
81
|
}
|
|
59
82
|
return "unknown";
|
|
60
83
|
}
|
|
61
|
-
function inferredKind(svc, surface, authSource
|
|
84
|
+
function inferredKind(svc, surface, authSource) {
|
|
62
85
|
if (svc.billingKind)
|
|
63
86
|
return svc.billingKind;
|
|
64
87
|
if (authSource === "api_key")
|
|
@@ -136,7 +159,7 @@ function inferredConfidence(svc, surface, kind) {
|
|
|
136
159
|
* (raw API keys bill from the first token, no included pool at all) and
|
|
137
160
|
* unknown (no data to reason about) — are blocked by default here.
|
|
138
161
|
*/
|
|
139
|
-
function inferredPaidUsagePossible(kind) {
|
|
162
|
+
export function inferredPaidUsagePossible(kind) {
|
|
140
163
|
switch (kind) {
|
|
141
164
|
case "local_compute":
|
|
142
165
|
case "included_plan_usage":
|
|
@@ -150,7 +173,7 @@ function inferredPaidUsagePossible(kind) {
|
|
|
150
173
|
return true;
|
|
151
174
|
}
|
|
152
175
|
}
|
|
153
|
-
function defaultNotes(svc, surface, kind
|
|
176
|
+
function defaultNotes(svc, surface, kind) {
|
|
154
177
|
if (svc.billingNotes)
|
|
155
178
|
return svc.billingNotes;
|
|
156
179
|
if (surface === "claude_agent_sdk" || surface === "claude_code") {
|
|
@@ -165,12 +188,20 @@ function defaultNotes(svc, surface, kind, now) {
|
|
|
165
188
|
}
|
|
166
189
|
return undefined;
|
|
167
190
|
}
|
|
168
|
-
|
|
169
|
-
|
|
191
|
+
/**
|
|
192
|
+
* Classification is date-independent by construction.
|
|
193
|
+
*
|
|
194
|
+
* This took an `opts.now` that was threaded into inferredKind/defaultNotes
|
|
195
|
+
* and used by neither — a leftover from when the Agent SDK credit split was
|
|
196
|
+
* expected to change classification on 2026-06-15. Anthropic paused that on
|
|
197
|
+
* launch day, the date logic was removed, and the parameter stayed behind
|
|
198
|
+
* implying a time dependency that no longer exists.
|
|
199
|
+
*/
|
|
200
|
+
export function buildRouteBilling(svc) {
|
|
170
201
|
const provider = providerFromService(svc);
|
|
171
202
|
const surface = surfaceFromService(svc);
|
|
172
203
|
const authSource = authSourceFromService(svc, surface);
|
|
173
|
-
const kind = inferredKind(svc, surface, authSource
|
|
204
|
+
const kind = inferredKind(svc, surface, authSource);
|
|
174
205
|
const confidence = inferredConfidence(svc, surface, kind);
|
|
175
206
|
const paidUsagePossible = svc.paidUsagePossible ?? inferredPaidUsagePossible(kind);
|
|
176
207
|
const billing = {
|
|
@@ -183,7 +214,7 @@ export function buildRouteBilling(svc, opts = {}) {
|
|
|
183
214
|
paidUsageRequiresOptIn: paidUsagePossible ? true : false,
|
|
184
215
|
confidence,
|
|
185
216
|
};
|
|
186
|
-
const notes = defaultNotes(svc, surface, kind
|
|
217
|
+
const notes = defaultNotes(svc, surface, kind);
|
|
187
218
|
if (notes !== undefined)
|
|
188
219
|
billing.notes = notes;
|
|
189
220
|
return billing;
|
package/dist/billing.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"billing.js","sourceRoot":"","sources":["../src/billing.ts"],"names":[],"mappings":"AAUA,SAAS,
|
|
1
|
+
{"version":3,"file":"billing.js","sourceRoot":"","sources":["../src/billing.ts"],"names":[],"mappings":"AAUA;;;;;;;;;;;;;;GAcG;AACH,SAAS,MAAM,CAAC,OAA2B;IACzC,IAAI,CAAC,OAAO;QAAE,OAAO,SAAS,CAAC;IAC/B,IAAI,CAAC;QACH,MAAM,GAAG,GAAG,IAAI,GAAG,CAAC,OAAO,CAAC,CAAC;QAC7B,OAAO,EAAE,IAAI,EAAE,GAAG,CAAC,QAAQ,CAAC,WAAW,EAAE,EAAE,IAAI,EAAE,GAAG,CAAC,IAAI,EAAE,CAAC;IAC9D,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,SAAS,CAAC;IACnB,CAAC;AACH,CAAC;AAED,MAAM,cAAc,GAAG,IAAI,GAAG,CAAC,CAAC,WAAW,EAAE,WAAW,EAAE,KAAK,EAAE,OAAO,CAAC,CAAC,CAAC;AAE3E,SAAS,UAAU,CAAC,OAA2B;IAC7C,MAAM,MAAM,GAAG,MAAM,CAAC,OAAO,CAAC,CAAC;IAC/B,OAAO,MAAM,KAAK,SAAS,IAAI,cAAc,CAAC,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;AACjE,CAAC;AAED,sFAAsF;AACtF,SAAS,mBAAmB,CAAC,OAA2B;IACtD,MAAM,MAAM,GAAG,MAAM,CAAC,OAAO,CAAC,CAAC;IAC/B,IAAI,MAAM,KAAK,SAAS,IAAI,CAAC,cAAc,CAAC,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC;QAAE,OAAO,KAAK,CAAC;IAC3E,OAAO,MAAM,CAAC,IAAI,KAAK,OAAO,IAAI,MAAM,CAAC,IAAI,KAAK,MAAM,CAAC;AAC3D,CAAC;AAED,uEAAuE;AACvE,gFAAgF;AAChF,8EAA8E;AAC9E,0EAA0E;AAC1E,6EAA6E;AAC7E,4CAA4C;AAE5C,SAAS,mBAAmB,CAAC,GAAkB;IAC7C,IAAI,GAAG,CAAC,QAAQ;QAAE,OAAO,GAAG,CAAC,QAAQ,CAAC;IACtC,IAAI,GAAG,CAAC,IAAI,KAAK,mBAAmB,IAAI,mBAAmB,CAAC,GAAG,CAAC,OAAO,CAAC;QAAE,OAAO,OAAO,CAAC;IACzF,IAAI,GAAG,CAAC,OAAO,EAAE,QAAQ,CAAC,gBAAgB,CAAC;QAAE,OAAO,QAAQ,CAAC;IAC7D,OAAO,QAAQ,CAAC;AAClB,CAAC;AAED,SAAS,kBAAkB,CAAC,GAAkB;IAC5C,IAAI,GAAG,CAAC,OAAO;QAAE,OAAO,GAAG,CAAC,OAAO,CAAC;IACpC,IAAI,GAAG,CAAC,IAAI,KAAK,mBAAmB,EAAE,CAAC;QACrC,IAAI,GAAG,CAAC,OAAO,EAAE,QAAQ,CAAC,gBAAgB,CAAC;YAAE,OAAO,YAAY,CAAC;QACjE,IAAI,mBAAmB,CAAC,GAAG,CAAC,OAAO,CAAC;YAAE,OAAO,gBAAgB,CAAC;QAC9D,OAAO,mBAAmB,CAAC;IAC7B,CAAC;IACD,OAAO,QAAQ,CAAC;AAClB,CAAC;AAED,SAAS,qBAAqB,CAAC,GAAkB,EAAE,OAAuB;IACxE,IAAI,GAAG,CAAC,UAAU;QAAE,OAAO,GAAG,CAAC,UAAU,CAAC;IAC1C,IAAI,GAAG,CAAC,MAAM;QAAE,OAAO,SAAS,CAAC;IACjC,IAAI,OAAO,KAAK,gBAAgB;QAAE,OAAO,eAAe,CAAC;IACzD,IAAI,GAAG,CAAC,IAAI,KAAK,mBAAmB;QAAE,OAAO,qBAAqB,CAAC;IACnE,IAAI,OAAO,KAAK,kBAAkB;QAAE,OAAO,eAAe,CAAC;IAC3D,IACE,OAAO,KAAK,WAAW;QACvB,OAAO,KAAK,kBAAkB;QAC9B,OAAO,KAAK,iBAAiB,EAC7B,CAAC;QACD,OAAO,eAAe,CAAC;IACzB,CAAC;IACD,OAAO,SAAS,CAAC;AACnB,CAAC;AAED,SAAS,YAAY,CACnB,GAAkB,EAClB,OAAuB,EACvB,UAAsB;IAEtB,IAAI,GAAG,CAAC,WAAW;QAAE,OAAO,GAAG,CAAC,WAAW,CAAC;IAC5C,IAAI,UAAU,KAAK,SAAS;QAAE,OAAO,aAAa,CAAC;IACnD,QAAQ,OAAO,EAAE,CAAC;QAChB,KAAK,kBAAkB,CAAC;QACxB,KAAK,aAAa;YAChB,qEAAqE;YACrE,mEAAmE;YACnE,mEAAmE;YACnE,kEAAkE;YAClE,+DAA+D;YAC/D,2CAA2C;YAC3C,0DAA0D;YAC1D,OAAO,qBAAqB,CAAC;QAC/B,KAAK,WAAW,CAAC;QACjB,KAAK,WAAW;YACd,OAAO,qCAAqC,CAAC;QAC/C,KAAK,kBAAkB;YACrB,OAAO,+BAA+B,CAAC;QACzC,KAAK,iBAAiB;YACpB,OAAO,YAAY,CAAC;QACtB,KAAK,gBAAgB;YACnB,OAAO,eAAe,CAAC;QACzB,KAAK,YAAY,CAAC;QAClB,KAAK,eAAe,CAAC;QACrB,KAAK,YAAY,CAAC;QAClB,KAAK,WAAW;YACd,OAAO,aAAa,CAAC;QACvB,KAAK,mBAAmB;YACtB,OAAO,UAAU,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,aAAa,CAAC;QAC7D,KAAK,QAAQ;YACX,OAAO,SAAS,CAAC;IACrB,CAAC;AACH,CAAC;AAED,SAAS,kBAAkB,CACzB,GAAkB,EAClB,OAAuB,EACvB,IAAiB;IAEjB,IAAI,GAAG,CAAC,iBAAiB;QAAE,OAAO,GAAG,CAAC,iBAAiB,CAAC;IACxD,IAAI,IAAI,KAAK,SAAS;QAAE,OAAO,SAAS,CAAC;IACzC,wEAAwE;IACxE,oEAAoE;IACpE,sEAAsE;IACtE,iEAAiE;IACjE,IAAI,GAAG,CAAC,WAAW;QAAE,OAAO,YAAY,CAAC;IACzC,IAAI,OAAO,KAAK,mBAAmB,IAAI,UAAU,CAAC,GAAG,CAAC,OAAO,CAAC;QAAE,OAAO,SAAS,CAAC;IACjF,IAAI,OAAO,KAAK,QAAQ;QAAE,OAAO,SAAS,CAAC;IAC3C,IAAI,OAAO,KAAK,kBAAkB,IAAI,GAAG,CAAC,UAAU,KAAK,SAAS;QAAE,OAAO,UAAU,CAAC;IACtF,OAAO,YAAY,CAAC;AACtB,CAAC;AAED;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,MAAM,UAAU,yBAAyB,CAAC,IAAiB;IACzD,QAAQ,IAAI,EAAE,CAAC;QACb,KAAK,eAAe,CAAC;QACrB,KAAK,qBAAqB,CAAC;QAC3B,KAAK,YAAY,CAAC;QAClB,KAAK,qCAAqC,CAAC;QAC3C,KAAK,uCAAuC,CAAC;QAC7C,KAAK,+BAA+B;YAClC,OAAO,KAAK,CAAC;QACf,KAAK,aAAa,CAAC;QACnB,KAAK,SAAS;YACZ,OAAO,IAAI,CAAC;IAChB,CAAC;AACH,CAAC;AAED,SAAS,YAAY,CACnB,GAAkB,EAClB,OAAuB,EACvB,IAAiB;IAEjB,IAAI,GAAG,CAAC,YAAY;QAAE,OAAO,GAAG,CAAC,YAAY,CAAC;IAC9C,IAAI,OAAO,KAAK,kBAAkB,IAAI,OAAO,KAAK,aAAa,EAAE,CAAC;QAChE,wEAAwE;QACxE,gEAAgE;QAChE,OAAO,CACL,uEAAuE;YACvE,uEAAuE;YACvE,mCAAmC,CACpC,CAAC;IACJ,CAAC;IACD,IAAI,OAAO,KAAK,mBAAmB,IAAI,UAAU,CAAC,GAAG,CAAC,OAAO,CAAC,IAAI,IAAI,KAAK,SAAS,EAAE,CAAC;QACrF,OAAO,oFAAoF,CAAC;IAC9F,CAAC;IACD,OAAO,SAAS,CAAC;AACnB,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,UAAU,iBAAiB,CAAC,GAAkB;IAClD,MAAM,QAAQ,GAAG,mBAAmB,CAAC,GAAG,CAAC,CAAC;IAC1C,MAAM,OAAO,GAAG,kBAAkB,CAAC,GAAG,CAAC,CAAC;IACxC,MAAM,UAAU,GAAG,qBAAqB,CAAC,GAAG,EAAE,OAAO,CAAC,CAAC;IACvD,MAAM,IAAI,GAAG,YAAY,CAAC,GAAG,EAAE,OAAO,EAAE,UAAU,CAAC,CAAC;IACpD,MAAM,UAAU,GAAG,kBAAkB,CAAC,GAAG,EAAE,OAAO,EAAE,IAAI,CAAC,CAAC;IAC1D,MAAM,iBAAiB,GAAG,GAAG,CAAC,iBAAiB,IAAI,yBAAyB,CAAC,IAAI,CAAC,CAAC;IACnF,MAAM,OAAO,GAAiB;QAC5B,QAAQ;QACR,OAAO;QACP,UAAU;QACV,IAAI;QACJ,iBAAiB;QACjB,cAAc,EAAE,GAAG,CAAC,cAAc,IAAI,KAAK;QAC3C,sBAAsB,EAAE,iBAAiB,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,KAAK;QACxD,UAAU;KACX,CAAC;IACF,MAAM,KAAK,GAAG,YAAY,CAAC,GAAG,EAAE,OAAO,EAAE,IAAI,CAAC,CAAC;IAC/C,IAAI,KAAK,KAAK,SAAS;QAAE,OAAO,CAAC,KAAK,GAAG,KAAK,CAAC;IAC/C,OAAO,OAAO,CAAC;AACjB,CAAC;AAED,MAAM,UAAU,gBAAgB,CAAC,OAAqB;IACpD,OAAO,OAAO,CAAC,IAAI,KAAK,SAAS,IAAI,OAAO,CAAC,UAAU,KAAK,SAAS,CAAC;AACxE,CAAC;AAED,MAAM,UAAU,gBAAgB,CAAC,OAAqB;IACpD,IAAI,gBAAgB,CAAC,OAAO,CAAC;QAAE,OAAO,CAAC,OAAO,CAAC,cAAc,CAAC;IAC9D,IAAI,OAAO,CAAC,iBAAiB;QAAE,OAAO,CAAC,OAAO,CAAC,cAAc,CAAC;IAC9D,OAAO,KAAK,CAAC;AACf,CAAC"}
|
package/dist/bin.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"bin.d.ts","sourceRoot":"","sources":["../src/bin.ts"],"names":[],"mappings":";AACA;;GAEG;
|
|
1
|
+
{"version":3,"file":"bin.d.ts","sourceRoot":"","sources":["../src/bin.ts"],"names":[],"mappings":";AACA;;GAEG;AA6sBH,wBAAsB,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC,CAiH1D"}
|
package/dist/bin.js
CHANGED
|
@@ -2,13 +2,13 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* harness-dispatch CLI entrypoint.
|
|
4
4
|
*/
|
|
5
|
-
import { existsSync } from "node:fs";
|
|
5
|
+
import { existsSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
|
|
6
6
|
import { promises as fs } from "node:fs";
|
|
7
7
|
import path from "node:path";
|
|
8
8
|
import { parseArgs } from "node:util";
|
|
9
9
|
import yaml from "js-yaml";
|
|
10
10
|
import { ensureHttpToken, maskToken, readHttpToken, rotateHttpToken, tokenPath } from "./auth.js";
|
|
11
|
-
import { loadConfig } from "./config.js";
|
|
11
|
+
import { AUTO_DETECT_COMMANDS, loadConfig } from "./config.js";
|
|
12
12
|
import { LeaderboardCache } from "./leaderboard.js";
|
|
13
13
|
import { buildDispatchers } from "./mcp/dispatcher-factory.js";
|
|
14
14
|
import { startMcpServer } from "./mcp/server.js";
|
|
@@ -19,11 +19,14 @@ import { buildStatus, buildUsage, renderStatusText, renderUsageText } from "./st
|
|
|
19
19
|
import { startHttpServer } from "./http/server.js";
|
|
20
20
|
import { billingIsBlocked, buildRouteBilling } from "./billing.js";
|
|
21
21
|
import { effectiveSafetyProfile } from "./safety.js";
|
|
22
|
+
import { stateRoot } from "./state-dir.js";
|
|
22
23
|
async function buildRuntime(configPath) {
|
|
23
24
|
const config = await loadConfig(configPath);
|
|
24
25
|
const dispatchers = await buildDispatchers(config);
|
|
25
26
|
const quota = new QuotaCache(dispatchers);
|
|
26
|
-
const leaderboard = new LeaderboardCache(
|
|
27
|
+
const leaderboard = new LeaderboardCache(undefined, {
|
|
28
|
+
enabled: config.leaderboard?.enabled === true,
|
|
29
|
+
});
|
|
27
30
|
const router = new Router(config, quota, dispatchers, leaderboard);
|
|
28
31
|
return { config, dispatchers, quota, leaderboard, router };
|
|
29
32
|
}
|
|
@@ -52,6 +55,7 @@ function printUsage() {
|
|
|
52
55
|
" --json Print JSON where supported.",
|
|
53
56
|
" --print configure: print generated config YAML without writing it.",
|
|
54
57
|
" --yes configure: write config.yaml instead of only previewing it.",
|
|
58
|
+
" --force configure: overwrite an existing config file.",
|
|
55
59
|
" --allow-paid Allow doctor --live to probe paid or unknown-paid routes.",
|
|
56
60
|
" -h, --help Show help.",
|
|
57
61
|
"",
|
|
@@ -86,6 +90,7 @@ function commonEntryFields(svc) {
|
|
|
86
90
|
escalate_model: svc.escalateModel,
|
|
87
91
|
escalate_on: svc.escalateOn.length > 0 ? svc.escalateOn : undefined,
|
|
88
92
|
capabilities: Object.keys(svc.capabilities).length > 0 ? svc.capabilities : undefined,
|
|
93
|
+
timeout_ms: svc.timeoutMs,
|
|
89
94
|
max_output_tokens: svc.maxOutputTokens,
|
|
90
95
|
max_input_tokens: svc.maxInputTokens,
|
|
91
96
|
allow_paid_usage: svc.allowPaidUsage ? true : undefined,
|
|
@@ -99,33 +104,131 @@ function commonEntryFields(svc) {
|
|
|
99
104
|
model_hint: svc.modelHint,
|
|
100
105
|
};
|
|
101
106
|
}
|
|
102
|
-
|
|
107
|
+
/**
|
|
108
|
+
* Render a route's api_key WITHOUT materialising the secret.
|
|
109
|
+
*
|
|
110
|
+
* `svc.apiKey` is the RESOLVED value — config.ts interpolates `${VAR}` at
|
|
111
|
+
* load time, so by here the reference is gone. Emitting it verbatim wrote
|
|
112
|
+
* live credentials into config.yaml and echoed them to stdout on
|
|
113
|
+
* `configure --print`, which is documented as the safe preview and is exactly
|
|
114
|
+
* what someone pastes into a bug report. It also broke the project's own
|
|
115
|
+
* stated invariant (plugin/commands/setup.md: "API keys MUST be written as
|
|
116
|
+
* ${ENV_VAR} references — never literal").
|
|
117
|
+
*
|
|
118
|
+
* `config.envRefs` maps the resolved value back to the reference that
|
|
119
|
+
* produced it, so a key that came from `${GROQ_API_KEY}` round-trips exactly.
|
|
120
|
+
*
|
|
121
|
+
* A key written as a LITERAL in the source file has no reference to restore.
|
|
122
|
+
* That case splits by destination: `--yes` writes to disk, where the literal
|
|
123
|
+
* already lives and dropping it would break a working config, so it is
|
|
124
|
+
* preserved; `--print` goes to a terminal and a bug report, so it is redacted
|
|
125
|
+
* to the placeholder below.
|
|
126
|
+
*/
|
|
127
|
+
function apiKeyForYaml(svc, config, opts) {
|
|
128
|
+
if (svc.apiKey === undefined || svc.apiKey === "")
|
|
129
|
+
return undefined;
|
|
130
|
+
const ref = config.envRefs?.get(svc.apiKey);
|
|
131
|
+
if (ref !== undefined)
|
|
132
|
+
return ref;
|
|
133
|
+
if (!opts.redactLiterals)
|
|
134
|
+
return svc.apiKey;
|
|
135
|
+
// protocol.apiKeyEnvVar is the var the CHILD CLI reads, which is only a
|
|
136
|
+
// suggestion for what to name the config reference — they need not match,
|
|
137
|
+
// and endpoint routes have no protocol at all. Hence the generic fallback.
|
|
138
|
+
const envVar = svc.protocol?.apiKeyEnvVar ?? "YOUR_API_KEY_ENV_VAR";
|
|
139
|
+
return `\${${envVar}}`;
|
|
140
|
+
}
|
|
141
|
+
function cliEntryToYaml(svc, config, opts) {
|
|
142
|
+
// `harness: generic` has NO shipped preset behind it, so anything omitted
|
|
143
|
+
// here is not recoverable on reload — it is gone. Dropping `protocol:` was
|
|
144
|
+
// fatal: config.ts refuses a generic entry without one ("requires a
|
|
145
|
+
// protocol block — entry ignored"), so a round-trip deleted every
|
|
146
|
+
// user-added harness, and `configure --yes --force` wrote that over their
|
|
147
|
+
// file. This is the documented README#adding-a-harness path.
|
|
148
|
+
//
|
|
149
|
+
// Built-in harnesses keep the lean output: their preset supplies protocol
|
|
150
|
+
// and billing, and emitting a copy would freeze a snapshot that stops
|
|
151
|
+
// tracking future default changes — the same reasoning commonEntryFields
|
|
152
|
+
// gives for omitting billing fields generally.
|
|
153
|
+
const isGeneric = svc.harness === "generic";
|
|
103
154
|
return {
|
|
104
155
|
name: svc.name,
|
|
105
156
|
harness: svc.harness,
|
|
106
157
|
command: svc.command,
|
|
107
|
-
api_key: svc
|
|
158
|
+
api_key: apiKeyForYaml(svc, config, opts),
|
|
108
159
|
...commonEntryFields(svc),
|
|
160
|
+
...(isGeneric
|
|
161
|
+
? {
|
|
162
|
+
provider: svc.provider,
|
|
163
|
+
surface: svc.surface,
|
|
164
|
+
auth_source: svc.authSource,
|
|
165
|
+
billing_kind: svc.billingKind,
|
|
166
|
+
paid_usage_possible: svc.paidUsagePossible,
|
|
167
|
+
billing_notes: svc.billingNotes,
|
|
168
|
+
protocol: svc.protocol,
|
|
169
|
+
}
|
|
170
|
+
: {}),
|
|
109
171
|
};
|
|
110
172
|
}
|
|
111
|
-
function endpointEntryToYaml(svc) {
|
|
173
|
+
function endpointEntryToYaml(svc, config, opts) {
|
|
112
174
|
return {
|
|
113
175
|
name: svc.name,
|
|
114
176
|
base_url: svc.baseUrl,
|
|
115
|
-
api_key: svc
|
|
177
|
+
api_key: apiKeyForYaml(svc, config, opts),
|
|
116
178
|
...commonEntryFields(svc),
|
|
179
|
+
// Endpoints have no shipped preset behind them, so an omitted billing
|
|
180
|
+
// field is not recomputed on reload — it is lost. Verified: an endpoint
|
|
181
|
+
// declaring `billing_kind: local_compute` and `paid_usage_possible: false`
|
|
182
|
+
// came back undefined, flipping it to paid=possible and getting it skipped
|
|
183
|
+
// by billing policy.
|
|
184
|
+
//
|
|
185
|
+
// This is the same reasoning already applied to `harness: generic` CLI
|
|
186
|
+
// routes, and it should have been applied here at the same time. Built-in
|
|
187
|
+
// harnesses still keep the lean output because their preset supplies these.
|
|
188
|
+
provider: svc.provider,
|
|
189
|
+
surface: svc.surface,
|
|
190
|
+
auth_source: svc.authSource,
|
|
191
|
+
billing_kind: svc.billingKind,
|
|
192
|
+
paid_usage_possible: svc.paidUsagePossible,
|
|
193
|
+
billing_notes: svc.billingNotes,
|
|
117
194
|
};
|
|
118
195
|
}
|
|
119
|
-
|
|
196
|
+
/**
|
|
197
|
+
* Top-level settings a user set and that have no defaults to track.
|
|
198
|
+
*
|
|
199
|
+
* These were silently dropped on every round-trip. Unlike the billing fields
|
|
200
|
+
* (see commonEntryFields — omitted deliberately so they keep following harness
|
|
201
|
+
* defaults), nothing recomputes these: a dropped `max_concurrent_runs` is
|
|
202
|
+
* simply gone, and `configure --yes --force` wrote the result over the user's
|
|
203
|
+
* file.
|
|
204
|
+
*/
|
|
205
|
+
function topLevelToYaml(config) {
|
|
206
|
+
const out = {};
|
|
207
|
+
if (config.disabled && config.disabled.length > 0)
|
|
208
|
+
out.disabled = [...config.disabled];
|
|
209
|
+
if (config.maxConcurrentRuns !== undefined)
|
|
210
|
+
out.max_concurrent_runs = config.maxConcurrentRuns;
|
|
211
|
+
if (config.retention?.jobsDays !== undefined) {
|
|
212
|
+
out.retention = { jobs_days: config.retention.jobsDays };
|
|
213
|
+
}
|
|
214
|
+
if (config.telemetry?.enabled !== undefined) {
|
|
215
|
+
out.telemetry = { enabled: config.telemetry.enabled };
|
|
216
|
+
}
|
|
217
|
+
if (config.leaderboard?.enabled !== undefined) {
|
|
218
|
+
out.leaderboard = { enabled: config.leaderboard.enabled };
|
|
219
|
+
}
|
|
220
|
+
return out;
|
|
221
|
+
}
|
|
222
|
+
function configToYaml(config, opts) {
|
|
120
223
|
const clis = [];
|
|
121
224
|
const endpoints = [];
|
|
122
225
|
for (const svc of Object.values(config.services)) {
|
|
123
226
|
if (svc.type === "cli")
|
|
124
|
-
clis.push(cliEntryToYaml(svc));
|
|
227
|
+
clis.push(cliEntryToYaml(svc, config, opts));
|
|
125
228
|
else
|
|
126
|
-
endpoints.push(endpointEntryToYaml(svc));
|
|
229
|
+
endpoints.push(endpointEntryToYaml(svc, config, opts));
|
|
127
230
|
}
|
|
128
|
-
const doc = {};
|
|
231
|
+
const doc = { ...topLevelToYaml(config) };
|
|
129
232
|
if (clis.length > 0)
|
|
130
233
|
doc.clis = clis;
|
|
131
234
|
if (endpoints.length > 0)
|
|
@@ -133,13 +236,27 @@ function configToYaml(config) {
|
|
|
133
236
|
return yaml.dump(doc, { noRefs: true, lineWidth: 100 });
|
|
134
237
|
}
|
|
135
238
|
async function cmdConfigure(configPath, explicitConfigPath, opts) {
|
|
136
|
-
|
|
137
|
-
|
|
239
|
+
// configure's --config names where it will WRITE, so a path that does not
|
|
240
|
+
// exist yet is the normal first-run case, not a typo.
|
|
241
|
+
const config = await loadConfig(configPath, { allowMissing: true });
|
|
138
242
|
const routeCount = Object.keys(config.services).length;
|
|
139
243
|
if (opts.print) {
|
|
140
|
-
|
|
244
|
+
// Preview goes to a terminal and, routinely, into a bug report — a
|
|
245
|
+
// literal key with no ${VAR} to restore is redacted rather than echoed.
|
|
246
|
+
const preview = configToYaml(config, { redactLiterals: true });
|
|
247
|
+
process.stdout.write(preview);
|
|
248
|
+
const redacted = Object.values(config.services).some((svc) => svc.apiKey !== undefined &&
|
|
249
|
+
svc.apiKey !== "" &&
|
|
250
|
+
config.envRefs?.get(svc.apiKey) === undefined);
|
|
251
|
+
if (redacted) {
|
|
252
|
+
process.stderr.write("note: one or more api_key values are literals in the source config and were " +
|
|
253
|
+
"replaced with ${ENV_VAR} placeholders in this preview. Move them to " +
|
|
254
|
+
"environment variables — this output is not a drop-in replacement for that file " +
|
|
255
|
+
"until you do.\n");
|
|
256
|
+
}
|
|
141
257
|
return 0;
|
|
142
258
|
}
|
|
259
|
+
const yamlText = configToYaml(config, { redactLiterals: false });
|
|
143
260
|
process.stdout.write(`Detected ${routeCount} harness route${routeCount === 1 ? "" : "s"}.\n`);
|
|
144
261
|
for (const [name, svc] of Object.entries(config.services)) {
|
|
145
262
|
process.stdout.write(`- ${name}: harness=${svc.harness ?? name} billing=${buildRouteBilling(svc).kind} safety=${effectiveSafetyProfile(svc)} model=${svc.model ?? svc.leaderboardModel ?? "unknown"}\n`);
|
|
@@ -165,8 +282,17 @@ async function cmdConfigure(configPath, explicitConfigPath, opts) {
|
|
|
165
282
|
process.stdout.write("After writing config, connect agents by adding the harness-dispatch MCP snippet to the agent you use.\n");
|
|
166
283
|
return 0;
|
|
167
284
|
}
|
|
168
|
-
|
|
169
|
-
|
|
285
|
+
// Guard ANY existing file, however the path was supplied.
|
|
286
|
+
//
|
|
287
|
+
// This previously read `existsSync(target) && explicitConfigPath === undefined`,
|
|
288
|
+
// so passing --config skipped the protection entirely — and the message told
|
|
289
|
+
// you to pass --config, which is exactly what disabled it. Overwriting a
|
|
290
|
+
// hand-written config is not recoverable, so it now takes an explicit
|
|
291
|
+
// --force rather than an accident of which flag you happened to use.
|
|
292
|
+
if (existsSync(target) && !opts.force) {
|
|
293
|
+
process.stderr.write(`configure: ${target} already exists and would be overwritten.\n` +
|
|
294
|
+
"Use --print to inspect the generated YAML, --config <other-path> to write\n" +
|
|
295
|
+
"elsewhere, or --force to overwrite it deliberately.\n");
|
|
170
296
|
return 1;
|
|
171
297
|
}
|
|
172
298
|
await fs.writeFile(target, yamlText, "utf-8");
|
|
@@ -220,6 +346,30 @@ async function cmdUsage(configPath, opts) {
|
|
|
220
346
|
process.stdout.write(opts.json ? `${JSON.stringify(usage, null, 2)}\n` : `${renderUsageText(usage)}\n`);
|
|
221
347
|
return 0;
|
|
222
348
|
}
|
|
349
|
+
/**
|
|
350
|
+
* Can we actually persist state? Breaker cooldowns, quota counters and job
|
|
351
|
+
* records all live here, and every write path deliberately swallows its own
|
|
352
|
+
* failures so a dispatch is never lost to a bookkeeping problem. The cost of
|
|
353
|
+
* that choice is silence, which is what this check buys back.
|
|
354
|
+
*/
|
|
355
|
+
function stateDirWritable() {
|
|
356
|
+
const dir = stateRoot();
|
|
357
|
+
const probe = path.join(dir, `.write-probe-${process.pid}`);
|
|
358
|
+
try {
|
|
359
|
+
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
360
|
+
writeFileSync(probe, "", { mode: 0o600 });
|
|
361
|
+
rmSync(probe, { force: true });
|
|
362
|
+
return { ok: true, detail: `writable: ${dir}` };
|
|
363
|
+
}
|
|
364
|
+
catch (err) {
|
|
365
|
+
return {
|
|
366
|
+
ok: false,
|
|
367
|
+
detail: `NOT writable: ${dir} (${err instanceof Error ? err.message : String(err)}). ` +
|
|
368
|
+
`Breaker cooldowns and usage counters will not persist, and jobs may be ` +
|
|
369
|
+
`reported as orphaned after they have actually succeeded.`,
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
}
|
|
223
373
|
async function cmdDoctor(configPath, opts) {
|
|
224
374
|
const runtime = await buildRuntime(configPath);
|
|
225
375
|
if (opts.allowPaid) {
|
|
@@ -228,12 +378,24 @@ async function cmdDoctor(configPath, opts) {
|
|
|
228
378
|
}
|
|
229
379
|
}
|
|
230
380
|
const status = await buildStatus(runtime.config, runtime.dispatchers, runtime.quota, runtime.router, runtime.leaderboard);
|
|
231
|
-
|
|
381
|
+
// Must agree with package.json engines (>=22.22.2) and the README. It said
|
|
382
|
+
// >= 24 while both of those said 22, so a user following the README's own
|
|
383
|
+
// install block hit `fail node` on the second command — on a runtime where
|
|
384
|
+
// dispatch works correctly. Three sources, two answers, and the one the
|
|
385
|
+
// user sees first was the wrong one.
|
|
386
|
+
const [nodeMajor, nodeMinor, nodePatch] = process.versions.node
|
|
387
|
+
.split(".")
|
|
388
|
+
.map((n) => Number(n) || 0);
|
|
389
|
+
const nodeOk = (nodeMajor ?? 0) > 22 ||
|
|
390
|
+
((nodeMajor ?? 0) === 22 &&
|
|
391
|
+
((nodeMinor ?? 0) > 22 || ((nodeMinor ?? 0) === 22 && (nodePatch ?? 0) >= 2)));
|
|
232
392
|
const checks = [
|
|
233
393
|
{
|
|
234
394
|
name: "node",
|
|
235
|
-
ok:
|
|
236
|
-
detail:
|
|
395
|
+
ok: nodeOk,
|
|
396
|
+
detail: nodeOk
|
|
397
|
+
? `Node ${process.versions.node}`
|
|
398
|
+
: `Node ${process.versions.node} — harness-dispatch needs >=22.22.2`,
|
|
237
399
|
},
|
|
238
400
|
{
|
|
239
401
|
name: "config",
|
|
@@ -250,7 +412,23 @@ async function cmdDoctor(configPath, opts) {
|
|
|
250
412
|
{
|
|
251
413
|
name: "routes",
|
|
252
414
|
ok: status.ready.length > 0,
|
|
253
|
-
|
|
415
|
+
// When nothing is ready, say what was looked for. "0 ready route(s)" on
|
|
416
|
+
// its own leaves a new user with no idea whether the tool is broken or
|
|
417
|
+
// simply has nothing to route to, and no hint what to install.
|
|
418
|
+
detail: status.ready.length > 0
|
|
419
|
+
? `${status.ready.length} ready route(s)`
|
|
420
|
+
: `0 ready route(s). Looked for these harness CLIs on PATH: ` +
|
|
421
|
+
`${Object.values(AUTO_DETECT_COMMANDS).join(", ")}. ` +
|
|
422
|
+
`Install one, or add a route to config.yaml (endpoints: need no CLI).`,
|
|
423
|
+
},
|
|
424
|
+
{
|
|
425
|
+
// Nothing checked this, so an unwritable state directory surfaced only
|
|
426
|
+
// as jobs mysteriously reported "the dispatch server exited before the
|
|
427
|
+
// run finished" — a false cause, 90s after the work had actually
|
|
428
|
+
// succeeded.
|
|
429
|
+
name: "state-dir",
|
|
430
|
+
ok: stateDirWritable().ok,
|
|
431
|
+
detail: stateDirWritable().detail,
|
|
254
432
|
},
|
|
255
433
|
{
|
|
256
434
|
name: "http-auth",
|
|
@@ -385,10 +563,26 @@ function parsePositiveInt(value, fallback) {
|
|
|
385
563
|
const parsed = Number(value);
|
|
386
564
|
return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
|
|
387
565
|
}
|
|
566
|
+
/**
|
|
567
|
+
* Thrown for a flag value the user typed wrong. main() turns it into a plain
|
|
568
|
+
* one-line message and exit 1 — never a stack trace, which tells a CLI user
|
|
569
|
+
* nothing they can act on.
|
|
570
|
+
*/
|
|
571
|
+
class UsageError extends Error {
|
|
572
|
+
}
|
|
388
573
|
function serveOpts(values) {
|
|
389
574
|
const out = {};
|
|
390
|
-
if (values.port !== undefined)
|
|
391
|
-
|
|
575
|
+
if (values.port !== undefined) {
|
|
576
|
+
// `--port abc` used to fall back to 0, which binds a RANDOM free port and
|
|
577
|
+
// prints it as though it were what was asked for. A typo'd port silently
|
|
578
|
+
// serving somewhere else is worse than refusing to start.
|
|
579
|
+
const port = parsePositiveInt(values.port, 0);
|
|
580
|
+
if (port === 0 || !Number.isInteger(port) || port > 65535) {
|
|
581
|
+
throw new UsageError(`--port must be an integer between 1 and 65535 (got ${JSON.stringify(values.port)}). ` +
|
|
582
|
+
`Omit --port to bind a random free one.`);
|
|
583
|
+
}
|
|
584
|
+
out.port = port;
|
|
585
|
+
}
|
|
392
586
|
if (typeof values.host === "string")
|
|
393
587
|
out.host = values.host;
|
|
394
588
|
return out;
|
|
@@ -408,17 +602,37 @@ export async function main(argv) {
|
|
|
408
602
|
host: { type: "string" },
|
|
409
603
|
print: { type: "boolean" },
|
|
410
604
|
yes: { type: "boolean" },
|
|
605
|
+
force: { type: "boolean" },
|
|
411
606
|
http: { type: "string" },
|
|
412
607
|
},
|
|
413
608
|
allowPositionals: true,
|
|
414
609
|
strict: false,
|
|
415
610
|
});
|
|
611
|
+
// parseArgs runs with strict:false so positionals and subcommand shapes stay
|
|
612
|
+
// flexible — the cost is that an unknown flag is silently accepted. `status
|
|
613
|
+
// --jsonn` printed human text and exited 0, so a cron job piping it to jq
|
|
614
|
+
// got garbage AND a success code. PRODUCT.md names automation as a user, and
|
|
615
|
+
// a wrong exit code is the one thing automation cannot recover from.
|
|
616
|
+
const knownFlags = new Set([
|
|
617
|
+
"help", "config", "json", "live", "allow-paid", "watch", "interval",
|
|
618
|
+
"port", "host", "print", "yes", "force", "http",
|
|
619
|
+
]);
|
|
620
|
+
const unknownFlags = Object.keys(values).filter((k) => !knownFlags.has(k));
|
|
621
|
+
if (unknownFlags.length > 0) {
|
|
622
|
+
throw new UsageError(`unknown option${unknownFlags.length > 1 ? "s" : ""}: ` +
|
|
623
|
+
`${unknownFlags.map((f) => `--${f}`).join(", ")}. Run --help for the list.`);
|
|
624
|
+
}
|
|
416
625
|
if (values.help) {
|
|
417
626
|
printUsage();
|
|
418
627
|
return 0;
|
|
419
628
|
}
|
|
420
629
|
await initObservability();
|
|
421
630
|
const [command, ...rest] = positionals;
|
|
631
|
+
// `--config` with no value: parseArgs yields boolean true, which reached
|
|
632
|
+
// path.join and threw ERR_INVALID_ARG_TYPE as a raw Node stack trace.
|
|
633
|
+
if (values.config !== undefined && typeof values.config !== "string") {
|
|
634
|
+
throw new UsageError("--config needs a path, e.g. --config ./config.yaml");
|
|
635
|
+
}
|
|
422
636
|
const explicitConfigPath = values.config;
|
|
423
637
|
// If the caller didn't pass --config, fall back to ./config.yaml when it
|
|
424
638
|
// exists, rather than silently ignoring it and running pure auto-detect.
|
|
@@ -445,6 +659,7 @@ export async function main(argv) {
|
|
|
445
659
|
return cmdConfigure(configPath, explicitConfigPath, {
|
|
446
660
|
print: Boolean(values.print),
|
|
447
661
|
yes: Boolean(values.yes),
|
|
662
|
+
force: Boolean(values.force),
|
|
448
663
|
});
|
|
449
664
|
case "doctor":
|
|
450
665
|
return cmdDoctor(configPath, {
|
|
@@ -481,8 +696,23 @@ export async function main(argv) {
|
|
|
481
696
|
}
|
|
482
697
|
const entrypoint = typeof process !== "undefined" && Array.isArray(process.argv) ? process.argv[1] : "";
|
|
483
698
|
if (entrypoint && (entrypoint.endsWith("bin.ts") || entrypoint.endsWith("bin.js"))) {
|
|
484
|
-
void main(process.argv.slice(2))
|
|
699
|
+
void main(process.argv.slice(2))
|
|
700
|
+
.then((code) => {
|
|
485
701
|
process.exit(code);
|
|
702
|
+
})
|
|
703
|
+
.catch((err) => {
|
|
704
|
+
// A CLI user gets one actionable line, not a stack trace. Every Error is
|
|
705
|
+
// flattened to its message — UsageError and config-loading failures
|
|
706
|
+
// (missing file, bad YAML) are things the user typed and can fix, and
|
|
707
|
+
// the codebase throws user-facing Errors by convention, so there is no
|
|
708
|
+
// reliable way to tell "bug" from "bad input" by class here. Only a
|
|
709
|
+
// non-Error throw (a genuine programming error) keeps its stack.
|
|
710
|
+
if (err instanceof UsageError || err instanceof Error) {
|
|
711
|
+
process.stderr.write(`harness-dispatch: ${err.message}
|
|
712
|
+
`);
|
|
713
|
+
process.exit(1);
|
|
714
|
+
}
|
|
715
|
+
throw err;
|
|
486
716
|
});
|
|
487
717
|
}
|
|
488
718
|
//# sourceMappingURL=bin.js.map
|