@harness-engineering/orchestrator 0.9.0 → 0.9.1

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/dist/index.mjs CHANGED
@@ -2445,6 +2445,9 @@ var RoadmapTrackerAdapter = class {
2445
2445
  // src/tracker/extensions/linear.ts
2446
2446
  import { Ok as Ok5, Err as Err4 } from "@harness-engineering/types";
2447
2447
  var LINEAR_GRAPHQL_ENDPOINT = "https://api.linear.app/graphql";
2448
+ function errorMessage(err) {
2449
+ return err instanceof Error ? err.message : String(err);
2450
+ }
2448
2451
  var LinearGraphQLClient = class {
2449
2452
  apiKey;
2450
2453
  endpoint;
@@ -2455,9 +2458,23 @@ var LinearGraphQLClient = class {
2455
2458
  this.fetchFn = opts.fetchFn ?? globalThis.fetch;
2456
2459
  }
2457
2460
  async query(query, variables) {
2458
- let res;
2461
+ const sent = await this.sendRequest(query, variables);
2462
+ if (!sent.ok) return sent;
2463
+ const res = sent.value;
2464
+ if (!res.ok) {
2465
+ return Err4(await this.httpError(res));
2466
+ }
2467
+ const parsed = await this.parseEnvelope(res);
2468
+ if (!parsed.ok) return parsed;
2469
+ const envelope = parsed.value;
2470
+ const graphqlError = envelopeError(envelope);
2471
+ if (graphqlError) return Err4(graphqlError);
2472
+ return Ok5(envelope.data ?? {});
2473
+ }
2474
+ /** POST the operation to Linear, normalizing transport throws into an `Err`. */
2475
+ async sendRequest(query, variables) {
2459
2476
  try {
2460
- res = await this.fetchFn(this.endpoint, {
2477
+ const res = await this.fetchFn(this.endpoint, {
2461
2478
  method: "POST",
2462
2479
  headers: {
2463
2480
  Authorization: this.apiKey,
@@ -2465,35 +2482,31 @@ var LinearGraphQLClient = class {
2465
2482
  },
2466
2483
  body: JSON.stringify({ query, variables: variables ?? {} })
2467
2484
  });
2485
+ return Ok5(res);
2468
2486
  } catch (err) {
2469
- return Err4(
2470
- new Error(
2471
- `Linear GraphQL request failed: ${err instanceof Error ? err.message : String(err)}`
2472
- )
2473
- );
2474
- }
2475
- if (!res.ok) {
2476
- const body = await res.text().catch(() => "");
2477
- const detail = body ? `: ${body.slice(0, 500)}` : "";
2478
- return Err4(new Error(`Linear GraphQL HTTP ${res.status}${detail}`));
2487
+ return Err4(new Error(`Linear GraphQL request failed: ${errorMessage(err)}`));
2479
2488
  }
2480
- let envelope;
2489
+ }
2490
+ /** Build the error for a non-2xx HTTP response, including a truncated body. */
2491
+ async httpError(res) {
2492
+ const body = await res.text().catch(() => "");
2493
+ const detail = body ? `: ${body.slice(0, 500)}` : "";
2494
+ return new Error(`Linear GraphQL HTTP ${res.status}${detail}`);
2495
+ }
2496
+ /** Parse the JSON envelope, normalizing parse failures into an `Err`. */
2497
+ async parseEnvelope(res) {
2481
2498
  try {
2482
- envelope = await res.json();
2499
+ return Ok5(await res.json());
2483
2500
  } catch (err) {
2484
- return Err4(
2485
- new Error(
2486
- `Linear GraphQL response was not valid JSON: ${err instanceof Error ? err.message : String(err)}`
2487
- )
2488
- );
2489
- }
2490
- if (envelope.errors && envelope.errors.length > 0) {
2491
- const message = envelope.errors.map((e) => e.message ?? "unknown error").join("; ");
2492
- return Err4(new Error(`Linear GraphQL error: ${message}`));
2501
+ return Err4(new Error(`Linear GraphQL response was not valid JSON: ${errorMessage(err)}`));
2493
2502
  }
2494
- return Ok5(envelope.data ?? {});
2495
2503
  }
2496
2504
  };
2505
+ function envelopeError(envelope) {
2506
+ if (!envelope.errors || envelope.errors.length === 0) return void 0;
2507
+ const message = envelope.errors.map((e) => e.message ?? "unknown error").join("; ");
2508
+ return new Error(`Linear GraphQL error: ${message}`);
2509
+ }
2497
2510
  var LinearGraphQLStub = class {
2498
2511
  async query(query, _variables) {
2499
2512
  console.log("Linear GraphQL query (stub):", query);
@@ -4696,18 +4709,18 @@ var AnthropicBackend = class {
4696
4709
  usage
4697
4710
  };
4698
4711
  } catch (err) {
4699
- const errorMessage = err instanceof Error ? err.message : "Anthropic request failed";
4712
+ const errorMessage2 = err instanceof Error ? err.message : "Anthropic request failed";
4700
4713
  yield {
4701
4714
  type: "error",
4702
4715
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
4703
- content: errorMessage,
4716
+ content: errorMessage2,
4704
4717
  sessionId: session.sessionId
4705
4718
  };
4706
4719
  return {
4707
4720
  success: false,
4708
4721
  sessionId: session.sessionId,
4709
4722
  usage: { inputTokens: 0, outputTokens: 0, totalTokens: 0 },
4710
- error: errorMessage
4723
+ error: errorMessage2
4711
4724
  };
4712
4725
  }
4713
4726
  }
@@ -4801,18 +4814,18 @@ var OpenAIBackend = class {
4801
4814
  }
4802
4815
  }
4803
4816
  } catch (err) {
4804
- const errorMessage = err instanceof Error ? err.message : "OpenAI request failed";
4817
+ const errorMessage2 = err instanceof Error ? err.message : "OpenAI request failed";
4805
4818
  yield {
4806
4819
  type: "error",
4807
4820
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
4808
- content: errorMessage,
4821
+ content: errorMessage2,
4809
4822
  sessionId: session.sessionId
4810
4823
  };
4811
4824
  return {
4812
4825
  success: false,
4813
4826
  sessionId: session.sessionId,
4814
4827
  usage: { inputTokens: 0, outputTokens: 0, totalTokens: 0 },
4815
- error: errorMessage
4828
+ error: errorMessage2
4816
4829
  };
4817
4830
  }
4818
4831
  const usage = {
@@ -4926,11 +4939,11 @@ var GeminiBackend = class {
4926
4939
  }
4927
4940
  }
4928
4941
  } catch (err) {
4929
- const errorMessage = err instanceof Error ? err.message : "Gemini request failed";
4942
+ const errorMessage2 = err instanceof Error ? err.message : "Gemini request failed";
4930
4943
  yield {
4931
4944
  type: "error",
4932
4945
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
4933
- content: errorMessage,
4946
+ content: errorMessage2,
4934
4947
  sessionId: session.sessionId
4935
4948
  };
4936
4949
  return {
@@ -4943,7 +4956,7 @@ var GeminiBackend = class {
4943
4956
  cacheCreationTokens,
4944
4957
  cacheReadTokens
4945
4958
  },
4946
- error: errorMessage
4959
+ error: errorMessage2
4947
4960
  };
4948
4961
  }
4949
4962
  const usage = {
@@ -5072,18 +5085,18 @@ var LocalBackend = class {
5072
5085
  }
5073
5086
  }
5074
5087
  } catch (err) {
5075
- const errorMessage = err instanceof Error ? err.message : "Local backend request failed";
5088
+ const errorMessage2 = err instanceof Error ? err.message : "Local backend request failed";
5076
5089
  yield {
5077
5090
  type: "error",
5078
5091
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
5079
- content: errorMessage,
5092
+ content: errorMessage2,
5080
5093
  sessionId: session.sessionId
5081
5094
  };
5082
5095
  return {
5083
5096
  success: false,
5084
5097
  sessionId: session.sessionId,
5085
5098
  usage: { inputTokens: 0, outputTokens: 0, totalTokens: 0 },
5086
- error: errorMessage
5099
+ error: errorMessage2
5087
5100
  };
5088
5101
  }
5089
5102
  const usage = { inputTokens, outputTokens, totalTokens };
@@ -5425,30 +5438,8 @@ var SshBackend = class {
5425
5438
  config;
5426
5439
  spawnImpl;
5427
5440
  constructor(config) {
5428
- if (!config.host || typeof config.host !== "string") {
5429
- throw new Error("SshBackend: `host` is required");
5430
- }
5431
- if (FORBIDDEN_HOST_CHARS.test(config.host) || config.host.startsWith("-")) {
5432
- throw new Error(
5433
- `SshBackend: invalid host '${config.host}' (contains shell metacharacters or starts with '-')`
5434
- );
5435
- }
5436
- if (!config.remoteCommand || typeof config.remoteCommand !== "string") {
5437
- throw new Error("SshBackend: `remoteCommand` is required");
5438
- }
5439
- if (config.user !== void 0 && /[\s;&|`$]/.test(config.user)) {
5440
- throw new Error(`SshBackend: invalid user '${config.user}'`);
5441
- }
5442
- this.config = {
5443
- host: config.host,
5444
- remoteCommand: config.remoteCommand,
5445
- sshBinary: config.sshBinary ?? "ssh",
5446
- sshOptions: config.sshOptions ?? [],
5447
- timeoutMs: config.timeoutMs ?? DEFAULT_TIMEOUT_MS2,
5448
- ...config.user !== void 0 ? { user: config.user } : {},
5449
- ...config.port !== void 0 ? { port: config.port } : {},
5450
- ...config.identityFile !== void 0 ? { identityFile: config.identityFile } : {}
5451
- };
5441
+ validateSshConfig(config);
5442
+ this.config = normalizeSshConfig(config);
5452
5443
  this.spawnImpl = config.spawnImpl ?? spawn3;
5453
5444
  }
5454
5445
  /**
@@ -5600,6 +5591,34 @@ var SshBackend = class {
5600
5591
  });
5601
5592
  }
5602
5593
  };
5594
+ function validateSshConfig(config) {
5595
+ if (!config.host || typeof config.host !== "string") {
5596
+ throw new Error("SshBackend: `host` is required");
5597
+ }
5598
+ if (FORBIDDEN_HOST_CHARS.test(config.host) || config.host.startsWith("-")) {
5599
+ throw new Error(
5600
+ `SshBackend: invalid host '${config.host}' (contains shell metacharacters or starts with '-')`
5601
+ );
5602
+ }
5603
+ if (!config.remoteCommand || typeof config.remoteCommand !== "string") {
5604
+ throw new Error("SshBackend: `remoteCommand` is required");
5605
+ }
5606
+ if (config.user !== void 0 && /[\s;&|`$]/.test(config.user)) {
5607
+ throw new Error(`SshBackend: invalid user '${config.user}'`);
5608
+ }
5609
+ }
5610
+ function normalizeSshConfig(config) {
5611
+ return {
5612
+ host: config.host,
5613
+ remoteCommand: config.remoteCommand,
5614
+ sshBinary: config.sshBinary ?? "ssh",
5615
+ sshOptions: config.sshOptions ?? [],
5616
+ timeoutMs: config.timeoutMs ?? DEFAULT_TIMEOUT_MS2,
5617
+ ...config.user !== void 0 ? { user: config.user } : {},
5618
+ ...config.port !== void 0 ? { port: config.port } : {},
5619
+ ...config.identityFile !== void 0 ? { identityFile: config.identityFile } : {}
5620
+ };
5621
+ }
5603
5622
  function errResult(sessionId, message) {
5604
5623
  return {
5605
5624
  success: false,
@@ -5709,23 +5728,8 @@ var OciServerlessBackend = class extends ServerlessBackend {
5709
5728
  envSource;
5710
5729
  constructor(config) {
5711
5730
  super();
5712
- if (!config.image || typeof config.image !== "string") {
5713
- throw new Error("OciServerlessBackend: `image` is required");
5714
- }
5715
- if (FORBIDDEN_IMAGE_CHARS.test(config.image) || config.image.startsWith("-")) {
5716
- throw new Error(
5717
- `OciServerlessBackend: invalid image '${config.image}' (contains shell metacharacters or starts with '-')`
5718
- );
5719
- }
5720
- this.config = {
5721
- image: config.image,
5722
- pullPolicy: config.pullPolicy ?? "if-not-present",
5723
- runtime: config.runtime ?? "docker",
5724
- envPassthrough: config.envPassthrough ?? [],
5725
- timeoutMs: config.timeoutMs ?? DEFAULT_OCI_TIMEOUT_MS,
5726
- extraArgs: sanitizeExtraArgs(config.extraArgs),
5727
- ...config.registry !== void 0 ? { registry: config.registry } : {}
5728
- };
5731
+ validateOciImage(config.image);
5732
+ this.config = resolveOciConfig(config);
5729
5733
  this.spawnImpl = config.spawnImpl ?? spawn4;
5730
5734
  this.envSource = config.envSource ?? process.env;
5731
5735
  }
@@ -5888,6 +5892,27 @@ var OciServerlessBackend = class extends ServerlessBackend {
5888
5892
  });
5889
5893
  }
5890
5894
  };
5895
+ function validateOciImage(image) {
5896
+ if (!image || typeof image !== "string") {
5897
+ throw new Error("OciServerlessBackend: `image` is required");
5898
+ }
5899
+ if (FORBIDDEN_IMAGE_CHARS.test(image) || image.startsWith("-")) {
5900
+ throw new Error(
5901
+ `OciServerlessBackend: invalid image '${image}' (contains shell metacharacters or starts with '-')`
5902
+ );
5903
+ }
5904
+ }
5905
+ function resolveOciConfig(config) {
5906
+ return {
5907
+ image: config.image,
5908
+ pullPolicy: config.pullPolicy ?? "if-not-present",
5909
+ runtime: config.runtime ?? "docker",
5910
+ envPassthrough: config.envPassthrough ?? [],
5911
+ timeoutMs: config.timeoutMs ?? DEFAULT_OCI_TIMEOUT_MS,
5912
+ extraArgs: sanitizeExtraArgs(config.extraArgs),
5913
+ ...config.registry !== void 0 ? { registry: config.registry } : {}
5914
+ };
5915
+ }
5891
5916
  function sanitizeExtraArgs(extraArgs) {
5892
5917
  if (!extraArgs) return [];
5893
5918
  return extraArgs.filter((arg) => !BLOCKED_DOCKER_FLAGS.some((flag) => arg.startsWith(flag)));
@@ -5959,78 +5984,96 @@ function makeGetModel(model) {
5959
5984
  if (Array.isArray(model) && model.length > 0) return () => model[0] ?? null;
5960
5985
  return () => null;
5961
5986
  }
5987
+ function createClaudeBackend(def, options) {
5988
+ return new ClaudeBackend(def.command ?? "claude", {
5989
+ ...options.cacheMetrics ? { cacheMetrics: options.cacheMetrics } : {}
5990
+ });
5991
+ }
5992
+ function createAnthropicBackend(def) {
5993
+ return new AnthropicBackend({
5994
+ model: def.model,
5995
+ ...def.apiKey !== void 0 ? { apiKey: def.apiKey } : {}
5996
+ });
5997
+ }
5998
+ function createOpenAIBackend(def) {
5999
+ return new OpenAIBackend({
6000
+ model: def.model,
6001
+ ...def.apiKey !== void 0 ? { apiKey: def.apiKey } : {}
6002
+ });
6003
+ }
6004
+ function createGeminiBackend(def) {
6005
+ return new GeminiBackend({
6006
+ model: def.model,
6007
+ ...def.apiKey !== void 0 ? { apiKey: def.apiKey } : {}
6008
+ });
6009
+ }
6010
+ function createLocalBackend(def) {
6011
+ const isArray = Array.isArray(def.model);
6012
+ return new LocalBackend({
6013
+ endpoint: def.endpoint,
6014
+ ...typeof def.model === "string" ? { model: def.model } : {},
6015
+ ...isArray ? { getModel: makeGetModel(def.model) } : {},
6016
+ ...def.apiKey !== void 0 ? { apiKey: def.apiKey } : {},
6017
+ ...def.timeoutMs !== void 0 ? { timeoutMs: def.timeoutMs } : {}
6018
+ });
6019
+ }
6020
+ function createPiBackend(def) {
6021
+ const isArray = Array.isArray(def.model);
6022
+ return new PiBackend({
6023
+ endpoint: def.endpoint,
6024
+ ...typeof def.model === "string" ? { model: def.model } : {},
6025
+ ...isArray ? { getModel: makeGetModel(def.model) } : {},
6026
+ ...def.apiKey !== void 0 ? { apiKey: def.apiKey } : {},
6027
+ ...def.timeoutMs !== void 0 ? { timeoutMs: def.timeoutMs } : {}
6028
+ });
6029
+ }
6030
+ function createSshBackend(def) {
6031
+ return new SshBackend({
6032
+ host: def.host,
6033
+ remoteCommand: def.remoteCommand,
6034
+ ...def.user !== void 0 ? { user: def.user } : {},
6035
+ ...def.port !== void 0 ? { port: def.port } : {},
6036
+ ...def.identityFile !== void 0 ? { identityFile: def.identityFile } : {},
6037
+ ...def.sshOptions !== void 0 ? { sshOptions: def.sshOptions } : {},
6038
+ ...def.sshBinary !== void 0 ? { sshBinary: def.sshBinary } : {}
6039
+ });
6040
+ }
6041
+ function createServerlessBackend(def) {
6042
+ switch (def.adapter) {
6043
+ case "oci":
6044
+ return new OciServerlessBackend({
6045
+ image: def.image,
6046
+ ...def.registry !== void 0 ? { registry: def.registry } : {},
6047
+ ...def.pullPolicy !== void 0 ? { pullPolicy: def.pullPolicy } : {},
6048
+ ...def.envPassthrough !== void 0 ? { envPassthrough: def.envPassthrough } : {},
6049
+ ...def.runtime !== void 0 ? { runtime: def.runtime } : {}
6050
+ });
6051
+ default: {
6052
+ const exhaustive = def.adapter;
6053
+ throw new Error(`createBackend: unknown serverless adapter ${JSON.stringify(exhaustive)}`);
6054
+ }
6055
+ }
6056
+ }
5962
6057
  function createBackend(def, options = {}) {
5963
6058
  switch (def.type) {
5964
6059
  case "mock":
5965
6060
  return new MockBackend();
5966
6061
  case "claude":
5967
- return new ClaudeBackend(def.command ?? "claude", {
5968
- ...options.cacheMetrics ? { cacheMetrics: options.cacheMetrics } : {}
5969
- });
6062
+ return createClaudeBackend(def, options);
5970
6063
  case "anthropic":
5971
- return new AnthropicBackend({
5972
- model: def.model,
5973
- ...def.apiKey !== void 0 ? { apiKey: def.apiKey } : {}
5974
- });
6064
+ return createAnthropicBackend(def);
5975
6065
  case "openai":
5976
- return new OpenAIBackend({
5977
- model: def.model,
5978
- ...def.apiKey !== void 0 ? { apiKey: def.apiKey } : {}
5979
- });
6066
+ return createOpenAIBackend(def);
5980
6067
  case "gemini":
5981
- return new GeminiBackend({
5982
- model: def.model,
5983
- ...def.apiKey !== void 0 ? { apiKey: def.apiKey } : {}
5984
- });
5985
- case "local": {
5986
- const isArray = Array.isArray(def.model);
5987
- return new LocalBackend({
5988
- endpoint: def.endpoint,
5989
- ...typeof def.model === "string" ? { model: def.model } : {},
5990
- ...isArray ? { getModel: makeGetModel(def.model) } : {},
5991
- ...def.apiKey !== void 0 ? { apiKey: def.apiKey } : {},
5992
- ...def.timeoutMs !== void 0 ? { timeoutMs: def.timeoutMs } : {}
5993
- });
5994
- }
5995
- case "pi": {
5996
- const isArray = Array.isArray(def.model);
5997
- return new PiBackend({
5998
- endpoint: def.endpoint,
5999
- ...typeof def.model === "string" ? { model: def.model } : {},
6000
- ...isArray ? { getModel: makeGetModel(def.model) } : {},
6001
- ...def.apiKey !== void 0 ? { apiKey: def.apiKey } : {},
6002
- ...def.timeoutMs !== void 0 ? { timeoutMs: def.timeoutMs } : {}
6003
- });
6004
- }
6005
- case "ssh": {
6006
- return new SshBackend({
6007
- host: def.host,
6008
- remoteCommand: def.remoteCommand,
6009
- ...def.user !== void 0 ? { user: def.user } : {},
6010
- ...def.port !== void 0 ? { port: def.port } : {},
6011
- ...def.identityFile !== void 0 ? { identityFile: def.identityFile } : {},
6012
- ...def.sshOptions !== void 0 ? { sshOptions: def.sshOptions } : {},
6013
- ...def.sshBinary !== void 0 ? { sshBinary: def.sshBinary } : {}
6014
- });
6015
- }
6016
- case "serverless": {
6017
- switch (def.adapter) {
6018
- case "oci":
6019
- return new OciServerlessBackend({
6020
- image: def.image,
6021
- ...def.registry !== void 0 ? { registry: def.registry } : {},
6022
- ...def.pullPolicy !== void 0 ? { pullPolicy: def.pullPolicy } : {},
6023
- ...def.envPassthrough !== void 0 ? { envPassthrough: def.envPassthrough } : {},
6024
- ...def.runtime !== void 0 ? { runtime: def.runtime } : {}
6025
- });
6026
- default: {
6027
- const exhaustive = def.adapter;
6028
- throw new Error(
6029
- `createBackend: unknown serverless adapter ${JSON.stringify(exhaustive)}`
6030
- );
6031
- }
6032
- }
6033
- }
6068
+ return createGeminiBackend(def);
6069
+ case "local":
6070
+ return createLocalBackend(def);
6071
+ case "pi":
6072
+ return createPiBackend(def);
6073
+ case "ssh":
6074
+ return createSshBackend(def);
6075
+ case "serverless":
6076
+ return createServerlessBackend(def);
6034
6077
  default: {
6035
6078
  const exhaustive = def;
6036
6079
  throw new Error(`createBackend: unknown backend type ${JSON.stringify(exhaustive)}`);
@@ -9618,26 +9661,41 @@ function hasScope(held, required) {
9618
9661
  if (held.includes("admin")) return true;
9619
9662
  return held.includes(required);
9620
9663
  }
9621
- function requiredScopeForRoute(method, path24) {
9622
- const bridgeScope = requiredBridgeScope(method, path24);
9623
- if (bridgeScope) return bridgeScope;
9664
+ function exactScopeForRoute(method, path24) {
9624
9665
  if (path24 === "/api/v1/auth/token" && method === "POST") return "admin";
9625
9666
  if (path24 === "/api/v1/auth/tokens" && method === "GET") return "admin";
9626
9667
  if (/^\/api\/v1\/auth\/tokens\/[^/]+$/.test(path24) && method === "DELETE") return "admin";
9627
9668
  if ((path24 === "/api/state" || path24 === "/api/v1/state") && method === "GET") return "read-status";
9628
- if (path24.startsWith("/api/interactions")) return "resolve-interaction";
9629
- if (path24.startsWith("/api/plans")) return "read-status";
9630
- if (path24.startsWith("/api/analyze") || path24.startsWith("/api/analyses")) return "read-status";
9631
- if (path24.startsWith("/api/roadmap-actions")) return "modify-roadmap";
9632
- if (path24.startsWith("/api/dispatch-actions")) return "trigger-job";
9633
- if (path24.startsWith("/api/local-model") || path24.startsWith("/api/local-models"))
9634
- return "read-status";
9635
- if (path24.startsWith("/api/maintenance")) return "trigger-job";
9636
- if (path24.startsWith("/api/streams")) return "read-status";
9637
- if (path24.startsWith("/api/sessions")) return "read-status";
9638
- if (path24 === "/api/chat" || path24.startsWith("/api/chat-proxy")) return "trigger-job";
9639
9669
  return null;
9640
9670
  }
9671
+ var PREFIX_SCOPES = [
9672
+ ["/api/interactions", "resolve-interaction"],
9673
+ ["/api/plans", "read-status"],
9674
+ ["/api/analyze", "read-status"],
9675
+ ["/api/analyses", "read-status"],
9676
+ ["/api/roadmap-actions", "modify-roadmap"],
9677
+ ["/api/dispatch-actions", "trigger-job"],
9678
+ ["/api/local-model", "read-status"],
9679
+ ["/api/local-models", "read-status"],
9680
+ ["/api/maintenance", "trigger-job"],
9681
+ ["/api/streams", "read-status"],
9682
+ ["/api/sessions", "read-status"],
9683
+ ["/api/chat-proxy", "trigger-job"]
9684
+ ];
9685
+ function prefixScopeForPath(path24) {
9686
+ if (path24 === "/api/chat") return "trigger-job";
9687
+ for (const [prefix, scope] of PREFIX_SCOPES) {
9688
+ if (path24.startsWith(prefix)) return scope;
9689
+ }
9690
+ return null;
9691
+ }
9692
+ function requiredScopeForRoute(method, path24) {
9693
+ const bridgeScope = requiredBridgeScope(method, path24);
9694
+ if (bridgeScope) return bridgeScope;
9695
+ const exactScope = exactScopeForRoute(method, path24);
9696
+ if (exactScope) return exactScope;
9697
+ return prefixScopeForPath(path24);
9698
+ }
9641
9699
 
9642
9700
  // src/server/http.ts
9643
9701
  var RATE_LIMIT = Number(process.env["HARNESS_RATE_LIMIT"]) || 100;
@@ -10596,79 +10654,102 @@ function buildAttributes(payload, extras = {}) {
10596
10654
  }
10597
10655
  return attrs;
10598
10656
  }
10657
+ function extractIds(payload) {
10658
+ const correlationId = typeof payload["correlationId"] === "string" ? payload["correlationId"] : void 0;
10659
+ const taskId = typeof payload["taskId"] === "string" ? payload["taskId"] : void 0;
10660
+ return { correlationId, taskId };
10661
+ }
10662
+ function resolveActiveRun(registry, correlationId, taskId) {
10663
+ return registry.resolve({
10664
+ ...correlationId !== void 0 ? { correlationId } : {},
10665
+ ...taskId !== void 0 ? { taskId } : {}
10666
+ });
10667
+ }
10668
+ function openMaintenanceSpan(registry, correlationId, taskId) {
10669
+ const traceId = newTraceId();
10670
+ const spanId = newSpanId();
10671
+ const key = correlationId ?? taskId ?? `run_${spanId}`;
10672
+ registry.open(key, { traceId, spanId });
10673
+ return { traceId, spanId };
10674
+ }
10675
+ function closeMaintenanceSpan(registry, topic, correlationId, taskId) {
10676
+ const existing = resolveActiveRun(registry, correlationId, taskId);
10677
+ const traceId = existing?.traceId ?? newTraceId();
10678
+ const spanId = existing?.spanId ?? newSpanId();
10679
+ const statusCode = topic === TOPICS.MAINTENANCE_ERROR ? 2 : 1;
10680
+ const key = correlationId ?? taskId ?? "";
10681
+ if (key) registry.close(key);
10682
+ return { traceId, spanId, statusCode };
10683
+ }
10684
+ function childSpan(registry, correlationId, taskId) {
10685
+ const parent = resolveActiveRun(registry, correlationId, taskId);
10686
+ const plan = {
10687
+ traceId: parent?.traceId ?? newTraceId(),
10688
+ spanId: newSpanId()
10689
+ };
10690
+ if (parent?.spanId !== void 0) plan.parentSpanId = parent.spanId;
10691
+ return plan;
10692
+ }
10693
+ function planSpan(registry, topic, correlationId, taskId) {
10694
+ if (topic === TOPICS.MAINTENANCE_STARTED) {
10695
+ return openMaintenanceSpan(registry, correlationId, taskId);
10696
+ }
10697
+ if (topic === TOPICS.MAINTENANCE_COMPLETED || topic === TOPICS.MAINTENANCE_ERROR) {
10698
+ return closeMaintenanceSpan(registry, topic, correlationId, taskId);
10699
+ }
10700
+ return childSpan(registry, correlationId, taskId);
10701
+ }
10702
+ function buildSpan(topic, plan, payload) {
10703
+ const startNs = nowNs();
10704
+ return {
10705
+ traceId: plan.traceId,
10706
+ spanId: plan.spanId,
10707
+ ...plan.parentSpanId !== void 0 ? { parentSpanId: plan.parentSpanId } : {},
10708
+ name: SPAN_NAME[topic],
10709
+ kind: SpanKind.INTERNAL,
10710
+ startTimeNs: startNs,
10711
+ endTimeNs: startNs,
10712
+ attributes: buildAttributes(payload, { "harness.topic": topic }),
10713
+ ...plan.statusCode !== void 0 ? { statusCode: plan.statusCode } : {}
10714
+ };
10715
+ }
10716
+ function buildGatewayEvent(topic, payload, correlationId) {
10717
+ return {
10718
+ id: newEventId2(),
10719
+ type: TELEMETRY_TYPE[topic],
10720
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
10721
+ data: payload,
10722
+ ...correlationId !== void 0 ? { correlationId } : {}
10723
+ };
10724
+ }
10725
+ async function enqueueToMatchingSubs(store, webhookDelivery, event) {
10726
+ const subs = await store.listForEvent(event.type);
10727
+ if (subs.length === 0) return;
10728
+ const filtered = subs.filter((sub) => sub.events.some((p) => eventMatches(p, event.type)));
10729
+ for (const sub of filtered) {
10730
+ webhookDelivery.enqueue(sub, event);
10731
+ }
10732
+ }
10733
+ function makeTelemetryHandler(ctx, topic) {
10734
+ return (data) => {
10735
+ const payload = data ?? {};
10736
+ const { correlationId, taskId } = extractIds(payload);
10737
+ const plan = planSpan(ctx.registry, topic, correlationId, taskId);
10738
+ ctx.exporter.push(buildSpan(topic, plan, payload));
10739
+ void enqueueToMatchingSubs(
10740
+ ctx.store,
10741
+ ctx.webhookDelivery,
10742
+ buildGatewayEvent(topic, payload, correlationId)
10743
+ );
10744
+ };
10745
+ }
10599
10746
  function wireTelemetryFanout(params) {
10600
10747
  const { bus, exporter, webhookDelivery, store } = params;
10601
10748
  const registry = new ActiveRunRegistry();
10602
10749
  const handlers = [];
10603
- const enqueueToMatchingSubs = async (event) => {
10604
- const subs = await store.listForEvent(event.type);
10605
- if (subs.length === 0) return;
10606
- const filtered = subs.filter((sub) => sub.events.some((p) => eventMatches(p, event.type)));
10607
- for (const sub of filtered) {
10608
- webhookDelivery.enqueue(sub, event);
10609
- }
10610
- };
10611
- const makeHandler = (topic) => (data) => {
10612
- const payload = data ?? {};
10613
- const correlationId = typeof payload["correlationId"] === "string" ? payload["correlationId"] : void 0;
10614
- const taskId = typeof payload["taskId"] === "string" ? payload["taskId"] : void 0;
10615
- let traceId;
10616
- let spanId;
10617
- let parentSpanId;
10618
- let statusCode;
10619
- if (topic === TOPICS.MAINTENANCE_STARTED) {
10620
- traceId = newTraceId();
10621
- spanId = newSpanId();
10622
- const key = correlationId ?? taskId ?? `run_${spanId}`;
10623
- registry.open(key, { traceId, spanId });
10624
- } else if (topic === TOPICS.MAINTENANCE_COMPLETED || topic === TOPICS.MAINTENANCE_ERROR) {
10625
- const existing = registry.resolve({
10626
- ...correlationId !== void 0 ? { correlationId } : {},
10627
- ...taskId !== void 0 ? { taskId } : {}
10628
- });
10629
- if (existing !== void 0) {
10630
- traceId = existing.traceId;
10631
- spanId = existing.spanId;
10632
- } else {
10633
- traceId = newTraceId();
10634
- spanId = newSpanId();
10635
- }
10636
- statusCode = topic === TOPICS.MAINTENANCE_ERROR ? 2 : 1;
10637
- const key = correlationId ?? taskId ?? "";
10638
- if (key) registry.close(key);
10639
- } else {
10640
- const parent = registry.resolve({
10641
- ...correlationId !== void 0 ? { correlationId } : {},
10642
- ...taskId !== void 0 ? { taskId } : {}
10643
- });
10644
- traceId = parent?.traceId ?? newTraceId();
10645
- spanId = newSpanId();
10646
- parentSpanId = parent?.spanId;
10647
- }
10648
- const startNs = nowNs();
10649
- const span = {
10650
- traceId,
10651
- spanId,
10652
- ...parentSpanId !== void 0 ? { parentSpanId } : {},
10653
- name: SPAN_NAME[topic],
10654
- kind: SpanKind.INTERNAL,
10655
- startTimeNs: startNs,
10656
- endTimeNs: startNs,
10657
- attributes: buildAttributes(payload, { "harness.topic": topic }),
10658
- ...statusCode !== void 0 ? { statusCode } : {}
10659
- };
10660
- exporter.push(span);
10661
- const gatewayEvent = {
10662
- id: newEventId2(),
10663
- type: TELEMETRY_TYPE[topic],
10664
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
10665
- data: payload,
10666
- ...correlationId !== void 0 ? { correlationId } : {}
10667
- };
10668
- void enqueueToMatchingSubs(gatewayEvent);
10669
- };
10750
+ const ctx = { registry, exporter, webhookDelivery, store };
10670
10751
  for (const topic of Object.values(TOPICS)) {
10671
- const fn = makeHandler(topic);
10752
+ const fn = makeTelemetryHandler(ctx, topic);
10672
10753
  bus.on(topic, fn);
10673
10754
  handlers.push({ topic, fn });
10674
10755
  }
@@ -12371,39 +12452,50 @@ function classifyCheckExecutionFailure(output) {
12371
12452
  function parseStatusLine(output) {
12372
12453
  const lines = output.split("\n").map((l) => l.trim()).filter(Boolean);
12373
12454
  for (let i = lines.length - 1; i >= 0; i--) {
12374
- const line = lines[i];
12375
- if (!line || !line.startsWith("{") || !line.endsWith("}")) continue;
12376
- try {
12377
- const obj = JSON.parse(line);
12378
- const s = obj.status;
12379
- if (s === "success" || s === "skipped" || s === "failure" || s === "no-issues") {
12380
- const parsed = { status: s, rawStatus: s };
12381
- if (typeof obj.candidatesFound === "number") {
12382
- parsed.candidatesFound = obj.candidatesFound;
12383
- }
12384
- if (typeof obj.error === "string") {
12385
- parsed.error = obj.error;
12386
- }
12387
- if (typeof obj.reason === "string") {
12388
- parsed.reason = obj.reason;
12389
- }
12390
- if (typeof obj.detail === "string" && !parsed.error) {
12391
- parsed.error = `${parsed.reason ?? "skipped"}: ${obj.detail}`;
12392
- }
12393
- return parsed;
12394
- }
12395
- if (s === "updated" || s === "no-op") {
12396
- return { status: "success", rawStatus: s };
12397
- }
12398
- if (s === "error") {
12399
- const message = typeof obj.message === "string" ? obj.message : "unknown error";
12400
- return { status: "failure", error: message, rawStatus: "error" };
12401
- }
12402
- } catch {
12403
- }
12455
+ const parsed = parseStatusObjectLine(lines[i]);
12456
+ if (parsed) return parsed;
12404
12457
  }
12405
12458
  return null;
12406
12459
  }
12460
+ function parseStatusObjectLine(line) {
12461
+ if (!line || !line.startsWith("{") || !line.endsWith("}")) return null;
12462
+ let obj;
12463
+ try {
12464
+ obj = JSON.parse(line);
12465
+ } catch {
12466
+ return null;
12467
+ }
12468
+ return classifyStatusObject(obj);
12469
+ }
12470
+ var PHASE_STATUSES = /* @__PURE__ */ new Set(["success", "skipped", "failure", "no-issues"]);
12471
+ var SYNC_SUCCESS_STATUSES = /* @__PURE__ */ new Set(["updated", "no-op"]);
12472
+ function classifyStatusObject(obj) {
12473
+ const s = obj.status;
12474
+ if (typeof s !== "string") return null;
12475
+ if (PHASE_STATUSES.has(s)) return buildPhaseStatus(s, obj);
12476
+ if (SYNC_SUCCESS_STATUSES.has(s)) return { status: "success", rawStatus: s };
12477
+ if (s === "error") {
12478
+ const message = typeof obj.message === "string" ? obj.message : "unknown error";
12479
+ return { status: "failure", error: message, rawStatus: "error" };
12480
+ }
12481
+ return null;
12482
+ }
12483
+ function buildPhaseStatus(status, obj) {
12484
+ const parsed = { status, rawStatus: status };
12485
+ if (typeof obj.candidatesFound === "number") {
12486
+ parsed.candidatesFound = obj.candidatesFound;
12487
+ }
12488
+ if (typeof obj.error === "string") {
12489
+ parsed.error = obj.error;
12490
+ }
12491
+ if (typeof obj.reason === "string") {
12492
+ parsed.reason = obj.reason;
12493
+ }
12494
+ if (typeof obj.detail === "string" && !parsed.error) {
12495
+ parsed.error = `${parsed.reason ?? "skipped"}: ${obj.detail}`;
12496
+ }
12497
+ return parsed;
12498
+ }
12407
12499
 
12408
12500
  // src/maintenance/check-script-runner.ts
12409
12501
  import { execFile as execFile6 } from "child_process";
@@ -13298,22 +13390,7 @@ var Orchestrator = class extends EventEmitter {
13298
13390
  });
13299
13391
  webhookDelivery.start();
13300
13392
  this.setupNotifications(config.notifications);
13301
- const otlpCfg = config.telemetry?.export?.otlp;
13302
- if (otlpCfg) {
13303
- this.otlpExporter = new OTLPExporter({
13304
- endpoint: otlpCfg.endpoint,
13305
- ...otlpCfg.enabled !== void 0 ? { enabled: otlpCfg.enabled } : {},
13306
- ...otlpCfg.headers !== void 0 ? { headers: otlpCfg.headers } : {},
13307
- ...otlpCfg.flushIntervalMs !== void 0 ? { flushIntervalMs: otlpCfg.flushIntervalMs } : {},
13308
- ...otlpCfg.batchSize !== void 0 ? { batchSize: otlpCfg.batchSize } : {}
13309
- });
13310
- this.telemetryFanoutOff = wireTelemetryFanout({
13311
- bus: this,
13312
- exporter: this.otlpExporter,
13313
- webhookDelivery,
13314
- store: webhookStore
13315
- });
13316
- }
13393
+ this.setupTelemetryExport(config, webhookStore, webhookDelivery);
13317
13394
  this.server = new OrchestratorServer(this, config.server.port, {
13318
13395
  interactionQueue: this.interactionQueue,
13319
13396
  webhooks: {
@@ -13336,24 +13413,8 @@ var Orchestrator = class extends EventEmitter {
13336
13413
  analysisArchive: this.analysisArchive,
13337
13414
  roadmapPath: config.tracker.filePath ?? null,
13338
13415
  dispatchAdHoc: this.dispatchAdHoc.bind(this),
13339
- getLocalModelStatus: () => {
13340
- const first = this.localResolvers.values().next();
13341
- return first.done ? null : first.value.getStatus();
13342
- },
13343
- getLocalModelStatuses: () => {
13344
- const backends = this.config.agent.backends ?? {};
13345
- const out = [];
13346
- for (const [name, resolver] of this.localResolvers) {
13347
- const def = backends[name];
13348
- if (!def || def.type !== "local" && def.type !== "pi") continue;
13349
- out.push({
13350
- ...resolver.getStatus(),
13351
- backendName: name,
13352
- endpoint: def.endpoint
13353
- });
13354
- }
13355
- return out;
13356
- }
13416
+ getLocalModelStatus: () => this.getFirstLocalModelStatus(),
13417
+ getLocalModelStatuses: () => this.buildLocalModelStatuses()
13357
13418
  });
13358
13419
  this.server.setRecorder(this.recorder);
13359
13420
  this.interactionQueue.onPush((interaction) => {
@@ -13361,6 +13422,56 @@ var Orchestrator = class extends EventEmitter {
13361
13422
  });
13362
13423
  }
13363
13424
  }
13425
+ /**
13426
+ * Phase 5: construct the OTLP/HTTP trace exporter and wire telemetry fanout.
13427
+ * Only fires when the operator configures `telemetry.export.otlp` in
13428
+ * harness.config.json. Extracted from the server-init block in the
13429
+ * constructor to keep that block's cyclomatic complexity under threshold.
13430
+ */
13431
+ setupTelemetryExport(config, webhookStore, webhookDelivery) {
13432
+ const otlpCfg = config.telemetry?.export?.otlp;
13433
+ if (!otlpCfg) return;
13434
+ this.otlpExporter = new OTLPExporter({
13435
+ endpoint: otlpCfg.endpoint,
13436
+ ...otlpCfg.enabled !== void 0 ? { enabled: otlpCfg.enabled } : {},
13437
+ ...otlpCfg.headers !== void 0 ? { headers: otlpCfg.headers } : {},
13438
+ ...otlpCfg.flushIntervalMs !== void 0 ? { flushIntervalMs: otlpCfg.flushIntervalMs } : {},
13439
+ ...otlpCfg.batchSize !== void 0 ? { batchSize: otlpCfg.batchSize } : {}
13440
+ });
13441
+ this.telemetryFanoutOff = wireTelemetryFanout({
13442
+ bus: this,
13443
+ exporter: this.otlpExporter,
13444
+ webhookDelivery,
13445
+ store: webhookStore
13446
+ });
13447
+ }
13448
+ /**
13449
+ * Deprecated alias for /api/v1/local-model/status (Spec 1 endpoint retained
13450
+ * as a compat shim per spec line 35; superseded by getLocalModelStatuses for
13451
+ * the multi-local UI). Returns the first-registered resolver's status.
13452
+ */
13453
+ getFirstLocalModelStatus() {
13454
+ const first = this.localResolvers.values().next();
13455
+ return first.done ? null : first.value.getStatus();
13456
+ }
13457
+ /**
13458
+ * SC38: build NamedLocalModelStatus[] from each registered resolver, tagged
13459
+ * with its backendName + endpoint from the config.
13460
+ */
13461
+ buildLocalModelStatuses() {
13462
+ const backends = this.config.agent.backends ?? {};
13463
+ const out = [];
13464
+ for (const [name, resolver] of this.localResolvers) {
13465
+ const def = backends[name];
13466
+ if (!def || def.type !== "local" && def.type !== "pi") continue;
13467
+ out.push({
13468
+ ...resolver.getStatus(),
13469
+ backendName: name,
13470
+ endpoint: def.endpoint
13471
+ });
13472
+ }
13473
+ return out;
13474
+ }
13364
13475
  createTracker() {
13365
13476
  if (this.config.tracker.kind === "github-issues") {
13366
13477
  const trackerCfg = {