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.
Files changed (185) hide show
  1. package/CHANGELOG.md +152 -0
  2. package/README.md +232 -87
  3. package/config.default.yaml +416 -397
  4. package/dist/auth.d.ts.map +1 -1
  5. package/dist/auth.js +2 -2
  6. package/dist/auth.js.map +1 -1
  7. package/dist/billing.d.ts +35 -4
  8. package/dist/billing.d.ts.map +1 -1
  9. package/dist/billing.js +48 -17
  10. package/dist/billing.js.map +1 -1
  11. package/dist/bin.d.ts.map +1 -1
  12. package/dist/bin.js +147 -91
  13. package/dist/bin.js.map +1 -1
  14. package/dist/breaker-store.d.ts +60 -9
  15. package/dist/breaker-store.d.ts.map +1 -1
  16. package/dist/breaker-store.js +246 -50
  17. package/dist/breaker-store.js.map +1 -1
  18. package/dist/circuit-breaker.d.ts +35 -0
  19. package/dist/circuit-breaker.d.ts.map +1 -1
  20. package/dist/circuit-breaker.js +68 -11
  21. package/dist/circuit-breaker.js.map +1 -1
  22. package/dist/config/coercions.d.ts +31 -0
  23. package/dist/config/coercions.d.ts.map +1 -0
  24. package/dist/config/coercions.js +169 -0
  25. package/dist/config/coercions.js.map +1 -0
  26. package/dist/config/env-interpolation.d.ts +36 -0
  27. package/dist/config/env-interpolation.d.ts.map +1 -0
  28. package/dist/config/env-interpolation.js +73 -0
  29. package/dist/config/env-interpolation.js.map +1 -0
  30. package/dist/config/protocol.d.ts +51 -0
  31. package/dist/config/protocol.d.ts.map +1 -0
  32. package/dist/config/protocol.js +300 -0
  33. package/dist/config/protocol.js.map +1 -0
  34. package/dist/config/route-fields.d.ts +72 -0
  35. package/dist/config/route-fields.d.ts.map +1 -0
  36. package/dist/config/route-fields.js +128 -0
  37. package/dist/config/route-fields.js.map +1 -0
  38. package/dist/config/validation.d.ts +72 -0
  39. package/dist/config/validation.d.ts.map +1 -0
  40. package/dist/config/validation.js +189 -0
  41. package/dist/config/validation.js.map +1 -0
  42. package/dist/config.d.ts +19 -2
  43. package/dist/config.d.ts.map +1 -1
  44. package/dist/config.js +214 -635
  45. package/dist/config.js.map +1 -1
  46. package/dist/configure-yaml.d.ts +23 -0
  47. package/dist/configure-yaml.d.ts.map +1 -0
  48. package/dist/configure-yaml.js +193 -0
  49. package/dist/configure-yaml.js.map +1 -0
  50. package/dist/dispatch-log.d.ts.map +1 -1
  51. package/dist/dispatch-log.js +2 -3
  52. package/dist/dispatch-log.js.map +1 -1
  53. package/dist/dispatchers/base.d.ts +3 -0
  54. package/dist/dispatchers/base.d.ts.map +1 -1
  55. package/dist/dispatchers/base.js.map +1 -1
  56. package/dist/dispatchers/generic-cli.d.ts +15 -1
  57. package/dist/dispatchers/generic-cli.d.ts.map +1 -1
  58. package/dist/dispatchers/generic-cli.js +69 -5
  59. package/dist/dispatchers/generic-cli.js.map +1 -1
  60. package/dist/dispatchers/openai-compatible.d.ts.map +1 -1
  61. package/dist/dispatchers/openai-compatible.js +70 -2
  62. package/dist/dispatchers/openai-compatible.js.map +1 -1
  63. package/dist/dispatchers/shared/kill-tree.d.ts +17 -4
  64. package/dist/dispatchers/shared/kill-tree.d.ts.map +1 -1
  65. package/dist/dispatchers/shared/kill-tree.js +28 -4
  66. package/dist/dispatchers/shared/kill-tree.js.map +1 -1
  67. package/dist/dispatchers/shared/rate-limit-headers.d.ts +17 -0
  68. package/dist/dispatchers/shared/rate-limit-headers.d.ts.map +1 -1
  69. package/dist/dispatchers/shared/rate-limit-headers.js +27 -0
  70. package/dist/dispatchers/shared/rate-limit-headers.js.map +1 -1
  71. package/dist/dispatchers/shared/stream-subprocess.d.ts +11 -0
  72. package/dist/dispatchers/shared/stream-subprocess.d.ts.map +1 -1
  73. package/dist/dispatchers/shared/stream-subprocess.js +62 -35
  74. package/dist/dispatchers/shared/stream-subprocess.js.map +1 -1
  75. package/dist/dispatchers/shared/subprocess.d.ts.map +1 -1
  76. package/dist/dispatchers/shared/subprocess.js +3 -0
  77. package/dist/dispatchers/shared/subprocess.js.map +1 -1
  78. package/dist/dispatchers/shared/which-available.d.ts.map +1 -1
  79. package/dist/dispatchers/shared/which-available.js +16 -2
  80. package/dist/dispatchers/shared/which-available.js.map +1 -1
  81. package/dist/file-lock.d.ts +17 -0
  82. package/dist/file-lock.d.ts.map +1 -0
  83. package/dist/file-lock.js +143 -0
  84. package/dist/file-lock.js.map +1 -0
  85. package/dist/http/parse.d.ts +64 -0
  86. package/dist/http/parse.d.ts.map +1 -0
  87. package/dist/http/parse.js +230 -0
  88. package/dist/http/parse.js.map +1 -0
  89. package/dist/http/server.d.ts +2 -0
  90. package/dist/http/server.d.ts.map +1 -1
  91. package/dist/http/server.js +249 -286
  92. package/dist/http/server.js.map +1 -1
  93. package/dist/index.d.ts +1 -1
  94. package/dist/index.d.ts.map +1 -1
  95. package/dist/index.js +3 -2
  96. package/dist/index.js.map +1 -1
  97. package/dist/job-runner.js +23 -5
  98. package/dist/job-runner.js.map +1 -1
  99. package/dist/jobs/context.d.ts +17 -0
  100. package/dist/jobs/context.d.ts.map +1 -0
  101. package/dist/jobs/context.js +84 -0
  102. package/dist/jobs/context.js.map +1 -0
  103. package/dist/jobs/store.d.ts +80 -0
  104. package/dist/jobs/store.d.ts.map +1 -0
  105. package/dist/jobs/store.js +279 -0
  106. package/dist/jobs/store.js.map +1 -0
  107. package/dist/jobs/types.d.ts +135 -0
  108. package/dist/jobs/types.d.ts.map +1 -0
  109. package/dist/jobs/types.js +11 -0
  110. package/dist/jobs/types.js.map +1 -0
  111. package/dist/jobs.d.ts +165 -85
  112. package/dist/jobs.d.ts.map +1 -1
  113. package/dist/jobs.js +824 -208
  114. package/dist/jobs.js.map +1 -1
  115. package/dist/leaderboard.d.ts +24 -1
  116. package/dist/leaderboard.d.ts.map +1 -1
  117. package/dist/leaderboard.js +94 -2
  118. package/dist/leaderboard.js.map +1 -1
  119. package/dist/mcp/config-hot-reload.d.ts.map +1 -1
  120. package/dist/mcp/config-hot-reload.js +1 -1
  121. package/dist/mcp/config-hot-reload.js.map +1 -1
  122. package/dist/mcp/dispatcher-factory.d.ts +16 -1
  123. package/dist/mcp/dispatcher-factory.d.ts.map +1 -1
  124. package/dist/mcp/dispatcher-factory.js +41 -3
  125. package/dist/mcp/dispatcher-factory.js.map +1 -1
  126. package/dist/mcp/tool-schemas.d.ts +194 -0
  127. package/dist/mcp/tool-schemas.d.ts.map +1 -0
  128. package/dist/mcp/tool-schemas.js +311 -0
  129. package/dist/mcp/tool-schemas.js.map +1 -0
  130. package/dist/mcp/tools.d.ts +26 -51
  131. package/dist/mcp/tools.d.ts.map +1 -1
  132. package/dist/mcp/tools.js +122 -151
  133. package/dist/mcp/tools.js.map +1 -1
  134. package/dist/observability/index.d.ts +16 -3
  135. package/dist/observability/index.d.ts.map +1 -1
  136. package/dist/observability/index.js +23 -7
  137. package/dist/observability/index.js.map +1 -1
  138. package/dist/observability/spans.d.ts.map +1 -1
  139. package/dist/observability/spans.js +4 -1
  140. package/dist/observability/spans.js.map +1 -1
  141. package/dist/quota.d.ts +106 -22
  142. package/dist/quota.d.ts.map +1 -1
  143. package/dist/quota.js +198 -58
  144. package/dist/quota.js.map +1 -1
  145. package/dist/route-policy.d.ts.map +1 -1
  146. package/dist/route-policy.js +13 -3
  147. package/dist/route-policy.js.map +1 -1
  148. package/dist/router.d.ts +29 -1
  149. package/dist/router.d.ts.map +1 -1
  150. package/dist/router.js +102 -40
  151. package/dist/router.js.map +1 -1
  152. package/dist/safety.d.ts.map +1 -1
  153. package/dist/safety.js +11 -2
  154. package/dist/safety.js.map +1 -1
  155. package/dist/state-dir.d.ts +17 -0
  156. package/dist/state-dir.d.ts.map +1 -0
  157. package/dist/state-dir.js +21 -0
  158. package/dist/state-dir.js.map +1 -0
  159. package/dist/status.d.ts +49 -0
  160. package/dist/status.d.ts.map +1 -1
  161. package/dist/status.js +67 -4
  162. package/dist/status.js.map +1 -1
  163. package/dist/types.d.ts +74 -1
  164. package/dist/types.d.ts.map +1 -1
  165. package/dist/working-dir.d.ts +13 -0
  166. package/dist/working-dir.d.ts.map +1 -1
  167. package/dist/working-dir.js +27 -0
  168. package/dist/working-dir.js.map +1 -1
  169. package/dist/workspace-lock.d.ts +40 -0
  170. package/dist/workspace-lock.d.ts.map +1 -0
  171. package/dist/workspace-lock.js +258 -0
  172. package/dist/workspace-lock.js.map +1 -0
  173. package/dist/workspace-resolve.d.ts +98 -0
  174. package/dist/workspace-resolve.d.ts.map +1 -0
  175. package/dist/workspace-resolve.js +382 -0
  176. package/dist/workspace-resolve.js.map +1 -0
  177. package/dist/workspaces.d.ts +16 -0
  178. package/dist/workspaces.d.ts.map +1 -1
  179. package/dist/workspaces.js +166 -22
  180. package/dist/workspaces.js.map +1 -1
  181. package/package.json +99 -96
  182. package/dist/dashboard/live.d.ts +0 -49
  183. package/dist/dashboard/live.d.ts.map +0 -1
  184. package/dist/dashboard/live.js +0 -149
  185. package/dist/dashboard/live.js.map +0 -1
@@ -1 +1 @@
1
- {"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../src/auth.ts"],"names":[],"mappings":"AAOA,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"}
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 ?? path.join(os.homedir(), ".harness-dispatch");
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":"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"}
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
- export declare function buildRouteBilling(svc: ServiceConfig, opts?: {
3
- now?: Date;
4
- }): RouteBilling;
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
@@ -1 +1 @@
1
- {"version":3,"file":"billing.d.ts","sourceRoot":"","sources":["../src/billing.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAMV,YAAY,EACZ,aAAa,EACd,MAAM,YAAY,CAAC;AAmLpB,wBAAgB,iBAAiB,CAC/B,GAAG,EAAE,aAAa,EAClB,IAAI,GAAE;IAAE,GAAG,CAAC,EAAE,IAAI,CAAA;CAAO,GACxB,YAAY,CAqBd;AAED,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,YAAY,GAAG,OAAO,CAE/D;AAED,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,YAAY,GAAG,OAAO,CAI/D"}
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
- function isLoopback(baseUrl) {
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 false;
4
- return (baseUrl.includes("localhost") ||
5
- baseUrl.includes("127.0.0.1") ||
6
- baseUrl.includes("::1"));
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
- if (!baseUrl)
34
+ const parsed = hostOf(baseUrl);
35
+ if (parsed === undefined || !LOOPBACK_HOSTS.has(parsed.host))
10
36
  return false;
11
- return (baseUrl.includes("localhost:11434") ||
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, now) {
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, now) {
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
- export function buildRouteBilling(svc, opts = {}) {
169
- const now = opts.now ?? new Date();
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, now);
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, now);
217
+ const notes = defaultNotes(svc, surface, kind);
187
218
  if (notes !== undefined)
188
219
  billing.notes = notes;
189
220
  return billing;
@@ -1 +1 @@
1
- {"version":3,"file":"billing.js","sourceRoot":"","sources":["../src/billing.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"billing.js","sourceRoot":"","sources":["../src/billing.ts"],"names":[],"mappings":"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"}
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;AAkfH,wBAAsB,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC,CAyF1D"}
1
+ {"version":3,"file":"bin.d.ts","sourceRoot":"","sources":["../src/bin.ts"],"names":[],"mappings":";AACA;;GAEG;AAmhBH,wBAAsB,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC,CAiH1D"}
package/dist/bin.js CHANGED
@@ -2,13 +2,12 @@
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
- import yaml from "js-yaml";
10
9
  import { ensureHttpToken, maskToken, readHttpToken, rotateHttpToken, tokenPath } from "./auth.js";
11
- import { loadConfig } from "./config.js";
10
+ import { AUTO_DETECT_COMMANDS, loadConfig } from "./config.js";
12
11
  import { LeaderboardCache } from "./leaderboard.js";
13
12
  import { buildDispatchers } from "./mcp/dispatcher-factory.js";
14
13
  import { startMcpServer } from "./mcp/server.js";
@@ -19,11 +18,15 @@ import { buildStatus, buildUsage, renderStatusText, renderUsageText } from "./st
19
18
  import { startHttpServer } from "./http/server.js";
20
19
  import { billingIsBlocked, buildRouteBilling } from "./billing.js";
21
20
  import { effectiveSafetyProfile } from "./safety.js";
21
+ import { configToYaml } from "./configure-yaml.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,94 +55,34 @@ 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
  "",
58
62
  ].join("\n"));
59
63
  }
60
- /**
61
- * Common fields between clis: and endpoints: entries. Billing fields
62
- * (provider/surface/auth_source/billing_kind/paid_usage_possible/
63
- * billing_confidence/billing_notes) are deliberately NOT emitted — they're
64
- * computed from the harness/endpoint defaults every time the config loads
65
- * (see buildRouteBilling), so writing them out would freeze a snapshot that
66
- * silently stops tracking future default changes and looks like a deliberate
67
- * user override when it never was one. `allow_paid_usage` is the one real
68
- * opt-in flag here, so it's the only billing-adjacent field written.
69
- *
70
- * `safety_profile`/`effective_safety` are only emitted when the service
71
- * actually carries an explicit value — never a fallback default. Baking in
72
- * `requestedSafetyProfile()`'s "workspace_edit" fallback for every route used
73
- * to write `safety_profile: workspace_edit` on cursor_cli even though its
74
- * real effective_safety (the capability floor that actually governs it) is
75
- * full_auto, making the written file self-contradictory next to `status`.
76
- */
77
- function commonEntryFields(svc) {
78
- return {
79
- enabled: svc.enabled ? undefined : false,
80
- model: svc.model,
81
- tier: svc.tier,
82
- weight: svc.weight,
83
- cli_capability: svc.cliCapability,
84
- leaderboard_model: svc.leaderboardModel,
85
- thinking_level: svc.thinkingLevel,
86
- escalate_model: svc.escalateModel,
87
- escalate_on: svc.escalateOn.length > 0 ? svc.escalateOn : undefined,
88
- capabilities: Object.keys(svc.capabilities).length > 0 ? svc.capabilities : undefined,
89
- max_output_tokens: svc.maxOutputTokens,
90
- max_input_tokens: svc.maxInputTokens,
91
- allow_paid_usage: svc.allowPaidUsage ? true : undefined,
92
- safety_profile: svc.safetyProfile,
93
- effective_safety: svc.effectiveSafety,
94
- endpoint_mode: svc.endpointMode,
95
- endpoint_provider: svc.endpointProvider,
96
- wire_protocol: svc.wireProtocol,
97
- workspace_policy: svc.workspacePolicy,
98
- models: svc.models && svc.models.length > 0 ? svc.models : undefined,
99
- model_hint: svc.modelHint,
100
- };
101
- }
102
- function cliEntryToYaml(svc) {
103
- return {
104
- name: svc.name,
105
- harness: svc.harness,
106
- command: svc.command,
107
- api_key: svc.apiKey,
108
- ...commonEntryFields(svc),
109
- };
110
- }
111
- function endpointEntryToYaml(svc) {
112
- return {
113
- name: svc.name,
114
- base_url: svc.baseUrl,
115
- api_key: svc.apiKey,
116
- ...commonEntryFields(svc),
117
- };
118
- }
119
- function configToYaml(config) {
120
- const clis = [];
121
- const endpoints = [];
122
- for (const svc of Object.values(config.services)) {
123
- if (svc.type === "cli")
124
- clis.push(cliEntryToYaml(svc));
125
- else
126
- endpoints.push(endpointEntryToYaml(svc));
127
- }
128
- const doc = {};
129
- if (clis.length > 0)
130
- doc.clis = clis;
131
- if (endpoints.length > 0)
132
- doc.endpoints = endpoints;
133
- return yaml.dump(doc, { noRefs: true, lineWidth: 100 });
134
- }
135
64
  async function cmdConfigure(configPath, explicitConfigPath, opts) {
136
- const config = await loadConfig(configPath);
137
- const yamlText = configToYaml(config);
65
+ // configure's --config names where it will WRITE, so a path that does not
66
+ // exist yet is the normal first-run case, not a typo.
67
+ const config = await loadConfig(configPath, { allowMissing: true });
138
68
  const routeCount = Object.keys(config.services).length;
139
69
  if (opts.print) {
140
- process.stdout.write(yamlText);
70
+ // Preview goes to a terminal and, routinely, into a bug report — a
71
+ // literal key with no ${VAR} to restore is redacted rather than echoed.
72
+ const preview = configToYaml(config, { redactLiterals: true });
73
+ process.stdout.write(preview);
74
+ const redacted = Object.values(config.services).some((svc) => svc.apiKey !== undefined &&
75
+ svc.apiKey !== "" &&
76
+ config.envRefs?.get(svc.apiKey) === undefined);
77
+ if (redacted) {
78
+ process.stderr.write("note: one or more api_key values are literals in the source config and were " +
79
+ "replaced with ${ENV_VAR} placeholders in this preview. Move them to " +
80
+ "environment variables — this output is not a drop-in replacement for that file " +
81
+ "until you do.\n");
82
+ }
141
83
  return 0;
142
84
  }
85
+ const yamlText = configToYaml(config, { redactLiterals: false });
143
86
  process.stdout.write(`Detected ${routeCount} harness route${routeCount === 1 ? "" : "s"}.\n`);
144
87
  for (const [name, svc] of Object.entries(config.services)) {
145
88
  process.stdout.write(`- ${name}: harness=${svc.harness ?? name} billing=${buildRouteBilling(svc).kind} safety=${effectiveSafetyProfile(svc)} model=${svc.model ?? svc.leaderboardModel ?? "unknown"}\n`);
@@ -165,8 +108,17 @@ async function cmdConfigure(configPath, explicitConfigPath, opts) {
165
108
  process.stdout.write("After writing config, connect agents by adding the harness-dispatch MCP snippet to the agent you use.\n");
166
109
  return 0;
167
110
  }
168
- if (existsSync(target) && explicitConfigPath === undefined) {
169
- process.stderr.write("configure: config.yaml already exists. Pass --config <path> or --print to avoid overwriting it.\n");
111
+ // Guard ANY existing file, however the path was supplied.
112
+ //
113
+ // This previously read `existsSync(target) && explicitConfigPath === undefined`,
114
+ // so passing --config skipped the protection entirely — and the message told
115
+ // you to pass --config, which is exactly what disabled it. Overwriting a
116
+ // hand-written config is not recoverable, so it now takes an explicit
117
+ // --force rather than an accident of which flag you happened to use.
118
+ if (existsSync(target) && !opts.force) {
119
+ process.stderr.write(`configure: ${target} already exists and would be overwritten.\n` +
120
+ "Use --print to inspect the generated YAML, --config <other-path> to write\n" +
121
+ "elsewhere, or --force to overwrite it deliberately.\n");
170
122
  return 1;
171
123
  }
172
124
  await fs.writeFile(target, yamlText, "utf-8");
@@ -220,6 +172,30 @@ async function cmdUsage(configPath, opts) {
220
172
  process.stdout.write(opts.json ? `${JSON.stringify(usage, null, 2)}\n` : `${renderUsageText(usage)}\n`);
221
173
  return 0;
222
174
  }
175
+ /**
176
+ * Can we actually persist state? Breaker cooldowns, quota counters and job
177
+ * records all live here, and every write path deliberately swallows its own
178
+ * failures so a dispatch is never lost to a bookkeeping problem. The cost of
179
+ * that choice is silence, which is what this check buys back.
180
+ */
181
+ function stateDirWritable() {
182
+ const dir = stateRoot();
183
+ const probe = path.join(dir, `.write-probe-${process.pid}`);
184
+ try {
185
+ mkdirSync(dir, { recursive: true, mode: 0o700 });
186
+ writeFileSync(probe, "", { mode: 0o600 });
187
+ rmSync(probe, { force: true });
188
+ return { ok: true, detail: `writable: ${dir}` };
189
+ }
190
+ catch (err) {
191
+ return {
192
+ ok: false,
193
+ detail: `NOT writable: ${dir} (${err instanceof Error ? err.message : String(err)}). ` +
194
+ `Breaker cooldowns and usage counters will not persist, and jobs may be ` +
195
+ `reported as orphaned after they have actually succeeded.`,
196
+ };
197
+ }
198
+ }
223
199
  async function cmdDoctor(configPath, opts) {
224
200
  const runtime = await buildRuntime(configPath);
225
201
  if (opts.allowPaid) {
@@ -228,12 +204,24 @@ async function cmdDoctor(configPath, opts) {
228
204
  }
229
205
  }
230
206
  const status = await buildStatus(runtime.config, runtime.dispatchers, runtime.quota, runtime.router, runtime.leaderboard);
231
- const nodeMajor = Number(process.versions.node.split(".")[0] ?? "0");
207
+ // Must agree with package.json engines (>=22.22.2) and the README. It said
208
+ // >= 24 while both of those said 22, so a user following the README's own
209
+ // install block hit `fail node` on the second command — on a runtime where
210
+ // dispatch works correctly. Three sources, two answers, and the one the
211
+ // user sees first was the wrong one.
212
+ const [nodeMajor, nodeMinor, nodePatch] = process.versions.node
213
+ .split(".")
214
+ .map((n) => Number(n) || 0);
215
+ const nodeOk = (nodeMajor ?? 0) > 22 ||
216
+ ((nodeMajor ?? 0) === 22 &&
217
+ ((nodeMinor ?? 0) > 22 || ((nodeMinor ?? 0) === 22 && (nodePatch ?? 0) >= 2)));
232
218
  const checks = [
233
219
  {
234
220
  name: "node",
235
- ok: nodeMajor >= 24,
236
- detail: `Node ${process.versions.node}`,
221
+ ok: nodeOk,
222
+ detail: nodeOk
223
+ ? `Node ${process.versions.node}`
224
+ : `Node ${process.versions.node} — harness-dispatch needs >=22.22.2`,
237
225
  },
238
226
  {
239
227
  name: "config",
@@ -250,7 +238,23 @@ async function cmdDoctor(configPath, opts) {
250
238
  {
251
239
  name: "routes",
252
240
  ok: status.ready.length > 0,
253
- detail: `${status.ready.length} ready route(s)`,
241
+ // When nothing is ready, say what was looked for. "0 ready route(s)" on
242
+ // its own leaves a new user with no idea whether the tool is broken or
243
+ // simply has nothing to route to, and no hint what to install.
244
+ detail: status.ready.length > 0
245
+ ? `${status.ready.length} ready route(s)`
246
+ : `0 ready route(s). Looked for these harness CLIs on PATH: ` +
247
+ `${Object.values(AUTO_DETECT_COMMANDS).join(", ")}. ` +
248
+ `Install one, or add a route to config.yaml (endpoints: need no CLI).`,
249
+ },
250
+ {
251
+ // Nothing checked this, so an unwritable state directory surfaced only
252
+ // as jobs mysteriously reported "the dispatch server exited before the
253
+ // run finished" — a false cause, 90s after the work had actually
254
+ // succeeded.
255
+ name: "state-dir",
256
+ ok: stateDirWritable().ok,
257
+ detail: stateDirWritable().detail,
254
258
  },
255
259
  {
256
260
  name: "http-auth",
@@ -385,10 +389,26 @@ function parsePositiveInt(value, fallback) {
385
389
  const parsed = Number(value);
386
390
  return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
387
391
  }
392
+ /**
393
+ * Thrown for a flag value the user typed wrong. main() turns it into a plain
394
+ * one-line message and exit 1 — never a stack trace, which tells a CLI user
395
+ * nothing they can act on.
396
+ */
397
+ class UsageError extends Error {
398
+ }
388
399
  function serveOpts(values) {
389
400
  const out = {};
390
- if (values.port !== undefined)
391
- out.port = parsePositiveInt(values.port, 0);
401
+ if (values.port !== undefined) {
402
+ // `--port abc` used to fall back to 0, which binds a RANDOM free port and
403
+ // prints it as though it were what was asked for. A typo'd port silently
404
+ // serving somewhere else is worse than refusing to start.
405
+ const port = parsePositiveInt(values.port, 0);
406
+ if (port === 0 || !Number.isInteger(port) || port > 65535) {
407
+ throw new UsageError(`--port must be an integer between 1 and 65535 (got ${JSON.stringify(values.port)}). ` +
408
+ `Omit --port to bind a random free one.`);
409
+ }
410
+ out.port = port;
411
+ }
392
412
  if (typeof values.host === "string")
393
413
  out.host = values.host;
394
414
  return out;
@@ -408,17 +428,37 @@ export async function main(argv) {
408
428
  host: { type: "string" },
409
429
  print: { type: "boolean" },
410
430
  yes: { type: "boolean" },
431
+ force: { type: "boolean" },
411
432
  http: { type: "string" },
412
433
  },
413
434
  allowPositionals: true,
414
435
  strict: false,
415
436
  });
437
+ // parseArgs runs with strict:false so positionals and subcommand shapes stay
438
+ // flexible — the cost is that an unknown flag is silently accepted. `status
439
+ // --jsonn` printed human text and exited 0, so a cron job piping it to jq
440
+ // got garbage AND a success code. PRODUCT.md names automation as a user, and
441
+ // a wrong exit code is the one thing automation cannot recover from.
442
+ const knownFlags = new Set([
443
+ "help", "config", "json", "live", "allow-paid", "watch", "interval",
444
+ "port", "host", "print", "yes", "force", "http",
445
+ ]);
446
+ const unknownFlags = Object.keys(values).filter((k) => !knownFlags.has(k));
447
+ if (unknownFlags.length > 0) {
448
+ throw new UsageError(`unknown option${unknownFlags.length > 1 ? "s" : ""}: ` +
449
+ `${unknownFlags.map((f) => `--${f}`).join(", ")}. Run --help for the list.`);
450
+ }
416
451
  if (values.help) {
417
452
  printUsage();
418
453
  return 0;
419
454
  }
420
455
  await initObservability();
421
456
  const [command, ...rest] = positionals;
457
+ // `--config` with no value: parseArgs yields boolean true, which reached
458
+ // path.join and threw ERR_INVALID_ARG_TYPE as a raw Node stack trace.
459
+ if (values.config !== undefined && typeof values.config !== "string") {
460
+ throw new UsageError("--config needs a path, e.g. --config ./config.yaml");
461
+ }
422
462
  const explicitConfigPath = values.config;
423
463
  // If the caller didn't pass --config, fall back to ./config.yaml when it
424
464
  // exists, rather than silently ignoring it and running pure auto-detect.
@@ -445,6 +485,7 @@ export async function main(argv) {
445
485
  return cmdConfigure(configPath, explicitConfigPath, {
446
486
  print: Boolean(values.print),
447
487
  yes: Boolean(values.yes),
488
+ force: Boolean(values.force),
448
489
  });
449
490
  case "doctor":
450
491
  return cmdDoctor(configPath, {
@@ -481,8 +522,23 @@ export async function main(argv) {
481
522
  }
482
523
  const entrypoint = typeof process !== "undefined" && Array.isArray(process.argv) ? process.argv[1] : "";
483
524
  if (entrypoint && (entrypoint.endsWith("bin.ts") || entrypoint.endsWith("bin.js"))) {
484
- void main(process.argv.slice(2)).then((code) => {
525
+ void main(process.argv.slice(2))
526
+ .then((code) => {
485
527
  process.exit(code);
528
+ })
529
+ .catch((err) => {
530
+ // A CLI user gets one actionable line, not a stack trace. Every Error is
531
+ // flattened to its message — UsageError and config-loading failures
532
+ // (missing file, bad YAML) are things the user typed and can fix, and
533
+ // the codebase throws user-facing Errors by convention, so there is no
534
+ // reliable way to tell "bug" from "bad input" by class here. Only a
535
+ // non-Error throw (a genuine programming error) keeps its stack.
536
+ if (err instanceof UsageError || err instanceof Error) {
537
+ process.stderr.write(`harness-dispatch: ${err.message}
538
+ `);
539
+ process.exit(1);
540
+ }
541
+ throw err;
486
542
  });
487
543
  }
488
544
  //# sourceMappingURL=bin.js.map