@harness-engineering/orchestrator 0.4.2 → 0.4.4
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.d.mts +141 -5
- package/dist/index.d.ts +141 -5
- package/dist/index.js +1715 -144
- package/dist/index.mjs +1697 -123
- package/package.json +12 -5
package/dist/index.js
CHANGED
|
@@ -35,14 +35,18 @@ __export(index_exports, {
|
|
|
35
35
|
ClaimManager: () => ClaimManager,
|
|
36
36
|
InteractionQueue: () => InteractionQueue,
|
|
37
37
|
LinearGraphQLStub: () => LinearGraphQLStub,
|
|
38
|
+
MAX_ATTEMPTS: () => MAX_ATTEMPTS,
|
|
38
39
|
MockBackend: () => MockBackend,
|
|
39
40
|
ORCHESTRATOR_IDENTITY_FILE: () => ORCHESTRATOR_IDENTITY_FILE,
|
|
40
41
|
Orchestrator: () => Orchestrator,
|
|
41
42
|
OrchestratorBackendFactory: () => OrchestratorBackendFactory,
|
|
42
43
|
PRDetector: () => PRDetector,
|
|
43
44
|
PromptRenderer: () => PromptRenderer,
|
|
45
|
+
RETRY_DELAYS_MS: () => RETRY_DELAYS_MS,
|
|
44
46
|
RoadmapTrackerAdapter: () => RoadmapTrackerAdapter,
|
|
45
47
|
StreamRecorder: () => StreamRecorder,
|
|
48
|
+
TokenStore: () => TokenStore,
|
|
49
|
+
WebhookQueue: () => WebhookQueue,
|
|
46
50
|
WorkflowLoader: () => WorkflowLoader,
|
|
47
51
|
WorkspaceHooks: () => WorkspaceHooks,
|
|
48
52
|
WorkspaceManager: () => WorkspaceManager,
|
|
@@ -338,11 +342,18 @@ var path = __toESM(require("path"));
|
|
|
338
342
|
var InteractionQueue = class {
|
|
339
343
|
dir;
|
|
340
344
|
pushListeners = [];
|
|
345
|
+
emitter;
|
|
341
346
|
/**
|
|
342
347
|
* @param dir - Directory path for storing interaction JSON files
|
|
348
|
+
* @param emitter - Optional event bus that receives `interaction.created`
|
|
349
|
+
* and `interaction.resolved` events. When omitted, the queue behaves as
|
|
350
|
+
* it did pre-Phase-2 (no emission). Phase 2 Task 8 wires the
|
|
351
|
+
* orchestrator (itself an EventEmitter) in as the bus so the SSE
|
|
352
|
+
* handler (`GET /api/v1/events`) can fan these out to clients.
|
|
343
353
|
*/
|
|
344
|
-
constructor(dir) {
|
|
354
|
+
constructor(dir, emitter) {
|
|
345
355
|
this.dir = dir;
|
|
356
|
+
this.emitter = emitter ?? null;
|
|
346
357
|
}
|
|
347
358
|
/**
|
|
348
359
|
* Register a listener that fires after each push.
|
|
@@ -370,6 +381,13 @@ var InteractionQueue = class {
|
|
|
370
381
|
for (const listener of this.pushListeners) {
|
|
371
382
|
listener(interaction);
|
|
372
383
|
}
|
|
384
|
+
this.emitter?.emit("interaction.created", {
|
|
385
|
+
id: interaction.id,
|
|
386
|
+
issueId: interaction.issueId,
|
|
387
|
+
type: interaction.type,
|
|
388
|
+
status: interaction.status,
|
|
389
|
+
createdAt: interaction.createdAt
|
|
390
|
+
});
|
|
373
391
|
}
|
|
374
392
|
/**
|
|
375
393
|
* List all interactions (regardless of status).
|
|
@@ -416,6 +434,13 @@ var InteractionQueue = class {
|
|
|
416
434
|
const interaction = JSON.parse(raw);
|
|
417
435
|
interaction.status = status;
|
|
418
436
|
await fs.writeFile(filePath, JSON.stringify(interaction, null, 2), "utf-8");
|
|
437
|
+
if (status === "resolved") {
|
|
438
|
+
this.emitter?.emit("interaction.resolved", {
|
|
439
|
+
id,
|
|
440
|
+
status: "resolved",
|
|
441
|
+
resolvedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
442
|
+
});
|
|
443
|
+
}
|
|
419
444
|
}
|
|
420
445
|
};
|
|
421
446
|
|
|
@@ -2710,8 +2735,8 @@ var PromptRenderer = class {
|
|
|
2710
2735
|
// src/orchestrator.ts
|
|
2711
2736
|
var import_node_events = require("events");
|
|
2712
2737
|
var path16 = __toESM(require("path"));
|
|
2713
|
-
var
|
|
2714
|
-
var
|
|
2738
|
+
var import_node_crypto15 = require("crypto");
|
|
2739
|
+
var import_core11 = require("@harness-engineering/core");
|
|
2715
2740
|
|
|
2716
2741
|
// src/intelligence/pipeline-runner.ts
|
|
2717
2742
|
var path7 = __toESM(require("path"));
|
|
@@ -3272,7 +3297,7 @@ var CompletionHandler = class {
|
|
|
3272
3297
|
};
|
|
3273
3298
|
|
|
3274
3299
|
// src/orchestrator.ts
|
|
3275
|
-
var
|
|
3300
|
+
var import_core12 = require("@harness-engineering/core");
|
|
3276
3301
|
|
|
3277
3302
|
// src/tracker/adapters/github-issues-issue-tracker.ts
|
|
3278
3303
|
var import_types9 = require("@harness-engineering/types");
|
|
@@ -3966,6 +3991,13 @@ function extractUsage(usage) {
|
|
|
3966
3991
|
cacheReadTokens: usage.cache_read_input_tokens ?? 0
|
|
3967
3992
|
};
|
|
3968
3993
|
}
|
|
3994
|
+
function recordCacheUsage(recorder, rawUsage) {
|
|
3995
|
+
if (!recorder || !rawUsage) return;
|
|
3996
|
+
const cacheRead = rawUsage.cache_read_input_tokens ?? 0;
|
|
3997
|
+
const cacheCreation = rawUsage.cache_creation_input_tokens ?? 0;
|
|
3998
|
+
if (cacheRead === 0 && cacheCreation === 0) return;
|
|
3999
|
+
recorder.record("anthropic", cacheRead > 0, cacheCreation, cacheRead);
|
|
4000
|
+
}
|
|
3969
4001
|
function extractToolResultText(blockContent) {
|
|
3970
4002
|
if (typeof blockContent === "string") return blockContent;
|
|
3971
4003
|
if (!Array.isArray(blockContent)) return "";
|
|
@@ -4047,8 +4079,10 @@ function mapClaudeEvent(rawEvent, sessionId) {
|
|
|
4047
4079
|
var ClaudeBackend = class {
|
|
4048
4080
|
name = "claude";
|
|
4049
4081
|
command;
|
|
4050
|
-
|
|
4051
|
-
|
|
4082
|
+
cacheMetrics;
|
|
4083
|
+
constructor(command = "claude", options = {}) {
|
|
4084
|
+
this.command = options.command ?? command;
|
|
4085
|
+
if (options.cacheMetrics) this.cacheMetrics = options.cacheMetrics;
|
|
4052
4086
|
}
|
|
4053
4087
|
async startSession(params) {
|
|
4054
4088
|
const session = {
|
|
@@ -4114,6 +4148,9 @@ var ClaudeBackend = class {
|
|
|
4114
4148
|
totalTokens: 0
|
|
4115
4149
|
}
|
|
4116
4150
|
};
|
|
4151
|
+
recordCacheUsage(this.cacheMetrics, rawEvent.usage);
|
|
4152
|
+
} else if (rawEvent.type === "assistant" && rawEvent.message?.stop_reason !== null && rawEvent.message?.stop_reason !== void 0) {
|
|
4153
|
+
recordCacheUsage(this.cacheMetrics, rawEvent.message?.usage);
|
|
4117
4154
|
}
|
|
4118
4155
|
for (const mapped of mapClaudeEvent(rawEvent, session.sessionId)) {
|
|
4119
4156
|
yield mapped;
|
|
@@ -4967,12 +5004,14 @@ function makeGetModel(model) {
|
|
|
4967
5004
|
if (Array.isArray(model) && model.length > 0) return () => model[0] ?? null;
|
|
4968
5005
|
return () => null;
|
|
4969
5006
|
}
|
|
4970
|
-
function createBackend(def) {
|
|
5007
|
+
function createBackend(def, options = {}) {
|
|
4971
5008
|
switch (def.type) {
|
|
4972
5009
|
case "mock":
|
|
4973
5010
|
return new MockBackend();
|
|
4974
5011
|
case "claude":
|
|
4975
|
-
return new ClaudeBackend(def.command ?? "claude"
|
|
5012
|
+
return new ClaudeBackend(def.command ?? "claude", {
|
|
5013
|
+
...options.cacheMetrics ? { cacheMetrics: options.cacheMetrics } : {}
|
|
5014
|
+
});
|
|
4976
5015
|
case "anthropic":
|
|
4977
5016
|
return new AnthropicBackend({
|
|
4978
5017
|
model: def.model,
|
|
@@ -5408,11 +5447,12 @@ var OrchestratorBackendFactory = class {
|
|
|
5408
5447
|
const def = this.router.resolveDefinition(useCase);
|
|
5409
5448
|
const name = this.router.resolve(useCase);
|
|
5410
5449
|
let backend;
|
|
5450
|
+
const createOpts = this.opts.cacheMetrics ? { cacheMetrics: this.opts.cacheMetrics } : {};
|
|
5411
5451
|
if ((def.type === "local" || def.type === "pi") && this.opts.getResolverModelFor) {
|
|
5412
5452
|
const getModel = this.opts.getResolverModelFor(name);
|
|
5413
|
-
backend = getModel ? this.buildLocalLikeWithResolver(def, getModel) : createBackend(def);
|
|
5453
|
+
backend = getModel ? this.buildLocalLikeWithResolver(def, getModel) : createBackend(def, createOpts);
|
|
5414
5454
|
} else {
|
|
5415
|
-
backend = createBackend(def);
|
|
5455
|
+
backend = createBackend(def, createOpts);
|
|
5416
5456
|
}
|
|
5417
5457
|
if (this.opts.sandboxPolicy === "docker" && this.opts.container) {
|
|
5418
5458
|
backend = this.wrapInContainer(backend);
|
|
@@ -5799,13 +5839,75 @@ function handleInteractionsRoute(req, res, queue) {
|
|
|
5799
5839
|
return false;
|
|
5800
5840
|
}
|
|
5801
5841
|
|
|
5802
|
-
// src/server/routes/
|
|
5842
|
+
// src/server/routes/v1/interactions-resolve.ts
|
|
5803
5843
|
var import_zod4 = require("zod");
|
|
5844
|
+
var BodySchema = import_zod4.z.object({ answer: import_zod4.z.unknown().optional() });
|
|
5845
|
+
var RESOLVE_PATH_RE = /^\/api\/v1\/interactions\/([a-zA-Z0-9_-]+)\/resolve(?:\?.*)?$/;
|
|
5846
|
+
function sendJSON(res, status, body) {
|
|
5847
|
+
res.writeHead(status, { "Content-Type": "application/json" });
|
|
5848
|
+
res.end(JSON.stringify(body));
|
|
5849
|
+
}
|
|
5850
|
+
function handleV1InteractionsResolveRoute(req, res, queue) {
|
|
5851
|
+
if (req.method !== "POST") return false;
|
|
5852
|
+
const match = RESOLVE_PATH_RE.exec(req.url ?? "");
|
|
5853
|
+
if (!match || !match[1]) return false;
|
|
5854
|
+
if (!queue) {
|
|
5855
|
+
sendJSON(res, 503, { error: "Interaction queue not available" });
|
|
5856
|
+
return true;
|
|
5857
|
+
}
|
|
5858
|
+
const id = match[1];
|
|
5859
|
+
void (async () => {
|
|
5860
|
+
let raw;
|
|
5861
|
+
try {
|
|
5862
|
+
raw = await readBody(req);
|
|
5863
|
+
} catch {
|
|
5864
|
+
sendJSON(res, 413, { error: "Body too large" });
|
|
5865
|
+
return;
|
|
5866
|
+
}
|
|
5867
|
+
if (raw.length > 0) {
|
|
5868
|
+
try {
|
|
5869
|
+
const json = JSON.parse(raw);
|
|
5870
|
+
const parsed = BodySchema.safeParse(json);
|
|
5871
|
+
if (!parsed.success) {
|
|
5872
|
+
sendJSON(res, 400, { error: "Invalid body", issues: parsed.error.issues });
|
|
5873
|
+
return;
|
|
5874
|
+
}
|
|
5875
|
+
} catch {
|
|
5876
|
+
sendJSON(res, 400, { error: "Invalid JSON body" });
|
|
5877
|
+
return;
|
|
5878
|
+
}
|
|
5879
|
+
}
|
|
5880
|
+
try {
|
|
5881
|
+
const existing = (await queue.list()).find((i) => i.id === id);
|
|
5882
|
+
if (!existing) {
|
|
5883
|
+
sendJSON(res, 404, { error: `Interaction ${id} not found` });
|
|
5884
|
+
return;
|
|
5885
|
+
}
|
|
5886
|
+
if (existing.status === "resolved") {
|
|
5887
|
+
sendJSON(res, 409, { error: `Interaction ${id} already resolved` });
|
|
5888
|
+
return;
|
|
5889
|
+
}
|
|
5890
|
+
await queue.updateStatus(id, "resolved");
|
|
5891
|
+
sendJSON(res, 200, { resolved: true });
|
|
5892
|
+
} catch (err) {
|
|
5893
|
+
const msg = err instanceof Error ? err.message : "Failed to resolve";
|
|
5894
|
+
if (msg.includes("not found")) {
|
|
5895
|
+
sendJSON(res, 404, { error: msg });
|
|
5896
|
+
return;
|
|
5897
|
+
}
|
|
5898
|
+
sendJSON(res, 500, { error: "Internal error resolving interaction" });
|
|
5899
|
+
}
|
|
5900
|
+
})();
|
|
5901
|
+
return true;
|
|
5902
|
+
}
|
|
5903
|
+
|
|
5904
|
+
// src/server/routes/plans.ts
|
|
5905
|
+
var import_zod5 = require("zod");
|
|
5804
5906
|
var fs9 = __toESM(require("fs/promises"));
|
|
5805
5907
|
var path9 = __toESM(require("path"));
|
|
5806
|
-
var PlanWriteSchema =
|
|
5807
|
-
filename:
|
|
5808
|
-
content:
|
|
5908
|
+
var PlanWriteSchema = import_zod5.z.object({
|
|
5909
|
+
filename: import_zod5.z.string().min(1),
|
|
5910
|
+
content: import_zod5.z.string().min(1)
|
|
5809
5911
|
});
|
|
5810
5912
|
function handlePlansRoute(req, res, plansDir) {
|
|
5811
5913
|
const { method, url } = req;
|
|
@@ -5849,7 +5951,7 @@ function handlePlansRoute(req, res, plansDir) {
|
|
|
5849
5951
|
var import_node_child_process8 = require("child_process");
|
|
5850
5952
|
var import_node_crypto5 = require("crypto");
|
|
5851
5953
|
var readline2 = __toESM(require("readline"));
|
|
5852
|
-
var
|
|
5954
|
+
var import_zod6 = require("zod");
|
|
5853
5955
|
var UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
5854
5956
|
var SAFE_ENV_PREFIXES = [
|
|
5855
5957
|
"PATH",
|
|
@@ -5884,10 +5986,10 @@ function buildChildEnv() {
|
|
|
5884
5986
|
}
|
|
5885
5987
|
return env;
|
|
5886
5988
|
}
|
|
5887
|
-
var ChatRequestSchema =
|
|
5888
|
-
prompt:
|
|
5889
|
-
system:
|
|
5890
|
-
sessionId:
|
|
5989
|
+
var ChatRequestSchema = import_zod6.z.object({
|
|
5990
|
+
prompt: import_zod6.z.string().min(1),
|
|
5991
|
+
system: import_zod6.z.string().optional(),
|
|
5992
|
+
sessionId: import_zod6.z.string().regex(UUID_RE).optional()
|
|
5891
5993
|
});
|
|
5892
5994
|
function handleChatProxyRoute(req, res, command = "claude") {
|
|
5893
5995
|
const { method, url } = req;
|
|
@@ -6097,11 +6199,11 @@ function extractChunks(event) {
|
|
|
6097
6199
|
|
|
6098
6200
|
// src/server/routes/analyze.ts
|
|
6099
6201
|
var import_intelligence4 = require("@harness-engineering/intelligence");
|
|
6100
|
-
var
|
|
6101
|
-
var AnalyzeRequestSchema =
|
|
6102
|
-
title:
|
|
6103
|
-
description:
|
|
6104
|
-
labels:
|
|
6202
|
+
var import_zod7 = require("zod");
|
|
6203
|
+
var AnalyzeRequestSchema = import_zod7.z.object({
|
|
6204
|
+
title: import_zod7.z.string().min(1),
|
|
6205
|
+
description: import_zod7.z.string().optional(),
|
|
6206
|
+
labels: import_zod7.z.array(import_zod7.z.string()).optional()
|
|
6105
6207
|
});
|
|
6106
6208
|
function emit2(res, event) {
|
|
6107
6209
|
res.write(`data: ${JSON.stringify(event)}
|
|
@@ -6210,21 +6312,21 @@ function handleAnalyzeRoute(req, res, pipeline) {
|
|
|
6210
6312
|
var fs10 = __toESM(require("fs/promises"));
|
|
6211
6313
|
var path10 = __toESM(require("path"));
|
|
6212
6314
|
var import_core7 = require("@harness-engineering/core");
|
|
6213
|
-
var
|
|
6214
|
-
var AppendRoadmapRequestSchema =
|
|
6215
|
-
title:
|
|
6216
|
-
summary:
|
|
6217
|
-
labels:
|
|
6218
|
-
enrichedSpec:
|
|
6219
|
-
intent:
|
|
6220
|
-
unknowns:
|
|
6221
|
-
ambiguities:
|
|
6222
|
-
riskSignals:
|
|
6223
|
-
affectedSystems:
|
|
6315
|
+
var import_zod8 = require("zod");
|
|
6316
|
+
var AppendRoadmapRequestSchema = import_zod8.z.object({
|
|
6317
|
+
title: import_zod8.z.string().min(1),
|
|
6318
|
+
summary: import_zod8.z.string().optional(),
|
|
6319
|
+
labels: import_zod8.z.array(import_zod8.z.string()).optional(),
|
|
6320
|
+
enrichedSpec: import_zod8.z.object({
|
|
6321
|
+
intent: import_zod8.z.string(),
|
|
6322
|
+
unknowns: import_zod8.z.array(import_zod8.z.string()),
|
|
6323
|
+
ambiguities: import_zod8.z.array(import_zod8.z.string()),
|
|
6324
|
+
riskSignals: import_zod8.z.array(import_zod8.z.string()),
|
|
6325
|
+
affectedSystems: import_zod8.z.array(import_zod8.z.object({ name: import_zod8.z.string() }))
|
|
6224
6326
|
}).optional(),
|
|
6225
|
-
cmlRecommendedRoute:
|
|
6327
|
+
cmlRecommendedRoute: import_zod8.z.enum(["local", "human", "simulation-required"]).optional()
|
|
6226
6328
|
});
|
|
6227
|
-
function
|
|
6329
|
+
function sendJSON2(res, status, body) {
|
|
6228
6330
|
res.writeHead(status, { "Content-Type": "application/json" });
|
|
6229
6331
|
res.end(JSON.stringify(body));
|
|
6230
6332
|
}
|
|
@@ -6233,7 +6335,7 @@ function handleRoadmapActionsRoute(req, res, roadmapPath) {
|
|
|
6233
6335
|
void (async () => {
|
|
6234
6336
|
try {
|
|
6235
6337
|
if (!roadmapPath) {
|
|
6236
|
-
|
|
6338
|
+
sendJSON2(res, 503, { error: "Roadmap path not configured" });
|
|
6237
6339
|
return;
|
|
6238
6340
|
}
|
|
6239
6341
|
const projectRoot = path10.dirname(path10.dirname(roadmapPath));
|
|
@@ -6241,18 +6343,18 @@ function handleRoadmapActionsRoute(req, res, roadmapPath) {
|
|
|
6241
6343
|
if (mode === "file-less") {
|
|
6242
6344
|
const trackerCfg = (0, import_core7.loadTrackerClientConfigFromProject)(projectRoot);
|
|
6243
6345
|
if (!trackerCfg.ok) {
|
|
6244
|
-
|
|
6346
|
+
sendJSON2(res, 500, { error: trackerCfg.error.message });
|
|
6245
6347
|
return;
|
|
6246
6348
|
}
|
|
6247
6349
|
const clientR = (0, import_core7.createTrackerClient)(trackerCfg.value);
|
|
6248
6350
|
if (!clientR.ok) {
|
|
6249
|
-
|
|
6351
|
+
sendJSON2(res, 500, { error: clientR.error.message });
|
|
6250
6352
|
return;
|
|
6251
6353
|
}
|
|
6252
6354
|
const body2 = await readBody(req);
|
|
6253
6355
|
const parseResult = AppendRoadmapRequestSchema.safeParse(JSON.parse(body2));
|
|
6254
6356
|
if (!parseResult.success) {
|
|
6255
|
-
|
|
6357
|
+
sendJSON2(res, 400, {
|
|
6256
6358
|
error: parseResult.error.issues[0]?.message ?? "Invalid request body"
|
|
6257
6359
|
});
|
|
6258
6360
|
return;
|
|
@@ -6265,13 +6367,13 @@ function handleRoadmapActionsRoute(req, res, roadmapPath) {
|
|
|
6265
6367
|
const r = await clientR.value.create(newFeature);
|
|
6266
6368
|
if (!r.ok) {
|
|
6267
6369
|
if (r.error instanceof import_core7.ConflictError) {
|
|
6268
|
-
|
|
6370
|
+
sendJSON2(res, 409, (0, import_core7.makeTrackerConflictBody)(r.error));
|
|
6269
6371
|
return;
|
|
6270
6372
|
}
|
|
6271
|
-
|
|
6373
|
+
sendJSON2(res, 502, { error: r.error.message });
|
|
6272
6374
|
return;
|
|
6273
6375
|
}
|
|
6274
|
-
|
|
6376
|
+
sendJSON2(res, 201, {
|
|
6275
6377
|
ok: true,
|
|
6276
6378
|
featureName: r.value.name,
|
|
6277
6379
|
externalId: r.value.externalId
|
|
@@ -6281,18 +6383,18 @@ function handleRoadmapActionsRoute(req, res, roadmapPath) {
|
|
|
6281
6383
|
const body = await readBody(req);
|
|
6282
6384
|
const result = AppendRoadmapRequestSchema.safeParse(JSON.parse(body));
|
|
6283
6385
|
if (!result.success) {
|
|
6284
|
-
|
|
6386
|
+
sendJSON2(res, 400, { error: result.error.issues[0]?.message ?? "Invalid request body" });
|
|
6285
6387
|
return;
|
|
6286
6388
|
}
|
|
6287
6389
|
const parsed = result.data;
|
|
6288
6390
|
if (parsed.title.includes("\n") || parsed.title.includes("###")) {
|
|
6289
|
-
|
|
6391
|
+
sendJSON2(res, 400, { error: "Title must not contain newlines or markdown headings" });
|
|
6290
6392
|
return;
|
|
6291
6393
|
}
|
|
6292
6394
|
const content = await fs10.readFile(roadmapPath, "utf-8");
|
|
6293
6395
|
const roadmapResult = (0, import_core7.parseRoadmap)(content);
|
|
6294
6396
|
if (!roadmapResult.ok) {
|
|
6295
|
-
|
|
6397
|
+
sendJSON2(res, 500, { error: "Failed to parse roadmap file" });
|
|
6296
6398
|
return;
|
|
6297
6399
|
}
|
|
6298
6400
|
const roadmap = roadmapResult.value;
|
|
@@ -6322,11 +6424,11 @@ function handleRoadmapActionsRoute(req, res, roadmapPath) {
|
|
|
6322
6424
|
const serialized = (0, import_core7.serializeRoadmap)(roadmap);
|
|
6323
6425
|
await fs10.writeFile(tmpPath, serialized, "utf-8");
|
|
6324
6426
|
await fs10.rename(tmpPath, roadmapPath);
|
|
6325
|
-
|
|
6427
|
+
sendJSON2(res, 201, { ok: true, featureName: parsed.title });
|
|
6326
6428
|
} catch (err) {
|
|
6327
6429
|
const msg = err instanceof Error ? err.message : "Failed to append to roadmap";
|
|
6328
6430
|
if (!res.headersSent) {
|
|
6329
|
-
|
|
6431
|
+
sendJSON2(res, 500, { error: msg });
|
|
6330
6432
|
}
|
|
6331
6433
|
}
|
|
6332
6434
|
})();
|
|
@@ -6335,13 +6437,13 @@ function handleRoadmapActionsRoute(req, res, roadmapPath) {
|
|
|
6335
6437
|
|
|
6336
6438
|
// src/server/routes/dispatch-actions.ts
|
|
6337
6439
|
var import_node_crypto6 = require("crypto");
|
|
6338
|
-
var
|
|
6339
|
-
var DispatchAdHocRequestSchema =
|
|
6340
|
-
title:
|
|
6341
|
-
description:
|
|
6342
|
-
labels:
|
|
6440
|
+
var import_zod9 = require("zod");
|
|
6441
|
+
var DispatchAdHocRequestSchema = import_zod9.z.object({
|
|
6442
|
+
title: import_zod9.z.string().min(1),
|
|
6443
|
+
description: import_zod9.z.string().optional(),
|
|
6444
|
+
labels: import_zod9.z.array(import_zod9.z.string()).optional()
|
|
6343
6445
|
});
|
|
6344
|
-
function
|
|
6446
|
+
function sendJSON3(res, status, body) {
|
|
6345
6447
|
res.writeHead(status, { "Content-Type": "application/json" });
|
|
6346
6448
|
res.end(JSON.stringify(body));
|
|
6347
6449
|
}
|
|
@@ -6355,13 +6457,13 @@ function handleDispatchActionsRoute(req, res, dispatchFn) {
|
|
|
6355
6457
|
void (async () => {
|
|
6356
6458
|
try {
|
|
6357
6459
|
if (!dispatchFn) {
|
|
6358
|
-
|
|
6460
|
+
sendJSON3(res, 503, { error: "Dispatch not available" });
|
|
6359
6461
|
return;
|
|
6360
6462
|
}
|
|
6361
6463
|
const body = await readBody(req);
|
|
6362
6464
|
const result = DispatchAdHocRequestSchema.safeParse(JSON.parse(body));
|
|
6363
6465
|
if (!result.success) {
|
|
6364
|
-
|
|
6466
|
+
sendJSON3(res, 400, { error: result.error.issues[0]?.message ?? "Invalid request body" });
|
|
6365
6467
|
return;
|
|
6366
6468
|
}
|
|
6367
6469
|
const parsed = result.data;
|
|
@@ -6384,11 +6486,11 @@ function handleDispatchActionsRoute(req, res, dispatchFn) {
|
|
|
6384
6486
|
externalId: null
|
|
6385
6487
|
};
|
|
6386
6488
|
await dispatchFn(issue);
|
|
6387
|
-
|
|
6489
|
+
sendJSON3(res, 200, { ok: true, issueId: id });
|
|
6388
6490
|
} catch (err) {
|
|
6389
6491
|
const msg = err instanceof Error ? err.message : "Dispatch failed";
|
|
6390
6492
|
if (!res.headersSent) {
|
|
6391
|
-
|
|
6493
|
+
sendJSON3(res, 500, { error: msg });
|
|
6392
6494
|
}
|
|
6393
6495
|
}
|
|
6394
6496
|
})();
|
|
@@ -6436,7 +6538,7 @@ function handleAnalysesRoute(req, res, archive) {
|
|
|
6436
6538
|
}
|
|
6437
6539
|
|
|
6438
6540
|
// src/server/routes/maintenance.ts
|
|
6439
|
-
var
|
|
6541
|
+
var import_zod10 = require("zod");
|
|
6440
6542
|
function toMaintenanceHistoryEntry(r) {
|
|
6441
6543
|
const durationMs = r.startedAt && r.completedAt ? Date.parse(r.completedAt) - Date.parse(r.startedAt) : 0;
|
|
6442
6544
|
const status = r.status === "failure" ? "failed" : r.status;
|
|
@@ -6451,27 +6553,27 @@ function toMaintenanceHistoryEntry(r) {
|
|
|
6451
6553
|
if (r.error !== void 0) entry.error = r.error;
|
|
6452
6554
|
return entry;
|
|
6453
6555
|
}
|
|
6454
|
-
var TriggerRequestSchema =
|
|
6455
|
-
taskId:
|
|
6556
|
+
var TriggerRequestSchema = import_zod10.z.object({
|
|
6557
|
+
taskId: import_zod10.z.string().min(1)
|
|
6456
6558
|
});
|
|
6457
|
-
function
|
|
6559
|
+
function sendJSON4(res, status, body) {
|
|
6458
6560
|
res.writeHead(status, { "Content-Type": "application/json" });
|
|
6459
6561
|
res.end(JSON.stringify(body));
|
|
6460
6562
|
}
|
|
6461
6563
|
function handleGetSchedule(res, deps) {
|
|
6462
6564
|
const status = deps.scheduler.getStatus();
|
|
6463
|
-
|
|
6565
|
+
sendJSON4(res, 200, status.schedule);
|
|
6464
6566
|
}
|
|
6465
6567
|
function handleGetStatus(res, deps) {
|
|
6466
6568
|
const status = deps.scheduler.getStatus();
|
|
6467
|
-
|
|
6569
|
+
sendJSON4(res, 200, status);
|
|
6468
6570
|
}
|
|
6469
6571
|
function handleGetHistory(res, deps, queryString) {
|
|
6470
6572
|
const params = new URLSearchParams(queryString);
|
|
6471
6573
|
const limit = Math.min(100, Math.max(1, parseInt(params.get("limit") ?? "20", 10) || 20));
|
|
6472
6574
|
const offset = Math.max(0, parseInt(params.get("offset") ?? "0", 10) || 0);
|
|
6473
6575
|
const history = deps.reporter.getHistory(limit, offset).map(toMaintenanceHistoryEntry);
|
|
6474
|
-
|
|
6576
|
+
sendJSON4(res, 200, history);
|
|
6475
6577
|
}
|
|
6476
6578
|
function handlePostTrigger(req, res, deps) {
|
|
6477
6579
|
void (async () => {
|
|
@@ -6481,20 +6583,20 @@ function handlePostTrigger(req, res, deps) {
|
|
|
6481
6583
|
try {
|
|
6482
6584
|
json = JSON.parse(body);
|
|
6483
6585
|
} catch {
|
|
6484
|
-
|
|
6586
|
+
sendJSON4(res, 400, { error: "Invalid JSON body" });
|
|
6485
6587
|
return;
|
|
6486
6588
|
}
|
|
6487
6589
|
const result = TriggerRequestSchema.safeParse(json);
|
|
6488
6590
|
if (!result.success) {
|
|
6489
|
-
|
|
6591
|
+
sendJSON4(res, 400, { error: "Missing taskId string" });
|
|
6490
6592
|
return;
|
|
6491
6593
|
}
|
|
6492
6594
|
await deps.triggerFn(result.data.taskId);
|
|
6493
|
-
|
|
6595
|
+
sendJSON4(res, 200, { ok: true, taskId: result.data.taskId });
|
|
6494
6596
|
} catch (err) {
|
|
6495
6597
|
const msg = err instanceof Error ? err.message : "Trigger failed";
|
|
6496
6598
|
if (!res.headersSent) {
|
|
6497
|
-
|
|
6599
|
+
sendJSON4(res, 500, { error: msg });
|
|
6498
6600
|
}
|
|
6499
6601
|
}
|
|
6500
6602
|
})();
|
|
@@ -6503,7 +6605,7 @@ function handleMaintenanceRoute(req, res, deps) {
|
|
|
6503
6605
|
const { method, url } = req;
|
|
6504
6606
|
if (!url?.startsWith("/api/maintenance/")) return false;
|
|
6505
6607
|
if (!deps) {
|
|
6506
|
-
|
|
6608
|
+
sendJSON4(res, 503, { error: "Maintenance not available" });
|
|
6507
6609
|
return true;
|
|
6508
6610
|
}
|
|
6509
6611
|
const [pathname, queryString] = url.split("?");
|
|
@@ -6524,16 +6626,296 @@ function handleMaintenanceRoute(req, res, deps) {
|
|
|
6524
6626
|
handlePostTrigger(req, res, deps);
|
|
6525
6627
|
return true;
|
|
6526
6628
|
}
|
|
6527
|
-
|
|
6629
|
+
sendJSON4(res, 404, { error: "Not found" });
|
|
6630
|
+
return true;
|
|
6631
|
+
}
|
|
6632
|
+
|
|
6633
|
+
// src/server/routes/v1/jobs-maintenance.ts
|
|
6634
|
+
var import_node_crypto7 = require("crypto");
|
|
6635
|
+
var import_zod11 = require("zod");
|
|
6636
|
+
var BodySchema2 = import_zod11.z.object({
|
|
6637
|
+
taskId: import_zod11.z.string().min(1).max(200),
|
|
6638
|
+
params: import_zod11.z.record(import_zod11.z.unknown()).optional()
|
|
6639
|
+
});
|
|
6640
|
+
function sendJSON5(res, status, body) {
|
|
6641
|
+
res.writeHead(status, { "Content-Type": "application/json" });
|
|
6642
|
+
res.end(JSON.stringify(body));
|
|
6643
|
+
}
|
|
6644
|
+
function handleV1JobsMaintenanceRoute(req, res, deps) {
|
|
6645
|
+
if (req.url !== "/api/v1/jobs/maintenance" || req.method !== "POST") return false;
|
|
6646
|
+
if (!deps) {
|
|
6647
|
+
sendJSON5(res, 503, { error: "Maintenance not available" });
|
|
6648
|
+
return true;
|
|
6649
|
+
}
|
|
6650
|
+
void (async () => {
|
|
6651
|
+
let raw;
|
|
6652
|
+
try {
|
|
6653
|
+
raw = await readBody(req);
|
|
6654
|
+
} catch (err) {
|
|
6655
|
+
sendJSON5(res, 413, { error: err instanceof Error ? err.message : "Body too large" });
|
|
6656
|
+
return;
|
|
6657
|
+
}
|
|
6658
|
+
let json;
|
|
6659
|
+
try {
|
|
6660
|
+
json = JSON.parse(raw);
|
|
6661
|
+
} catch {
|
|
6662
|
+
sendJSON5(res, 400, { error: "Invalid JSON body" });
|
|
6663
|
+
return;
|
|
6664
|
+
}
|
|
6665
|
+
const parsed = BodySchema2.safeParse(json);
|
|
6666
|
+
if (!parsed.success) {
|
|
6667
|
+
sendJSON5(res, 400, { error: "Invalid body", issues: parsed.error.issues });
|
|
6668
|
+
return;
|
|
6669
|
+
}
|
|
6670
|
+
const runId = `run_${(0, import_node_crypto7.randomBytes)(8).toString("hex")}`;
|
|
6671
|
+
try {
|
|
6672
|
+
await deps.triggerFn(parsed.data.taskId);
|
|
6673
|
+
sendJSON5(res, 200, { ok: true, taskId: parsed.data.taskId, runId });
|
|
6674
|
+
} catch (err) {
|
|
6675
|
+
const msg = err instanceof Error ? err.message : "Trigger failed";
|
|
6676
|
+
const lower = msg.toLowerCase();
|
|
6677
|
+
if (lower.includes("unknown task") || lower.includes("not found")) {
|
|
6678
|
+
sendJSON5(res, 404, { error: msg });
|
|
6679
|
+
return;
|
|
6680
|
+
}
|
|
6681
|
+
if (lower.includes("already running")) {
|
|
6682
|
+
sendJSON5(res, 409, { error: msg });
|
|
6683
|
+
return;
|
|
6684
|
+
}
|
|
6685
|
+
sendJSON5(res, 500, { error: "Internal error triggering maintenance task" });
|
|
6686
|
+
}
|
|
6687
|
+
})();
|
|
6688
|
+
return true;
|
|
6689
|
+
}
|
|
6690
|
+
|
|
6691
|
+
// src/server/routes/v1/events-sse.ts
|
|
6692
|
+
var import_node_crypto8 = require("crypto");
|
|
6693
|
+
var SSE_TOPICS = [
|
|
6694
|
+
"state_change",
|
|
6695
|
+
"agent_event",
|
|
6696
|
+
"interaction.created",
|
|
6697
|
+
"interaction.resolved",
|
|
6698
|
+
"maintenance:started",
|
|
6699
|
+
"maintenance:completed",
|
|
6700
|
+
"maintenance:error",
|
|
6701
|
+
"maintenance:baseref_fallback",
|
|
6702
|
+
"local-model:status",
|
|
6703
|
+
// ── Phase 3 ──
|
|
6704
|
+
"webhook.subscription.created",
|
|
6705
|
+
"webhook.subscription.deleted"
|
|
6706
|
+
];
|
|
6707
|
+
var HEARTBEAT_MS = 15e3;
|
|
6708
|
+
function newEventId() {
|
|
6709
|
+
return `evt_${(0, import_node_crypto8.randomBytes)(8).toString("hex")}`;
|
|
6710
|
+
}
|
|
6711
|
+
function handleV1EventsSseRoute(req, res, bus) {
|
|
6712
|
+
if (req.method !== "GET" || req.url !== "/api/v1/events") return false;
|
|
6713
|
+
res.setHeader("Content-Type", "text/event-stream");
|
|
6714
|
+
res.setHeader("Cache-Control", "no-cache");
|
|
6715
|
+
res.setHeader("Connection", "keep-alive");
|
|
6716
|
+
res.setHeader("X-Accel-Buffering", "no");
|
|
6717
|
+
res.writeHead(200);
|
|
6718
|
+
res.write(`: harness gateway SSE \u2014 connected at ${(/* @__PURE__ */ new Date()).toISOString()}
|
|
6719
|
+
|
|
6720
|
+
`);
|
|
6721
|
+
const listeners = [];
|
|
6722
|
+
for (const topic of SSE_TOPICS) {
|
|
6723
|
+
const fn = (data) => {
|
|
6724
|
+
try {
|
|
6725
|
+
const frame = `event: ${topic}
|
|
6726
|
+
data: ${JSON.stringify(data)}
|
|
6727
|
+
id: ${newEventId()}
|
|
6728
|
+
|
|
6729
|
+
`;
|
|
6730
|
+
res.write(frame);
|
|
6731
|
+
} catch {
|
|
6732
|
+
}
|
|
6733
|
+
};
|
|
6734
|
+
bus.on(topic, fn);
|
|
6735
|
+
listeners.push({ topic, fn });
|
|
6736
|
+
}
|
|
6737
|
+
const heartbeat = setInterval(() => {
|
|
6738
|
+
try {
|
|
6739
|
+
res.write(": heartbeat\n\n");
|
|
6740
|
+
} catch {
|
|
6741
|
+
}
|
|
6742
|
+
}, HEARTBEAT_MS);
|
|
6743
|
+
heartbeat.unref();
|
|
6744
|
+
const cleanup = () => {
|
|
6745
|
+
clearInterval(heartbeat);
|
|
6746
|
+
for (const { topic, fn } of listeners) bus.removeListener(topic, fn);
|
|
6747
|
+
};
|
|
6748
|
+
res.on("close", cleanup);
|
|
6749
|
+
res.on("finish", cleanup);
|
|
6528
6750
|
return true;
|
|
6529
6751
|
}
|
|
6530
6752
|
|
|
6753
|
+
// src/server/routes/v1/webhooks.ts
|
|
6754
|
+
var import_zod12 = require("zod");
|
|
6755
|
+
|
|
6756
|
+
// src/server/utils/url-guard.ts
|
|
6757
|
+
var PRIVATE_HOSTNAME_RE = /^(localhost|.*\.local)$/i;
|
|
6758
|
+
var PRIVATE_IPV4_RE = /^(127\.\d+\.\d+\.\d+|10\.\d+\.\d+\.\d+|172\.(1[6-9]|2\d|3[01])\.\d+\.\d+|192\.168\.\d+\.\d+|169\.254\.\d+\.\d+|0\.0\.0\.0)$/;
|
|
6759
|
+
var LOOPBACK_IPV6_RE = /^(::1|::ffff:127\.\d+\.\d+\.\d+|::ffff:0:127\.\d+\.\d+\.\d+)$/i;
|
|
6760
|
+
function isPrivateHost(hostname) {
|
|
6761
|
+
return PRIVATE_HOSTNAME_RE.test(hostname) || PRIVATE_IPV4_RE.test(hostname) || LOOPBACK_IPV6_RE.test(hostname);
|
|
6762
|
+
}
|
|
6763
|
+
|
|
6764
|
+
// src/server/routes/v1/webhooks.ts
|
|
6765
|
+
var import_types21 = require("@harness-engineering/types");
|
|
6766
|
+
function isAdminAuth(authContext) {
|
|
6767
|
+
if (!authContext) return false;
|
|
6768
|
+
if (authContext.scopes.includes("admin")) return true;
|
|
6769
|
+
if (authContext.id.startsWith("tok_legacy_env")) return true;
|
|
6770
|
+
return false;
|
|
6771
|
+
}
|
|
6772
|
+
function getAuthContext(req) {
|
|
6773
|
+
return req._authToken;
|
|
6774
|
+
}
|
|
6775
|
+
var CreateBody = import_zod12.z.object({
|
|
6776
|
+
url: import_zod12.z.string().url(),
|
|
6777
|
+
events: import_zod12.z.array(import_zod12.z.string().min(1)).min(1)
|
|
6778
|
+
});
|
|
6779
|
+
var QUEUE_STATS_PATH_RE = /^\/api\/v1\/webhooks\/queue\/stats(?:\?.*)?$/;
|
|
6780
|
+
var DELETE_PATH_RE = /^\/api\/v1\/webhooks\/([a-zA-Z0-9_-]+)(?:\?.*)?$/;
|
|
6781
|
+
var LIST_OR_CREATE_PATH_RE = /^\/api\/v1\/webhooks(?:\?.*)?$/;
|
|
6782
|
+
function sendJSON6(res, status, body) {
|
|
6783
|
+
res.writeHead(status, { "Content-Type": "application/json" });
|
|
6784
|
+
res.end(JSON.stringify(body));
|
|
6785
|
+
}
|
|
6786
|
+
var unauthDevWarnedThisProcess = false;
|
|
6787
|
+
function maybeWarnUnauthDev(tokenId, url) {
|
|
6788
|
+
if (unauthDevWarnedThisProcess) return;
|
|
6789
|
+
const isUnauthDev = tokenId === "tok_legacy_env" || process.env["HARNESS_UNAUTH_DEV_ACTIVE"] === "1";
|
|
6790
|
+
if (!isUnauthDev) return;
|
|
6791
|
+
unauthDevWarnedThisProcess = true;
|
|
6792
|
+
console.warn(
|
|
6793
|
+
`[webhook] subscription created under unauth-dev mode (tokenId=${tokenId}). Webhook target URL: ${url}. Set HARNESS_API_TOKEN or configure tokens.json to silence this warning.`
|
|
6794
|
+
);
|
|
6795
|
+
}
|
|
6796
|
+
function handleV1WebhooksRoute(req, res, deps) {
|
|
6797
|
+
const url = req.url ?? "";
|
|
6798
|
+
const method = req.method ?? "GET";
|
|
6799
|
+
if (method === "GET" && QUEUE_STATS_PATH_RE.test(url)) {
|
|
6800
|
+
if (!deps.queue) {
|
|
6801
|
+
sendJSON6(res, 503, { error: "Queue not available" });
|
|
6802
|
+
return true;
|
|
6803
|
+
}
|
|
6804
|
+
sendJSON6(res, 200, deps.queue.stats());
|
|
6805
|
+
return true;
|
|
6806
|
+
}
|
|
6807
|
+
if (method === "GET" && LIST_OR_CREATE_PATH_RE.test(url)) {
|
|
6808
|
+
void (async () => {
|
|
6809
|
+
const subs = await deps.store.list();
|
|
6810
|
+
const authContext = getAuthContext(req);
|
|
6811
|
+
const visible = isAdminAuth(authContext) ? subs : subs.filter((s) => s.tokenId === authContext?.id);
|
|
6812
|
+
const publicView = visible.map((s) => import_types21.WebhookSubscriptionPublicSchema.parse(s));
|
|
6813
|
+
sendJSON6(res, 200, publicView);
|
|
6814
|
+
})();
|
|
6815
|
+
return true;
|
|
6816
|
+
}
|
|
6817
|
+
if (method === "POST" && LIST_OR_CREATE_PATH_RE.test(url)) {
|
|
6818
|
+
void (async () => {
|
|
6819
|
+
let raw;
|
|
6820
|
+
try {
|
|
6821
|
+
raw = await readBody(req);
|
|
6822
|
+
} catch (err) {
|
|
6823
|
+
sendJSON6(res, 413, { error: err instanceof Error ? err.message : "Body too large" });
|
|
6824
|
+
return;
|
|
6825
|
+
}
|
|
6826
|
+
let json;
|
|
6827
|
+
try {
|
|
6828
|
+
json = JSON.parse(raw);
|
|
6829
|
+
} catch {
|
|
6830
|
+
sendJSON6(res, 400, { error: "Invalid JSON body" });
|
|
6831
|
+
return;
|
|
6832
|
+
}
|
|
6833
|
+
const parsed = CreateBody.safeParse(json);
|
|
6834
|
+
if (!parsed.success) {
|
|
6835
|
+
sendJSON6(res, 400, { error: "Invalid body", issues: parsed.error.issues });
|
|
6836
|
+
return;
|
|
6837
|
+
}
|
|
6838
|
+
if (!parsed.data.url.startsWith("https://")) {
|
|
6839
|
+
sendJSON6(res, 422, { error: "URL must use https" });
|
|
6840
|
+
return;
|
|
6841
|
+
}
|
|
6842
|
+
const targetHostname = new URL(parsed.data.url).hostname;
|
|
6843
|
+
if (isPrivateHost(targetHostname)) {
|
|
6844
|
+
sendJSON6(res, 422, { error: "URL must not target private or loopback addresses" });
|
|
6845
|
+
return;
|
|
6846
|
+
}
|
|
6847
|
+
const tokenId = getAuthContext(req)?.id ?? "unknown";
|
|
6848
|
+
const sub = await deps.store.create({
|
|
6849
|
+
tokenId,
|
|
6850
|
+
url: parsed.data.url,
|
|
6851
|
+
events: parsed.data.events
|
|
6852
|
+
});
|
|
6853
|
+
maybeWarnUnauthDev(tokenId, parsed.data.url);
|
|
6854
|
+
deps.bus.emit("webhook.subscription.created", {
|
|
6855
|
+
id: sub.id,
|
|
6856
|
+
tokenId: sub.tokenId,
|
|
6857
|
+
url: sub.url,
|
|
6858
|
+
events: sub.events,
|
|
6859
|
+
createdAt: sub.createdAt
|
|
6860
|
+
});
|
|
6861
|
+
sendJSON6(res, 200, sub);
|
|
6862
|
+
})();
|
|
6863
|
+
return true;
|
|
6864
|
+
}
|
|
6865
|
+
const m = method === "DELETE" ? DELETE_PATH_RE.exec(url) : null;
|
|
6866
|
+
if (m) {
|
|
6867
|
+
const id = m[1] ?? "";
|
|
6868
|
+
void (async () => {
|
|
6869
|
+
const authContext = getAuthContext(req);
|
|
6870
|
+
const subs = await deps.store.list();
|
|
6871
|
+
const sub = subs.find((s) => s.id === id);
|
|
6872
|
+
if (!sub) {
|
|
6873
|
+
sendJSON6(res, 404, { error: "Subscription not found" });
|
|
6874
|
+
return;
|
|
6875
|
+
}
|
|
6876
|
+
if (!isAdminAuth(authContext) && sub.tokenId !== authContext?.id) {
|
|
6877
|
+
sendJSON6(res, 403, { error: "forbidden" });
|
|
6878
|
+
return;
|
|
6879
|
+
}
|
|
6880
|
+
const ok = await deps.store.delete(id);
|
|
6881
|
+
if (!ok) {
|
|
6882
|
+
sendJSON6(res, 404, { error: "Subscription not found" });
|
|
6883
|
+
return;
|
|
6884
|
+
}
|
|
6885
|
+
deps.bus.emit("webhook.subscription.deleted", { id });
|
|
6886
|
+
sendJSON6(res, 200, { deleted: true });
|
|
6887
|
+
})();
|
|
6888
|
+
return true;
|
|
6889
|
+
}
|
|
6890
|
+
return false;
|
|
6891
|
+
}
|
|
6892
|
+
|
|
6893
|
+
// src/server/routes/v1/telemetry.ts
|
|
6894
|
+
var CACHE_STATS_PATH_RE = /^\/api\/v1\/telemetry\/cache\/stats(?:\?.*)?$/;
|
|
6895
|
+
function sendJSON7(res, status, body) {
|
|
6896
|
+
res.writeHead(status, { "Content-Type": "application/json" });
|
|
6897
|
+
res.end(JSON.stringify(body));
|
|
6898
|
+
}
|
|
6899
|
+
function handleV1TelemetryRoute(req, res, deps) {
|
|
6900
|
+
const url = req.url ?? "";
|
|
6901
|
+
const method = req.method ?? "GET";
|
|
6902
|
+
if (method === "GET" && CACHE_STATS_PATH_RE.test(url)) {
|
|
6903
|
+
if (!deps.cacheMetrics) {
|
|
6904
|
+
sendJSON7(res, 503, { error: "Cache metrics recorder not available" });
|
|
6905
|
+
return true;
|
|
6906
|
+
}
|
|
6907
|
+
sendJSON7(res, 200, deps.cacheMetrics.getStats());
|
|
6908
|
+
return true;
|
|
6909
|
+
}
|
|
6910
|
+
return false;
|
|
6911
|
+
}
|
|
6912
|
+
|
|
6531
6913
|
// src/server/routes/sessions.ts
|
|
6532
6914
|
var fs11 = __toESM(require("fs/promises"));
|
|
6533
6915
|
var path11 = __toESM(require("path"));
|
|
6534
|
-
var
|
|
6535
|
-
var SessionCreateSchema =
|
|
6536
|
-
sessionId:
|
|
6916
|
+
var import_zod13 = require("zod");
|
|
6917
|
+
var SessionCreateSchema = import_zod13.z.object({
|
|
6918
|
+
sessionId: import_zod13.z.string().min(1)
|
|
6537
6919
|
}).passthrough();
|
|
6538
6920
|
var UUID_RE2 = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
6539
6921
|
function isSafeId(id) {
|
|
@@ -6620,7 +7002,7 @@ async function handleUpdate(req, res, url, sessionsDir) {
|
|
|
6620
7002
|
return;
|
|
6621
7003
|
}
|
|
6622
7004
|
const body = await readBody(req);
|
|
6623
|
-
const updates =
|
|
7005
|
+
const updates = import_zod13.z.record(import_zod13.z.unknown()).parse(JSON.parse(body));
|
|
6624
7006
|
const sessionFilePath = path11.join(sessionsDir, id, "session.json");
|
|
6625
7007
|
const current = JSON.parse(await fs11.readFile(sessionFilePath, "utf-8"));
|
|
6626
7008
|
await fs11.writeFile(sessionFilePath, JSON.stringify({ ...current, ...updates }, null, 2));
|
|
@@ -6739,8 +7121,112 @@ function handleStreamsRoute(req, res, recorder) {
|
|
|
6739
7121
|
return true;
|
|
6740
7122
|
}
|
|
6741
7123
|
|
|
7124
|
+
// src/server/routes/auth.ts
|
|
7125
|
+
var import_zod14 = require("zod");
|
|
7126
|
+
var import_types22 = require("@harness-engineering/types");
|
|
7127
|
+
var CreateBodySchema = import_zod14.z.object({
|
|
7128
|
+
name: import_zod14.z.string().min(1).max(100),
|
|
7129
|
+
scopes: import_zod14.z.array(import_types22.TokenScopeSchema).min(1),
|
|
7130
|
+
bridgeKind: import_types22.BridgeKindSchema.optional(),
|
|
7131
|
+
tenantId: import_zod14.z.string().optional(),
|
|
7132
|
+
expiresAt: import_zod14.z.string().datetime().optional()
|
|
7133
|
+
});
|
|
7134
|
+
function sendJSON8(res, status, body) {
|
|
7135
|
+
res.writeHead(status, { "Content-Type": "application/json" });
|
|
7136
|
+
res.end(JSON.stringify(body));
|
|
7137
|
+
}
|
|
7138
|
+
async function handlePost(req, res, store) {
|
|
7139
|
+
let raw;
|
|
7140
|
+
try {
|
|
7141
|
+
raw = await readBody(req);
|
|
7142
|
+
} catch (err) {
|
|
7143
|
+
const msg = err instanceof Error ? err.message : "Failed to read body";
|
|
7144
|
+
sendJSON8(res, 413, { error: msg });
|
|
7145
|
+
return;
|
|
7146
|
+
}
|
|
7147
|
+
let json;
|
|
7148
|
+
try {
|
|
7149
|
+
json = JSON.parse(raw);
|
|
7150
|
+
} catch {
|
|
7151
|
+
sendJSON8(res, 400, { error: "Invalid JSON body" });
|
|
7152
|
+
return;
|
|
7153
|
+
}
|
|
7154
|
+
const parsed = CreateBodySchema.safeParse(json);
|
|
7155
|
+
if (!parsed.success) {
|
|
7156
|
+
sendJSON8(res, 422, { error: "Invalid body", issues: parsed.error.issues });
|
|
7157
|
+
return;
|
|
7158
|
+
}
|
|
7159
|
+
try {
|
|
7160
|
+
const input = {
|
|
7161
|
+
name: parsed.data.name,
|
|
7162
|
+
scopes: parsed.data.scopes
|
|
7163
|
+
};
|
|
7164
|
+
if (parsed.data.bridgeKind !== void 0) input.bridgeKind = parsed.data.bridgeKind;
|
|
7165
|
+
if (parsed.data.tenantId !== void 0) input.tenantId = parsed.data.tenantId;
|
|
7166
|
+
if (parsed.data.expiresAt !== void 0) input.expiresAt = parsed.data.expiresAt;
|
|
7167
|
+
const result = await store.create(input);
|
|
7168
|
+
const publicRecord = import_types22.AuthTokenPublicSchema.parse(result.record);
|
|
7169
|
+
sendJSON8(res, 200, {
|
|
7170
|
+
...publicRecord,
|
|
7171
|
+
token: result.token
|
|
7172
|
+
});
|
|
7173
|
+
} catch (err) {
|
|
7174
|
+
const msg = err instanceof Error ? err.message : "Failed to create token";
|
|
7175
|
+
if (msg.includes("already exists")) {
|
|
7176
|
+
sendJSON8(res, 409, { error: msg });
|
|
7177
|
+
return;
|
|
7178
|
+
}
|
|
7179
|
+
sendJSON8(res, 500, { error: "Internal error creating token" });
|
|
7180
|
+
}
|
|
7181
|
+
}
|
|
7182
|
+
async function handleList2(res, store) {
|
|
7183
|
+
try {
|
|
7184
|
+
const list = await store.list();
|
|
7185
|
+
sendJSON8(res, 200, list);
|
|
7186
|
+
} catch {
|
|
7187
|
+
sendJSON8(res, 500, { error: "Internal error listing tokens" });
|
|
7188
|
+
}
|
|
7189
|
+
}
|
|
7190
|
+
async function handleDelete2(res, store, id) {
|
|
7191
|
+
try {
|
|
7192
|
+
const ok = await store.revoke(id);
|
|
7193
|
+
if (!ok) {
|
|
7194
|
+
sendJSON8(res, 404, { error: "Token not found" });
|
|
7195
|
+
return;
|
|
7196
|
+
}
|
|
7197
|
+
sendJSON8(res, 200, { deleted: true });
|
|
7198
|
+
} catch {
|
|
7199
|
+
sendJSON8(res, 500, { error: "Internal error revoking token" });
|
|
7200
|
+
}
|
|
7201
|
+
}
|
|
7202
|
+
var DELETE_PATH_RE2 = /^\/api\/v1\/auth\/tokens\/([^/?]+)(?:\?.*)?$/;
|
|
7203
|
+
function handleAuthRoute(req, res, store) {
|
|
7204
|
+
const { method, url } = req;
|
|
7205
|
+
if (!url) return false;
|
|
7206
|
+
if (!url.startsWith("/api/v1/auth/")) return false;
|
|
7207
|
+
const [pathname] = url.split("?");
|
|
7208
|
+
if (method === "POST" && pathname === "/api/v1/auth/token") {
|
|
7209
|
+
void handlePost(req, res, store);
|
|
7210
|
+
return true;
|
|
7211
|
+
}
|
|
7212
|
+
if (method === "GET" && pathname === "/api/v1/auth/tokens") {
|
|
7213
|
+
void handleList2(res, store);
|
|
7214
|
+
return true;
|
|
7215
|
+
}
|
|
7216
|
+
if (method === "DELETE") {
|
|
7217
|
+
const match = (pathname ?? "").match(DELETE_PATH_RE2);
|
|
7218
|
+
if (match && match[1]) {
|
|
7219
|
+
const id = decodeURIComponent(match[1]);
|
|
7220
|
+
void handleDelete2(res, store, id);
|
|
7221
|
+
return true;
|
|
7222
|
+
}
|
|
7223
|
+
}
|
|
7224
|
+
sendJSON8(res, 405, { error: "Method not allowed" });
|
|
7225
|
+
return true;
|
|
7226
|
+
}
|
|
7227
|
+
|
|
6742
7228
|
// src/server/routes/local-model.ts
|
|
6743
|
-
function
|
|
7229
|
+
function sendJSON9(res, status, body) {
|
|
6744
7230
|
res.writeHead(status, { "Content-Type": "application/json" });
|
|
6745
7231
|
res.end(JSON.stringify(body));
|
|
6746
7232
|
}
|
|
@@ -6748,30 +7234,30 @@ function handleLocalModelRoute(req, res, getStatus) {
|
|
|
6748
7234
|
const { method, url } = req;
|
|
6749
7235
|
if (url !== "/api/v1/local-model/status") return false;
|
|
6750
7236
|
if (method !== "GET") {
|
|
6751
|
-
|
|
7237
|
+
sendJSON9(res, 405, { error: "Method not allowed" });
|
|
6752
7238
|
return true;
|
|
6753
7239
|
}
|
|
6754
7240
|
if (!getStatus) {
|
|
6755
|
-
|
|
7241
|
+
sendJSON9(res, 503, { error: "Local backend not configured" });
|
|
6756
7242
|
return true;
|
|
6757
7243
|
}
|
|
6758
7244
|
const status = getStatus();
|
|
6759
7245
|
if (!status) {
|
|
6760
|
-
|
|
7246
|
+
sendJSON9(res, 503, { error: "Local backend not configured" });
|
|
6761
7247
|
return true;
|
|
6762
7248
|
}
|
|
6763
|
-
|
|
7249
|
+
sendJSON9(res, 200, status);
|
|
6764
7250
|
return true;
|
|
6765
7251
|
}
|
|
6766
7252
|
function handleLocalModelsRoute(req, res, getStatuses) {
|
|
6767
7253
|
const { method, url } = req;
|
|
6768
7254
|
if (url !== "/api/v1/local-models/status") return false;
|
|
6769
7255
|
if (method !== "GET") {
|
|
6770
|
-
|
|
7256
|
+
sendJSON9(res, 405, { error: "Method not allowed" });
|
|
6771
7257
|
return true;
|
|
6772
7258
|
}
|
|
6773
7259
|
const statuses = getStatuses ? getStatuses() : [];
|
|
6774
|
-
|
|
7260
|
+
sendJSON9(res, 200, statuses);
|
|
6775
7261
|
return true;
|
|
6776
7262
|
}
|
|
6777
7263
|
|
|
@@ -6878,10 +7364,268 @@ var PlanWatcher = class {
|
|
|
6878
7364
|
}
|
|
6879
7365
|
};
|
|
6880
7366
|
|
|
7367
|
+
// src/auth/tokens.ts
|
|
7368
|
+
var import_node_crypto9 = require("crypto");
|
|
7369
|
+
var import_promises = require("fs/promises");
|
|
7370
|
+
var import_node_path = require("path");
|
|
7371
|
+
var import_bcryptjs = __toESM(require("bcryptjs"));
|
|
7372
|
+
var import_types23 = require("@harness-engineering/types");
|
|
7373
|
+
var BCRYPT_ROUNDS = 12;
|
|
7374
|
+
var LEGACY_ENV_ID = "tok_legacy_env";
|
|
7375
|
+
function genId() {
|
|
7376
|
+
return `tok_${(0, import_node_crypto9.randomBytes)(8).toString("hex")}`;
|
|
7377
|
+
}
|
|
7378
|
+
function genSecret() {
|
|
7379
|
+
return (0, import_node_crypto9.randomBytes)(24).toString("base64url");
|
|
7380
|
+
}
|
|
7381
|
+
function parseToken(raw) {
|
|
7382
|
+
const dot = raw.indexOf(".");
|
|
7383
|
+
if (dot < 0) return null;
|
|
7384
|
+
return { id: raw.slice(0, dot), secret: raw.slice(dot + 1) };
|
|
7385
|
+
}
|
|
7386
|
+
var TokenStore = class {
|
|
7387
|
+
constructor(path17) {
|
|
7388
|
+
this.path = path17;
|
|
7389
|
+
}
|
|
7390
|
+
path;
|
|
7391
|
+
cache = null;
|
|
7392
|
+
async load() {
|
|
7393
|
+
if (this.cache) return this.cache;
|
|
7394
|
+
try {
|
|
7395
|
+
const raw = await (0, import_promises.readFile)(this.path, "utf8");
|
|
7396
|
+
const parsed = JSON.parse(raw);
|
|
7397
|
+
const list = Array.isArray(parsed) ? parsed : [];
|
|
7398
|
+
this.cache = list.map((entry) => {
|
|
7399
|
+
const r = import_types23.AuthTokenSchema.safeParse(entry);
|
|
7400
|
+
return r.success ? r.data : null;
|
|
7401
|
+
}).filter((x) => x !== null);
|
|
7402
|
+
} catch (err) {
|
|
7403
|
+
if (err.code === "ENOENT") this.cache = [];
|
|
7404
|
+
else throw err;
|
|
7405
|
+
}
|
|
7406
|
+
return this.cache;
|
|
7407
|
+
}
|
|
7408
|
+
async persist(records) {
|
|
7409
|
+
await (0, import_promises.mkdir)((0, import_node_path.dirname)(this.path), { recursive: true });
|
|
7410
|
+
const tmp = `${this.path}.tmp-${process.pid}-${Date.now()}-${(0, import_node_crypto9.randomBytes)(4).toString("hex")}`;
|
|
7411
|
+
await (0, import_promises.writeFile)(tmp, JSON.stringify(records, null, 2), "utf8");
|
|
7412
|
+
await (0, import_promises.rename)(tmp, this.path);
|
|
7413
|
+
this.cache = records;
|
|
7414
|
+
}
|
|
7415
|
+
async create(input) {
|
|
7416
|
+
const id = genId();
|
|
7417
|
+
const secret = genSecret();
|
|
7418
|
+
const hashedSecret = await import_bcryptjs.default.hash(secret, BCRYPT_ROUNDS);
|
|
7419
|
+
const record = {
|
|
7420
|
+
id,
|
|
7421
|
+
name: input.name,
|
|
7422
|
+
scopes: input.scopes,
|
|
7423
|
+
...input.bridgeKind ? { bridgeKind: input.bridgeKind } : {},
|
|
7424
|
+
...input.tenantId ? { tenantId: input.tenantId } : {},
|
|
7425
|
+
hashedSecret,
|
|
7426
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
7427
|
+
...input.expiresAt ? { expiresAt: input.expiresAt } : {}
|
|
7428
|
+
};
|
|
7429
|
+
const records = await this.load();
|
|
7430
|
+
if (records.some((r) => r.name === input.name)) {
|
|
7431
|
+
throw new Error(`Token with name "${input.name}" already exists`);
|
|
7432
|
+
}
|
|
7433
|
+
await this.persist([...records, record]);
|
|
7434
|
+
return { id, token: `${id}.${secret}`, record };
|
|
7435
|
+
}
|
|
7436
|
+
async verify(raw) {
|
|
7437
|
+
const parsed = parseToken(raw);
|
|
7438
|
+
if (!parsed) return null;
|
|
7439
|
+
const records = await this.load();
|
|
7440
|
+
const rec = records.find((r) => r.id === parsed.id);
|
|
7441
|
+
if (!rec) return null;
|
|
7442
|
+
if (rec.expiresAt && Date.parse(rec.expiresAt) <= Date.now()) return null;
|
|
7443
|
+
const ok = await import_bcryptjs.default.compare(parsed.secret, rec.hashedSecret);
|
|
7444
|
+
if (!ok) return null;
|
|
7445
|
+
await this.touchLastUsed(rec.id);
|
|
7446
|
+
return rec;
|
|
7447
|
+
}
|
|
7448
|
+
async touchLastUsed(id) {
|
|
7449
|
+
const records = await this.load();
|
|
7450
|
+
const idx = records.findIndex((r) => r.id === id);
|
|
7451
|
+
if (idx < 0) return;
|
|
7452
|
+
const current = records[idx];
|
|
7453
|
+
if (!current) return;
|
|
7454
|
+
const next = { ...current, lastUsedAt: (/* @__PURE__ */ new Date()).toISOString() };
|
|
7455
|
+
const out = records.slice();
|
|
7456
|
+
out[idx] = next;
|
|
7457
|
+
await this.persist(out);
|
|
7458
|
+
}
|
|
7459
|
+
async list() {
|
|
7460
|
+
const records = await this.load();
|
|
7461
|
+
return records.map((r) => import_types23.AuthTokenPublicSchema.parse(r));
|
|
7462
|
+
}
|
|
7463
|
+
async revoke(id) {
|
|
7464
|
+
const records = await this.load();
|
|
7465
|
+
const next = records.filter((r) => r.id !== id);
|
|
7466
|
+
if (next.length === records.length) return false;
|
|
7467
|
+
await this.persist(next);
|
|
7468
|
+
return true;
|
|
7469
|
+
}
|
|
7470
|
+
/**
|
|
7471
|
+
* Synthetic admin record for the legacy HARNESS_API_TOKEN escape hatch.
|
|
7472
|
+
* Returned only when `presented` matches `envValue` byte-for-byte (constant-time).
|
|
7473
|
+
*/
|
|
7474
|
+
legacyEnvToken(presented, envValue) {
|
|
7475
|
+
if (!envValue) return null;
|
|
7476
|
+
const a = Buffer.from(presented);
|
|
7477
|
+
const b = Buffer.from(envValue);
|
|
7478
|
+
if (a.length !== b.length) return null;
|
|
7479
|
+
if (!(0, import_node_crypto9.timingSafeEqual)(a, b)) return null;
|
|
7480
|
+
return {
|
|
7481
|
+
id: LEGACY_ENV_ID,
|
|
7482
|
+
name: "legacy-env",
|
|
7483
|
+
scopes: ["admin"],
|
|
7484
|
+
hashedSecret: "<env>",
|
|
7485
|
+
createdAt: (/* @__PURE__ */ new Date(0)).toISOString()
|
|
7486
|
+
};
|
|
7487
|
+
}
|
|
7488
|
+
};
|
|
7489
|
+
|
|
7490
|
+
// src/auth/audit.ts
|
|
7491
|
+
var import_promises2 = require("fs/promises");
|
|
7492
|
+
var import_node_path2 = require("path");
|
|
7493
|
+
var import_types24 = require("@harness-engineering/types");
|
|
7494
|
+
var AuditLogger = class {
|
|
7495
|
+
constructor(path17, opts = {}) {
|
|
7496
|
+
this.path = path17;
|
|
7497
|
+
this.opts = opts;
|
|
7498
|
+
}
|
|
7499
|
+
path;
|
|
7500
|
+
opts;
|
|
7501
|
+
queue = Promise.resolve();
|
|
7502
|
+
dirEnsured = false;
|
|
7503
|
+
async append(input) {
|
|
7504
|
+
const entry = import_types24.AuthAuditEntrySchema.parse({
|
|
7505
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
7506
|
+
tokenId: input.tokenId,
|
|
7507
|
+
...input.tenantId ? { tenantId: input.tenantId } : {},
|
|
7508
|
+
route: input.route,
|
|
7509
|
+
method: input.method,
|
|
7510
|
+
status: input.status
|
|
7511
|
+
});
|
|
7512
|
+
const line = `${JSON.stringify(entry)}
|
|
7513
|
+
`;
|
|
7514
|
+
this.queue = this.queue.then(() => this.writeLine(line)).catch(() => void 0);
|
|
7515
|
+
}
|
|
7516
|
+
/** Wait for queued writes to drain. Test-only; not called on the hot path. */
|
|
7517
|
+
async flush() {
|
|
7518
|
+
await this.queue;
|
|
7519
|
+
}
|
|
7520
|
+
async writeLine(line) {
|
|
7521
|
+
try {
|
|
7522
|
+
if (this.opts.createDir !== false && !this.dirEnsured) {
|
|
7523
|
+
await (0, import_promises2.mkdir)((0, import_node_path2.dirname)(this.path), { recursive: true });
|
|
7524
|
+
this.dirEnsured = true;
|
|
7525
|
+
}
|
|
7526
|
+
await (0, import_promises2.appendFile)(this.path, line, "utf8");
|
|
7527
|
+
} catch (err) {
|
|
7528
|
+
console.warn(`[audit] write failed: ${err.message}`);
|
|
7529
|
+
}
|
|
7530
|
+
}
|
|
7531
|
+
};
|
|
7532
|
+
|
|
7533
|
+
// src/server/v1-bridge-routes.ts
|
|
7534
|
+
var V1_BRIDGE_ROUTES = [
|
|
7535
|
+
// ── Phase 2 bridge primitives ──
|
|
7536
|
+
{
|
|
7537
|
+
method: "POST",
|
|
7538
|
+
pattern: /^\/api\/v1\/jobs\/maintenance(?:\?.*)?$/,
|
|
7539
|
+
scope: "trigger-job",
|
|
7540
|
+
description: "Trigger a maintenance task ad-hoc."
|
|
7541
|
+
},
|
|
7542
|
+
{
|
|
7543
|
+
method: "POST",
|
|
7544
|
+
pattern: /^\/api\/v1\/interactions\/[^/]+\/resolve(?:\?.*)?$/,
|
|
7545
|
+
scope: "resolve-interaction",
|
|
7546
|
+
description: "Resolve a pending interaction."
|
|
7547
|
+
},
|
|
7548
|
+
{
|
|
7549
|
+
method: "GET",
|
|
7550
|
+
pattern: /^\/api\/v1\/events(?:\?.*)?$/,
|
|
7551
|
+
scope: "read-telemetry",
|
|
7552
|
+
description: "Server-Sent Events stream."
|
|
7553
|
+
},
|
|
7554
|
+
// ── Phase 3 bridge primitives ──
|
|
7555
|
+
{
|
|
7556
|
+
method: "POST",
|
|
7557
|
+
pattern: /^\/api\/v1\/webhooks(?:\?.*)?$/,
|
|
7558
|
+
scope: "subscribe-webhook",
|
|
7559
|
+
description: "Subscribe to outbound webhook fan-out."
|
|
7560
|
+
},
|
|
7561
|
+
{
|
|
7562
|
+
method: "DELETE",
|
|
7563
|
+
pattern: /^\/api\/v1\/webhooks\/[^/]+(?:\?.*)?$/,
|
|
7564
|
+
scope: "subscribe-webhook",
|
|
7565
|
+
description: "Delete a webhook subscription."
|
|
7566
|
+
},
|
|
7567
|
+
{
|
|
7568
|
+
method: "GET",
|
|
7569
|
+
pattern: /^\/api\/v1\/webhooks(?:\?.*)?$/,
|
|
7570
|
+
scope: "subscribe-webhook",
|
|
7571
|
+
description: "List webhook subscriptions."
|
|
7572
|
+
},
|
|
7573
|
+
// ── Phase 4 bridge primitives ──
|
|
7574
|
+
{
|
|
7575
|
+
method: "GET",
|
|
7576
|
+
pattern: /^\/api\/v1\/webhooks\/queue\/stats(?:\?.*)?$/,
|
|
7577
|
+
scope: "subscribe-webhook",
|
|
7578
|
+
description: "Webhook delivery queue depth + DLQ stats."
|
|
7579
|
+
},
|
|
7580
|
+
// ── Phase 5 bridge primitives ──
|
|
7581
|
+
{
|
|
7582
|
+
method: "GET",
|
|
7583
|
+
pattern: /^\/api\/v1\/telemetry\/cache\/stats(?:\?.*)?$/,
|
|
7584
|
+
scope: "read-telemetry",
|
|
7585
|
+
description: "Prompt-cache hit/miss snapshot (rolling window)."
|
|
7586
|
+
}
|
|
7587
|
+
];
|
|
7588
|
+
function isV1Bridge(method, url) {
|
|
7589
|
+
return V1_BRIDGE_ROUTES.some((r) => r.method === method && r.pattern.test(url));
|
|
7590
|
+
}
|
|
7591
|
+
function requiredBridgeScope(method, path17) {
|
|
7592
|
+
for (const r of V1_BRIDGE_ROUTES) {
|
|
7593
|
+
if (r.method === method && r.pattern.test(path17)) return r.scope;
|
|
7594
|
+
}
|
|
7595
|
+
return null;
|
|
7596
|
+
}
|
|
7597
|
+
|
|
7598
|
+
// src/auth/scopes.ts
|
|
7599
|
+
function hasScope(held, required) {
|
|
7600
|
+
if (held.includes("admin")) return true;
|
|
7601
|
+
return held.includes(required);
|
|
7602
|
+
}
|
|
7603
|
+
function requiredScopeForRoute(method, path17) {
|
|
7604
|
+
const bridgeScope = requiredBridgeScope(method, path17);
|
|
7605
|
+
if (bridgeScope) return bridgeScope;
|
|
7606
|
+
if (path17 === "/api/v1/auth/token" && method === "POST") return "admin";
|
|
7607
|
+
if (path17 === "/api/v1/auth/tokens" && method === "GET") return "admin";
|
|
7608
|
+
if (/^\/api\/v1\/auth\/tokens\/[^/]+$/.test(path17) && method === "DELETE") return "admin";
|
|
7609
|
+
if ((path17 === "/api/state" || path17 === "/api/v1/state") && method === "GET") return "read-status";
|
|
7610
|
+
if (path17.startsWith("/api/interactions")) return "resolve-interaction";
|
|
7611
|
+
if (path17.startsWith("/api/plans")) return "read-status";
|
|
7612
|
+
if (path17.startsWith("/api/analyze") || path17.startsWith("/api/analyses")) return "read-status";
|
|
7613
|
+
if (path17.startsWith("/api/roadmap-actions")) return "modify-roadmap";
|
|
7614
|
+
if (path17.startsWith("/api/dispatch-actions")) return "trigger-job";
|
|
7615
|
+
if (path17.startsWith("/api/local-model") || path17.startsWith("/api/local-models"))
|
|
7616
|
+
return "read-status";
|
|
7617
|
+
if (path17.startsWith("/api/maintenance")) return "trigger-job";
|
|
7618
|
+
if (path17.startsWith("/api/streams")) return "read-status";
|
|
7619
|
+
if (path17.startsWith("/api/sessions")) return "read-status";
|
|
7620
|
+
if (path17.startsWith("/api/chat-proxy")) return "trigger-job";
|
|
7621
|
+
return null;
|
|
7622
|
+
}
|
|
7623
|
+
|
|
6881
7624
|
// src/server/http.ts
|
|
6882
7625
|
var RATE_LIMIT = Number(process.env["HARNESS_RATE_LIMIT"]) || 100;
|
|
6883
7626
|
var WINDOW_MS = 6e4;
|
|
6884
7627
|
var rateBuckets = /* @__PURE__ */ new Map();
|
|
7628
|
+
var DEPRECATION_DATE = process.env["HARNESS_DEPRECATION_DATE"] ?? "2027-05-14";
|
|
6885
7629
|
var ratePruneTimer = setInterval(() => {
|
|
6886
7630
|
const now = Date.now();
|
|
6887
7631
|
for (const [ip, bucket] of rateBuckets) {
|
|
@@ -6930,8 +7674,13 @@ var OrchestratorServer = class {
|
|
|
6930
7674
|
maintenanceDeps = null;
|
|
6931
7675
|
getLocalModelStatus = null;
|
|
6932
7676
|
getLocalModelStatuses = null;
|
|
7677
|
+
webhooks;
|
|
7678
|
+
cacheMetrics;
|
|
6933
7679
|
recorder = null;
|
|
6934
7680
|
planWatcher = null;
|
|
7681
|
+
tokenStore;
|
|
7682
|
+
auditLogger;
|
|
7683
|
+
warnedUnauthDev = false;
|
|
6935
7684
|
stateChangeListener;
|
|
6936
7685
|
agentEventListener;
|
|
6937
7686
|
apiRoutes;
|
|
@@ -6939,6 +7688,10 @@ var OrchestratorServer = class {
|
|
|
6939
7688
|
this.orchestrator = orchestrator;
|
|
6940
7689
|
this.port = port;
|
|
6941
7690
|
this.initDependencies(deps);
|
|
7691
|
+
const tokensPath = process.env["HARNESS_TOKENS_PATH"] ?? path14.resolve(".harness", "tokens.json");
|
|
7692
|
+
const auditPath = process.env["HARNESS_AUDIT_PATH"] ?? path14.resolve(".harness", "audit.log");
|
|
7693
|
+
this.tokenStore = new TokenStore(tokensPath);
|
|
7694
|
+
this.auditLogger = new AuditLogger(auditPath);
|
|
6942
7695
|
this.httpServer = http.createServer(this.handleRequest.bind(this));
|
|
6943
7696
|
this.broadcaster = new WebSocketBroadcaster(
|
|
6944
7697
|
this.httpServer,
|
|
@@ -6960,6 +7713,8 @@ var OrchestratorServer = class {
|
|
|
6960
7713
|
this.maintenanceDeps = deps?.maintenanceDeps ?? null;
|
|
6961
7714
|
this.getLocalModelStatus = deps?.getLocalModelStatus ?? null;
|
|
6962
7715
|
this.getLocalModelStatuses = deps?.getLocalModelStatuses ?? null;
|
|
7716
|
+
this.webhooks = deps?.webhooks;
|
|
7717
|
+
this.cacheMetrics = deps?.cacheMetrics;
|
|
6963
7718
|
}
|
|
6964
7719
|
wireEvents() {
|
|
6965
7720
|
this.stateChangeListener = (snapshot) => {
|
|
@@ -7025,10 +7780,8 @@ var OrchestratorServer = class {
|
|
|
7025
7780
|
this.recorder = recorder;
|
|
7026
7781
|
}
|
|
7027
7782
|
handleRequest(req, res) {
|
|
7028
|
-
|
|
7029
|
-
|
|
7030
|
-
}
|
|
7031
|
-
if (!checkRateLimit(req, res)) {
|
|
7783
|
+
const isState = req.method === "GET" && (req.url === "/api/state" || req.url === "/api/v1/state");
|
|
7784
|
+
if (!isState && !checkRateLimit(req, res)) {
|
|
7032
7785
|
return;
|
|
7033
7786
|
}
|
|
7034
7787
|
if (this.handleApiRoutes(req, res)) {
|
|
@@ -7040,31 +7793,48 @@ var OrchestratorServer = class {
|
|
|
7040
7793
|
res.writeHead(404, { "Content-Type": "application/json" });
|
|
7041
7794
|
res.end(JSON.stringify({ error: "Not Found" }));
|
|
7042
7795
|
}
|
|
7043
|
-
/** Handle GET /api/state and legacy /api/v1/state */
|
|
7044
|
-
handleStateEndpoint(req, res) {
|
|
7045
|
-
const { method, url } = req;
|
|
7046
|
-
if (method === "GET" && (url === "/api/state" || url === "/api/v1/state")) {
|
|
7047
|
-
res.writeHead(200, { "Content-Type": "application/json" });
|
|
7048
|
-
res.end(JSON.stringify(this.orchestrator.getSnapshot()));
|
|
7049
|
-
return true;
|
|
7050
|
-
}
|
|
7051
|
-
return false;
|
|
7052
|
-
}
|
|
7053
7796
|
/**
|
|
7054
|
-
*
|
|
7055
|
-
*
|
|
7056
|
-
*
|
|
7797
|
+
* Phase 1 auth: bearer token lookup against TokenStore + scope check.
|
|
7798
|
+
* Legacy HARNESS_API_TOKEN env var still authenticates as a synthetic
|
|
7799
|
+
* admin record (see TokenStore.legacyEnvToken).
|
|
7800
|
+
*
|
|
7801
|
+
* Returns the resolved AuthToken on success; sends 401/403 + returns null on failure.
|
|
7057
7802
|
*/
|
|
7058
|
-
|
|
7059
|
-
const token = process.env["HARNESS_API_TOKEN"];
|
|
7060
|
-
if (!token) return true;
|
|
7803
|
+
async resolveAuth(req, res) {
|
|
7061
7804
|
const authHeader = req.headers["authorization"];
|
|
7062
|
-
|
|
7063
|
-
|
|
7064
|
-
|
|
7065
|
-
|
|
7066
|
-
|
|
7067
|
-
|
|
7805
|
+
const legacyEnv = process.env["HARNESS_API_TOKEN"];
|
|
7806
|
+
const listed = await this.tokenStore.list().catch(() => []);
|
|
7807
|
+
if (listed.length === 0 && !legacyEnv) {
|
|
7808
|
+
res.setHeader("X-Harness-Auth-Mode", "unauth-dev");
|
|
7809
|
+
if (!this.warnedUnauthDev) {
|
|
7810
|
+
this.warnedUnauthDev = true;
|
|
7811
|
+
console.warn(
|
|
7812
|
+
"harness orchestrator: running in UNAUTHENTICATED dev mode (tokens.json empty and HARNESS_API_TOKEN not set). All requests resolve as admin. Configure tokens before exposing the API beyond localhost."
|
|
7813
|
+
);
|
|
7814
|
+
}
|
|
7815
|
+
return {
|
|
7816
|
+
id: "tok_unauth_dev",
|
|
7817
|
+
name: "unauth-dev",
|
|
7818
|
+
scopes: ["admin"],
|
|
7819
|
+
hashedSecret: "<none>",
|
|
7820
|
+
createdAt: (/* @__PURE__ */ new Date(0)).toISOString()
|
|
7821
|
+
};
|
|
7822
|
+
}
|
|
7823
|
+
if (!authHeader || !authHeader.startsWith("Bearer ")) {
|
|
7824
|
+
res.writeHead(401, { "Content-Type": "application/json" });
|
|
7825
|
+
res.end(JSON.stringify({ error: "Unauthorized \u2014 set Authorization: Bearer <token>" }));
|
|
7826
|
+
return null;
|
|
7827
|
+
}
|
|
7828
|
+
const raw = authHeader.slice("Bearer ".length).trim();
|
|
7829
|
+
const legacyMatch = this.tokenStore.legacyEnvToken(raw, legacyEnv);
|
|
7830
|
+
if (legacyMatch) return legacyMatch;
|
|
7831
|
+
const verified = await this.tokenStore.verify(raw);
|
|
7832
|
+
if (!verified) {
|
|
7833
|
+
res.writeHead(401, { "Content-Type": "application/json" });
|
|
7834
|
+
res.end(JSON.stringify({ error: "Unauthorized \u2014 invalid or expired token" }));
|
|
7835
|
+
return null;
|
|
7836
|
+
}
|
|
7837
|
+
return verified;
|
|
7068
7838
|
}
|
|
7069
7839
|
/**
|
|
7070
7840
|
* Build the ordered API route table. Each entry is invoked in order and
|
|
@@ -7076,7 +7846,11 @@ var OrchestratorServer = class {
|
|
|
7076
7846
|
*/
|
|
7077
7847
|
buildApiRoutes() {
|
|
7078
7848
|
return [
|
|
7849
|
+
// Auth admin routes — scope-gated to `admin` by requiredScopeForRoute.
|
|
7850
|
+
// First in the table so the auth surface is unambiguously owned by the orchestrator.
|
|
7851
|
+
(req, res) => handleAuthRoute(req, res, this.tokenStore),
|
|
7079
7852
|
(req, res) => !!this.interactionQueue && handleInteractionsRoute(req, res, this.interactionQueue),
|
|
7853
|
+
(req, res) => !!this.interactionQueue && handleV1InteractionsResolveRoute(req, res, this.interactionQueue),
|
|
7080
7854
|
(req, res) => handlePlansRoute(req, res, this.plansDir),
|
|
7081
7855
|
(req, res) => handleAnalyzeRoute(req, res, this.pipeline),
|
|
7082
7856
|
(req, res) => handleAnalysesRoute(req, res, this.analysisArchive),
|
|
@@ -7086,19 +7860,103 @@ var OrchestratorServer = class {
|
|
|
7086
7860
|
// Local-models multi-status route (Spec 2 SC38)
|
|
7087
7861
|
(req, res) => handleLocalModelsRoute(req, res, this.getLocalModelStatuses),
|
|
7088
7862
|
(req, res) => handleMaintenanceRoute(req, res, this.maintenanceDeps),
|
|
7863
|
+
(req, res) => handleV1JobsMaintenanceRoute(req, res, this.maintenanceDeps),
|
|
7089
7864
|
(req, res) => !!this.recorder && handleStreamsRoute(req, res, this.recorder),
|
|
7090
7865
|
(req, res) => handleSessionsRoute(req, res, this.sessionsDir),
|
|
7866
|
+
// SSE event stream — long-lived; placed near end so cheaper routes
|
|
7867
|
+
// short-circuit first, but before the chat-proxy fallback.
|
|
7868
|
+
(req, res) => handleV1EventsSseRoute(req, res, this.orchestrator),
|
|
7869
|
+
// Phase 3 webhooks — short-circuits to false when webhooks is undefined
|
|
7870
|
+
// (e.g. FakeOrchestrator-based tests pass no webhooks dep).
|
|
7871
|
+
// Phase 4: forward the optional queue handle so the stats endpoint can
|
|
7872
|
+
// serve depth/DLQ counts.
|
|
7873
|
+
(req, res) => !!this.webhooks && handleV1WebhooksRoute(req, res, {
|
|
7874
|
+
store: this.webhooks.store,
|
|
7875
|
+
bus: this.orchestrator,
|
|
7876
|
+
...this.webhooks.queue ? { queue: this.webhooks.queue } : {}
|
|
7877
|
+
}),
|
|
7878
|
+
// Phase 5 — telemetry/cache/stats. Returns 503 when cacheMetrics is unset
|
|
7879
|
+
// (FakeOrchestrator tests, exporter-disabled configs).
|
|
7880
|
+
(req, res) => handleV1TelemetryRoute(req, res, {
|
|
7881
|
+
...this.cacheMetrics ? { cacheMetrics: this.cacheMetrics } : {}
|
|
7882
|
+
}),
|
|
7091
7883
|
// Chat proxy route (spawns Claude Code CLI — no API key required)
|
|
7092
7884
|
(req, res) => handleChatProxyRoute(req, res, this.claudeCommand)
|
|
7093
7885
|
];
|
|
7094
7886
|
}
|
|
7095
|
-
/**
|
|
7887
|
+
/**
|
|
7888
|
+
* Dispatch to API route handlers. Returns true immediately and resolves the
|
|
7889
|
+
* request asynchronously (auth + scope check + dispatch + audit log).
|
|
7890
|
+
*
|
|
7891
|
+
* Static-file fallback for non-/api/* requests requires returning false so
|
|
7892
|
+
* `handleRequest` can hand the request off to the static handler.
|
|
7893
|
+
*/
|
|
7096
7894
|
handleApiRoutes(req, res) {
|
|
7097
|
-
|
|
7895
|
+
const url = req.url ?? "";
|
|
7896
|
+
if (!url.startsWith("/api/")) return false;
|
|
7897
|
+
void this.dispatchAuthedRequest(req, res);
|
|
7898
|
+
return true;
|
|
7899
|
+
}
|
|
7900
|
+
async dispatchAuthedRequest(req, res) {
|
|
7901
|
+
const token = await this.resolveAuth(req, res);
|
|
7902
|
+
res.on("finish", () => this.audit(req, res, token));
|
|
7903
|
+
if (!token) return;
|
|
7904
|
+
req._authToken = {
|
|
7905
|
+
id: token.id,
|
|
7906
|
+
scopes: token.scopes
|
|
7907
|
+
};
|
|
7908
|
+
const V1_WRAPPABLE = /* @__PURE__ */ new Set([
|
|
7909
|
+
"interactions",
|
|
7910
|
+
"plans",
|
|
7911
|
+
"analyze",
|
|
7912
|
+
"analyses",
|
|
7913
|
+
"roadmap-actions",
|
|
7914
|
+
"dispatch-actions",
|
|
7915
|
+
"local-model",
|
|
7916
|
+
"local-models",
|
|
7917
|
+
"maintenance",
|
|
7918
|
+
"streams",
|
|
7919
|
+
"sessions",
|
|
7920
|
+
"chat-proxy"
|
|
7921
|
+
]);
|
|
7922
|
+
const v1Match = /^\/api\/v1\/([^/?]+)(.*)$/.exec(req.url ?? "");
|
|
7923
|
+
const rewrittenSlug = v1Match?.[1];
|
|
7924
|
+
const v1BridgeMatch = isV1Bridge(req.method ?? "GET", req.url ?? "");
|
|
7925
|
+
if (!v1BridgeMatch && rewrittenSlug && V1_WRAPPABLE.has(rewrittenSlug)) {
|
|
7926
|
+
req.url = `/api/${rewrittenSlug}${v1Match?.[2] ?? ""}`;
|
|
7927
|
+
}
|
|
7928
|
+
const isLegacyPrefix = !!req.url && // eslint-disable-next-line @harness-engineering/no-hardcoded-path-separator -- URL path, not filesystem
|
|
7929
|
+
req.url.startsWith("/api/") && // eslint-disable-next-line @harness-engineering/no-hardcoded-path-separator -- URL path, not filesystem
|
|
7930
|
+
!req.url.startsWith("/api/v1/") && !v1Match;
|
|
7931
|
+
if (isLegacyPrefix) {
|
|
7932
|
+
res.setHeader("Deprecation", DEPRECATION_DATE);
|
|
7933
|
+
}
|
|
7934
|
+
const pathname = (req.url ?? "").split("?")[0] ?? "";
|
|
7935
|
+
const required = requiredScopeForRoute(req.method ?? "GET", pathname);
|
|
7936
|
+
if (!required || !hasScope(token.scopes, required)) {
|
|
7937
|
+
res.writeHead(403, { "Content-Type": "application/json" });
|
|
7938
|
+
res.end(JSON.stringify({ error: "Insufficient scope", required: required ?? "unknown" }));
|
|
7939
|
+
return;
|
|
7940
|
+
}
|
|
7941
|
+
if (req.method === "GET" && (req.url === "/api/state" || req.url === "/api/v1/state")) {
|
|
7942
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
7943
|
+
res.end(JSON.stringify(this.orchestrator.getSnapshot()));
|
|
7944
|
+
return;
|
|
7945
|
+
}
|
|
7098
7946
|
for (const route of this.apiRoutes) {
|
|
7099
|
-
if (route(req, res)) return
|
|
7947
|
+
if (route(req, res)) return;
|
|
7100
7948
|
}
|
|
7101
|
-
|
|
7949
|
+
res.writeHead(404, { "Content-Type": "application/json" });
|
|
7950
|
+
res.end(JSON.stringify({ error: "Not Found" }));
|
|
7951
|
+
}
|
|
7952
|
+
audit(req, res, token) {
|
|
7953
|
+
void this.auditLogger.append({
|
|
7954
|
+
tokenId: token?.id ?? "anonymous",
|
|
7955
|
+
...token?.tenantId ? { tenantId: token.tenantId } : {},
|
|
7956
|
+
route: (req.url ?? "").split("?")[0] ?? "",
|
|
7957
|
+
method: req.method ?? "GET",
|
|
7958
|
+
status: res.statusCode || 0
|
|
7959
|
+
});
|
|
7102
7960
|
}
|
|
7103
7961
|
get wsClientCount() {
|
|
7104
7962
|
return this.broadcaster.clientCount;
|
|
@@ -7129,6 +7987,630 @@ var OrchestratorServer = class {
|
|
|
7129
7987
|
}
|
|
7130
7988
|
};
|
|
7131
7989
|
|
|
7990
|
+
// src/gateway/webhooks/store.ts
|
|
7991
|
+
var import_node_crypto11 = require("crypto");
|
|
7992
|
+
var import_promises3 = require("fs/promises");
|
|
7993
|
+
var import_node_path3 = require("path");
|
|
7994
|
+
var import_types25 = require("@harness-engineering/types");
|
|
7995
|
+
|
|
7996
|
+
// src/gateway/webhooks/signer.ts
|
|
7997
|
+
var import_node_crypto10 = require("crypto");
|
|
7998
|
+
function sign(secret, body) {
|
|
7999
|
+
const hex = (0, import_node_crypto10.createHmac)("sha256", secret).update(body).digest("hex");
|
|
8000
|
+
return `sha256=${hex}`;
|
|
8001
|
+
}
|
|
8002
|
+
function eventMatches(pattern, type) {
|
|
8003
|
+
if (type.startsWith("telemetry.")) {
|
|
8004
|
+
const pSegs2 = pattern.split(".");
|
|
8005
|
+
if (pSegs2[0] !== "telemetry") return false;
|
|
8006
|
+
}
|
|
8007
|
+
const pSegs = pattern.split(".");
|
|
8008
|
+
const tSegs = type.split(".");
|
|
8009
|
+
if (pSegs.length !== tSegs.length) return false;
|
|
8010
|
+
for (let i = 0; i < pSegs.length; i++) {
|
|
8011
|
+
if (pSegs[i] !== "*" && pSegs[i] !== tSegs[i]) return false;
|
|
8012
|
+
}
|
|
8013
|
+
return true;
|
|
8014
|
+
}
|
|
8015
|
+
|
|
8016
|
+
// src/gateway/webhooks/store.ts
|
|
8017
|
+
function genId2() {
|
|
8018
|
+
return `whk_${(0, import_node_crypto11.randomBytes)(8).toString("hex")}`;
|
|
8019
|
+
}
|
|
8020
|
+
function genSecret2() {
|
|
8021
|
+
return (0, import_node_crypto11.randomBytes)(32).toString("base64url");
|
|
8022
|
+
}
|
|
8023
|
+
var WebhookStore = class {
|
|
8024
|
+
constructor(path17) {
|
|
8025
|
+
this.path = path17;
|
|
8026
|
+
}
|
|
8027
|
+
path;
|
|
8028
|
+
cache = null;
|
|
8029
|
+
async load() {
|
|
8030
|
+
if (this.cache) return this.cache;
|
|
8031
|
+
try {
|
|
8032
|
+
const raw = await (0, import_promises3.readFile)(this.path, "utf8");
|
|
8033
|
+
const parsed = JSON.parse(raw);
|
|
8034
|
+
const list = Array.isArray(parsed) ? parsed : [];
|
|
8035
|
+
this.cache = list.map((entry) => {
|
|
8036
|
+
const r = import_types25.WebhookSubscriptionSchema.safeParse(entry);
|
|
8037
|
+
return r.success ? r.data : null;
|
|
8038
|
+
}).filter((x) => x !== null);
|
|
8039
|
+
} catch (err) {
|
|
8040
|
+
if (err.code === "ENOENT") this.cache = [];
|
|
8041
|
+
else throw err;
|
|
8042
|
+
}
|
|
8043
|
+
return this.cache;
|
|
8044
|
+
}
|
|
8045
|
+
async persist(records) {
|
|
8046
|
+
const tmp = `${this.path}.tmp-${process.pid}-${Date.now()}-${(0, import_node_crypto11.randomBytes)(4).toString("hex")}`;
|
|
8047
|
+
try {
|
|
8048
|
+
await (0, import_promises3.mkdir)((0, import_node_path3.dirname)(this.path), { recursive: true });
|
|
8049
|
+
await (0, import_promises3.writeFile)(tmp, JSON.stringify(records, null, 2), { encoding: "utf8", mode: 384 });
|
|
8050
|
+
await (0, import_promises3.rename)(tmp, this.path);
|
|
8051
|
+
await (0, import_promises3.chmod)(this.path, 384);
|
|
8052
|
+
} catch (err) {
|
|
8053
|
+
if (err.code !== "ENOENT") throw err;
|
|
8054
|
+
}
|
|
8055
|
+
this.cache = records;
|
|
8056
|
+
}
|
|
8057
|
+
async create(input) {
|
|
8058
|
+
const id = genId2();
|
|
8059
|
+
const secret = genSecret2();
|
|
8060
|
+
const record = {
|
|
8061
|
+
id,
|
|
8062
|
+
tokenId: input.tokenId,
|
|
8063
|
+
url: input.url,
|
|
8064
|
+
events: input.events,
|
|
8065
|
+
secret,
|
|
8066
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
8067
|
+
};
|
|
8068
|
+
const records = await this.load();
|
|
8069
|
+
await this.persist([...records, record]);
|
|
8070
|
+
return record;
|
|
8071
|
+
}
|
|
8072
|
+
async list() {
|
|
8073
|
+
return [...await this.load()];
|
|
8074
|
+
}
|
|
8075
|
+
async delete(id) {
|
|
8076
|
+
const records = await this.load();
|
|
8077
|
+
const next = records.filter((r) => r.id !== id);
|
|
8078
|
+
if (next.length === records.length) return false;
|
|
8079
|
+
await this.persist(next);
|
|
8080
|
+
return true;
|
|
8081
|
+
}
|
|
8082
|
+
/** Returns subs whose events list contains a pattern matching `eventType`. */
|
|
8083
|
+
async listForEvent(eventType) {
|
|
8084
|
+
const records = await this.load();
|
|
8085
|
+
return records.filter((r) => r.events.some((p) => eventMatches(p, eventType)));
|
|
8086
|
+
}
|
|
8087
|
+
};
|
|
8088
|
+
|
|
8089
|
+
// src/gateway/webhooks/delivery.ts
|
|
8090
|
+
var import_node_crypto12 = require("crypto");
|
|
8091
|
+
|
|
8092
|
+
// src/gateway/webhooks/queue.ts
|
|
8093
|
+
var import_better_sqlite3 = __toESM(require("better-sqlite3"));
|
|
8094
|
+
var RETRY_DELAYS_MS = [1e3, 4e3, 16e3, 64e3, 256e3];
|
|
8095
|
+
var MAX_ATTEMPTS = 5;
|
|
8096
|
+
var SCHEMA_SQL = `
|
|
8097
|
+
CREATE TABLE IF NOT EXISTS webhook_deliveries (
|
|
8098
|
+
id TEXT PRIMARY KEY,
|
|
8099
|
+
subscriptionId TEXT NOT NULL,
|
|
8100
|
+
eventType TEXT NOT NULL,
|
|
8101
|
+
payload TEXT NOT NULL,
|
|
8102
|
+
attempt INTEGER NOT NULL DEFAULT 0,
|
|
8103
|
+
status TEXT NOT NULL DEFAULT 'pending',
|
|
8104
|
+
nextAttemptAt INTEGER,
|
|
8105
|
+
lastError TEXT,
|
|
8106
|
+
deliveredAt INTEGER
|
|
8107
|
+
) STRICT;
|
|
8108
|
+
CREATE INDEX IF NOT EXISTS idx_deliverable
|
|
8109
|
+
ON webhook_deliveries(status, nextAttemptAt)
|
|
8110
|
+
WHERE status IN ('pending', 'failed', 'in_flight');
|
|
8111
|
+
`;
|
|
8112
|
+
var WebhookQueue = class {
|
|
8113
|
+
db;
|
|
8114
|
+
constructor(dbPath) {
|
|
8115
|
+
this.db = new import_better_sqlite3.default(dbPath);
|
|
8116
|
+
this.db.pragma("journal_mode = WAL");
|
|
8117
|
+
this.db.pragma("synchronous = NORMAL");
|
|
8118
|
+
this.db.exec(SCHEMA_SQL);
|
|
8119
|
+
this.recoverInFlight();
|
|
8120
|
+
}
|
|
8121
|
+
insert(row) {
|
|
8122
|
+
this.db.prepare(
|
|
8123
|
+
`INSERT INTO webhook_deliveries
|
|
8124
|
+
(id, subscriptionId, eventType, payload, attempt, status, nextAttemptAt)
|
|
8125
|
+
VALUES (@id, @subscriptionId, @eventType, @payload, 0, 'pending', @nextAttemptAt)`
|
|
8126
|
+
).run({ ...row, nextAttemptAt: Date.now() });
|
|
8127
|
+
}
|
|
8128
|
+
/**
|
|
8129
|
+
* Atomically lease a batch of deliverable rows: select pending|failed rows
|
|
8130
|
+
* whose nextAttemptAt has elapsed, mark them in_flight in the same
|
|
8131
|
+
* transaction, and return the leased rows. Subsequent calls cannot re-claim
|
|
8132
|
+
* the same rows because they are no longer in pending|failed.
|
|
8133
|
+
*
|
|
8134
|
+
* Without this, two overlapping ticks (tick interval 500ms, HTTP timeout
|
|
8135
|
+
* 5s) would both select the same row and double-fire the webhook.
|
|
8136
|
+
*/
|
|
8137
|
+
claim(now, limit = 20) {
|
|
8138
|
+
const selectIds = this.db.prepare(
|
|
8139
|
+
`SELECT id FROM webhook_deliveries
|
|
8140
|
+
WHERE (status = 'pending' OR status = 'failed')
|
|
8141
|
+
AND nextAttemptAt <= ?
|
|
8142
|
+
ORDER BY nextAttemptAt
|
|
8143
|
+
LIMIT ?`
|
|
8144
|
+
);
|
|
8145
|
+
const markInFlight = this.db.prepare(
|
|
8146
|
+
`UPDATE webhook_deliveries SET status = 'in_flight' WHERE id = ?`
|
|
8147
|
+
);
|
|
8148
|
+
const fetchById = this.db.prepare(`SELECT * FROM webhook_deliveries WHERE id = ?`);
|
|
8149
|
+
const txn = this.db.transaction((tNow, tLimit) => {
|
|
8150
|
+
const ids = selectIds.all(tNow, tLimit);
|
|
8151
|
+
const claimed = [];
|
|
8152
|
+
for (const { id } of ids) {
|
|
8153
|
+
markInFlight.run(id);
|
|
8154
|
+
const row = fetchById.get(id);
|
|
8155
|
+
if (row) claimed.push(row);
|
|
8156
|
+
}
|
|
8157
|
+
return claimed;
|
|
8158
|
+
});
|
|
8159
|
+
return txn(now, limit);
|
|
8160
|
+
}
|
|
8161
|
+
/**
|
|
8162
|
+
* Reset any rows stuck in in_flight (e.g. from a crashed or abruptly
|
|
8163
|
+
* stopped worker) back to failed so they can be re-claimed by the next
|
|
8164
|
+
* tick. At-most-once semantics within a single process; at-least-once
|
|
8165
|
+
* across restarts (a row whose HTTP POST completed but whose DB update was
|
|
8166
|
+
* lost will be re-delivered — bridges must be idempotent on delivery-id).
|
|
8167
|
+
*/
|
|
8168
|
+
recoverInFlight() {
|
|
8169
|
+
return this.db.prepare(
|
|
8170
|
+
`UPDATE webhook_deliveries
|
|
8171
|
+
SET status = 'failed', nextAttemptAt = ?
|
|
8172
|
+
WHERE status = 'in_flight'`
|
|
8173
|
+
).run(Date.now()).changes;
|
|
8174
|
+
}
|
|
8175
|
+
markDelivered(id, deliveredAt) {
|
|
8176
|
+
this.db.prepare(
|
|
8177
|
+
`UPDATE webhook_deliveries
|
|
8178
|
+
SET status = 'delivered', deliveredAt = ?, nextAttemptAt = NULL
|
|
8179
|
+
WHERE id = ?`
|
|
8180
|
+
).run(deliveredAt, id);
|
|
8181
|
+
}
|
|
8182
|
+
markFailed(id, attempt, nextAttemptAt, lastError) {
|
|
8183
|
+
if (attempt >= MAX_ATTEMPTS) {
|
|
8184
|
+
this.db.prepare(
|
|
8185
|
+
`UPDATE webhook_deliveries
|
|
8186
|
+
SET status = 'dead', attempt = ?, lastError = ?, nextAttemptAt = NULL
|
|
8187
|
+
WHERE id = ?`
|
|
8188
|
+
).run(attempt, lastError, id);
|
|
8189
|
+
} else {
|
|
8190
|
+
this.db.prepare(
|
|
8191
|
+
`UPDATE webhook_deliveries
|
|
8192
|
+
SET status = 'failed', attempt = ?, nextAttemptAt = ?, lastError = ?
|
|
8193
|
+
WHERE id = ?`
|
|
8194
|
+
).run(attempt, nextAttemptAt, lastError, id);
|
|
8195
|
+
}
|
|
8196
|
+
}
|
|
8197
|
+
retryDead(id) {
|
|
8198
|
+
const result = this.db.prepare(
|
|
8199
|
+
`UPDATE webhook_deliveries
|
|
8200
|
+
SET status = 'pending', attempt = 0, nextAttemptAt = ?, lastError = NULL
|
|
8201
|
+
WHERE id = ? AND status = 'dead'`
|
|
8202
|
+
).run(Date.now(), id);
|
|
8203
|
+
return result.changes > 0;
|
|
8204
|
+
}
|
|
8205
|
+
list(filter = {}) {
|
|
8206
|
+
const conditions = ["1=1"];
|
|
8207
|
+
const params = [];
|
|
8208
|
+
if (filter.status) {
|
|
8209
|
+
conditions.push("status = ?");
|
|
8210
|
+
params.push(filter.status);
|
|
8211
|
+
}
|
|
8212
|
+
if (filter.subscriptionId) {
|
|
8213
|
+
conditions.push("subscriptionId = ?");
|
|
8214
|
+
params.push(filter.subscriptionId);
|
|
8215
|
+
}
|
|
8216
|
+
const sql = `SELECT * FROM webhook_deliveries WHERE ${conditions.join(" AND ")} ORDER BY nextAttemptAt DESC LIMIT 200`;
|
|
8217
|
+
return this.db.prepare(sql).all(...params);
|
|
8218
|
+
}
|
|
8219
|
+
purge(opts = {}) {
|
|
8220
|
+
const conditions = ["1=1"];
|
|
8221
|
+
const params = [];
|
|
8222
|
+
if (opts.deadOnly) {
|
|
8223
|
+
conditions.push("status = 'dead'");
|
|
8224
|
+
}
|
|
8225
|
+
if (opts.olderThanMs !== void 0) {
|
|
8226
|
+
const cutoff = Date.now() - opts.olderThanMs;
|
|
8227
|
+
conditions.push("(deliveredAt IS NOT NULL AND deliveredAt < ?)");
|
|
8228
|
+
params.push(cutoff);
|
|
8229
|
+
}
|
|
8230
|
+
const sql = `DELETE FROM webhook_deliveries WHERE ${conditions.join(" AND ")}`;
|
|
8231
|
+
return this.db.prepare(sql).run(...params).changes;
|
|
8232
|
+
}
|
|
8233
|
+
/**
|
|
8234
|
+
* Count the rows a purge() call with these same options would delete.
|
|
8235
|
+
* Used by the CLI to show a confirmation preview before destructive deletes.
|
|
8236
|
+
*/
|
|
8237
|
+
previewPurge(opts = {}) {
|
|
8238
|
+
const conditions = ["1=1"];
|
|
8239
|
+
const params = [];
|
|
8240
|
+
if (opts.deadOnly) {
|
|
8241
|
+
conditions.push("status = 'dead'");
|
|
8242
|
+
}
|
|
8243
|
+
if (opts.olderThanMs !== void 0) {
|
|
8244
|
+
const cutoff = Date.now() - opts.olderThanMs;
|
|
8245
|
+
conditions.push("(deliveredAt IS NOT NULL AND deliveredAt < ?)");
|
|
8246
|
+
params.push(cutoff);
|
|
8247
|
+
}
|
|
8248
|
+
const sql = `SELECT COUNT(*) as count FROM webhook_deliveries WHERE ${conditions.join(" AND ")}`;
|
|
8249
|
+
const row = this.db.prepare(sql).get(...params);
|
|
8250
|
+
return row.count;
|
|
8251
|
+
}
|
|
8252
|
+
stats() {
|
|
8253
|
+
const rows = this.db.prepare(`SELECT status, COUNT(*) as count FROM webhook_deliveries GROUP BY status`).all();
|
|
8254
|
+
const m = Object.fromEntries(rows.map((r) => [r.status, r.count]));
|
|
8255
|
+
return {
|
|
8256
|
+
pending: m["pending"] ?? 0,
|
|
8257
|
+
inFlight: m["in_flight"] ?? 0,
|
|
8258
|
+
failed: m["failed"] ?? 0,
|
|
8259
|
+
dead: m["dead"] ?? 0,
|
|
8260
|
+
delivered: m["delivered"] ?? 0
|
|
8261
|
+
};
|
|
8262
|
+
}
|
|
8263
|
+
close() {
|
|
8264
|
+
this.db.close();
|
|
8265
|
+
}
|
|
8266
|
+
};
|
|
8267
|
+
|
|
8268
|
+
// src/gateway/webhooks/delivery.ts
|
|
8269
|
+
var WebhookDelivery = class {
|
|
8270
|
+
queue;
|
|
8271
|
+
store;
|
|
8272
|
+
timeoutMs;
|
|
8273
|
+
fetchImpl;
|
|
8274
|
+
tickIntervalMs;
|
|
8275
|
+
maxConcurrentPerSub;
|
|
8276
|
+
drainTimeoutMs;
|
|
8277
|
+
allowPrivateHosts;
|
|
8278
|
+
inFlight = /* @__PURE__ */ new Map();
|
|
8279
|
+
/**
|
|
8280
|
+
* AbortControllers for currently executing HTTP POSTs, keyed by delivery id.
|
|
8281
|
+
* On drain-timeout exhaustion stop() aborts each one so we never write to
|
|
8282
|
+
* the SQLite handle after orchestrator.stop() closes it.
|
|
8283
|
+
*/
|
|
8284
|
+
inFlightAborts = /* @__PURE__ */ new Map();
|
|
8285
|
+
tickTimer = null;
|
|
8286
|
+
draining = false;
|
|
8287
|
+
constructor(opts) {
|
|
8288
|
+
this.queue = opts.queue;
|
|
8289
|
+
this.store = opts.store;
|
|
8290
|
+
this.timeoutMs = opts.timeoutMs ?? 5e3;
|
|
8291
|
+
this.fetchImpl = opts.fetchImpl ?? fetch;
|
|
8292
|
+
this.tickIntervalMs = opts.tickIntervalMs ?? 500;
|
|
8293
|
+
this.maxConcurrentPerSub = opts.maxConcurrentPerSub ?? 4;
|
|
8294
|
+
this.drainTimeoutMs = opts.drainTimeoutMs ?? 3e4;
|
|
8295
|
+
this.allowPrivateHosts = opts.allowPrivateHosts ?? false;
|
|
8296
|
+
}
|
|
8297
|
+
enqueue(sub, event) {
|
|
8298
|
+
const payload = JSON.stringify(event);
|
|
8299
|
+
this.queue.insert({
|
|
8300
|
+
id: `dlv_${(0, import_node_crypto12.randomBytes)(8).toString("hex")}`,
|
|
8301
|
+
subscriptionId: sub.id,
|
|
8302
|
+
eventType: event.type,
|
|
8303
|
+
payload
|
|
8304
|
+
});
|
|
8305
|
+
}
|
|
8306
|
+
start() {
|
|
8307
|
+
if (this.tickTimer !== null) return;
|
|
8308
|
+
this.tickTimer = setInterval(() => void this.tick(), this.tickIntervalMs);
|
|
8309
|
+
}
|
|
8310
|
+
async stop() {
|
|
8311
|
+
this.draining = true;
|
|
8312
|
+
if (this.tickTimer !== null) {
|
|
8313
|
+
clearInterval(this.tickTimer);
|
|
8314
|
+
this.tickTimer = null;
|
|
8315
|
+
}
|
|
8316
|
+
const deadline = Date.now() + this.drainTimeoutMs;
|
|
8317
|
+
while (Date.now() < deadline) {
|
|
8318
|
+
const total = [...this.inFlight.values()].reduce((a, b) => a + b, 0);
|
|
8319
|
+
if (total === 0) break;
|
|
8320
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
8321
|
+
}
|
|
8322
|
+
if (this.inFlightAborts.size > 0) {
|
|
8323
|
+
for (const ctrl of this.inFlightAborts.values()) ctrl.abort();
|
|
8324
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
8325
|
+
}
|
|
8326
|
+
}
|
|
8327
|
+
async tick() {
|
|
8328
|
+
if (this.draining) return;
|
|
8329
|
+
const pending = this.queue.claim(Date.now());
|
|
8330
|
+
for (const row of pending) {
|
|
8331
|
+
const inFlight = this.inFlight.get(row.subscriptionId) ?? 0;
|
|
8332
|
+
if (inFlight >= this.maxConcurrentPerSub) continue;
|
|
8333
|
+
this.inFlight.set(row.subscriptionId, inFlight + 1);
|
|
8334
|
+
void this.executeDelivery(row);
|
|
8335
|
+
}
|
|
8336
|
+
}
|
|
8337
|
+
async executeDelivery(row) {
|
|
8338
|
+
const ctrl = new AbortController();
|
|
8339
|
+
this.inFlightAborts.set(row.id, ctrl);
|
|
8340
|
+
try {
|
|
8341
|
+
const subs = await this.store.list();
|
|
8342
|
+
const sub = subs.find((s) => s.id === row.subscriptionId);
|
|
8343
|
+
if (!sub) {
|
|
8344
|
+
this.queue.markFailed(row.id, MAX_ATTEMPTS, Date.now(), "subscription deleted");
|
|
8345
|
+
return;
|
|
8346
|
+
}
|
|
8347
|
+
let hostname;
|
|
8348
|
+
try {
|
|
8349
|
+
hostname = new URL(sub.url).hostname;
|
|
8350
|
+
} catch {
|
|
8351
|
+
this.queue.markFailed(row.id, MAX_ATTEMPTS, Date.now(), "invalid URL");
|
|
8352
|
+
return;
|
|
8353
|
+
}
|
|
8354
|
+
if (!this.allowPrivateHosts && isPrivateHost(hostname)) {
|
|
8355
|
+
this.queue.markFailed(
|
|
8356
|
+
row.id,
|
|
8357
|
+
MAX_ATTEMPTS,
|
|
8358
|
+
Date.now(),
|
|
8359
|
+
"URL resolves to private/loopback host"
|
|
8360
|
+
);
|
|
8361
|
+
return;
|
|
8362
|
+
}
|
|
8363
|
+
const signature = sign(sub.secret, row.payload);
|
|
8364
|
+
const timer = setTimeout(() => ctrl.abort(), this.timeoutMs);
|
|
8365
|
+
let ok = false;
|
|
8366
|
+
let lastError = "";
|
|
8367
|
+
let aborted = false;
|
|
8368
|
+
try {
|
|
8369
|
+
const res = await this.fetchImpl(sub.url, {
|
|
8370
|
+
method: "POST",
|
|
8371
|
+
headers: {
|
|
8372
|
+
"Content-Type": "application/json",
|
|
8373
|
+
"X-Harness-Delivery-Id": row.id,
|
|
8374
|
+
"X-Harness-Event-Type": row.eventType,
|
|
8375
|
+
"X-Harness-Signature": signature,
|
|
8376
|
+
"X-Harness-Timestamp": String(Date.now())
|
|
8377
|
+
},
|
|
8378
|
+
body: row.payload,
|
|
8379
|
+
signal: ctrl.signal
|
|
8380
|
+
});
|
|
8381
|
+
ok = res.ok;
|
|
8382
|
+
if (!ok) lastError = `HTTP ${res.status}`;
|
|
8383
|
+
} catch (err) {
|
|
8384
|
+
aborted = ctrl.signal.aborted;
|
|
8385
|
+
lastError = err instanceof Error ? err.message : String(err);
|
|
8386
|
+
} finally {
|
|
8387
|
+
clearTimeout(timer);
|
|
8388
|
+
}
|
|
8389
|
+
if (aborted && this.draining) {
|
|
8390
|
+
return;
|
|
8391
|
+
}
|
|
8392
|
+
if (ok) {
|
|
8393
|
+
this.queue.markDelivered(row.id, Date.now());
|
|
8394
|
+
} else {
|
|
8395
|
+
const nextAttempt = row.attempt + 1;
|
|
8396
|
+
const delay = RETRY_DELAYS_MS[row.attempt] ?? 256e3;
|
|
8397
|
+
this.queue.markFailed(row.id, nextAttempt, Date.now() + delay, lastError);
|
|
8398
|
+
}
|
|
8399
|
+
} finally {
|
|
8400
|
+
this.inFlightAborts.delete(row.id);
|
|
8401
|
+
const cur = this.inFlight.get(row.subscriptionId) ?? 1;
|
|
8402
|
+
this.inFlight.set(row.subscriptionId, Math.max(0, cur - 1));
|
|
8403
|
+
}
|
|
8404
|
+
}
|
|
8405
|
+
};
|
|
8406
|
+
|
|
8407
|
+
// src/gateway/webhooks/events.ts
|
|
8408
|
+
var import_node_crypto13 = require("crypto");
|
|
8409
|
+
var WEBHOOK_TOPICS = [
|
|
8410
|
+
"interaction.created",
|
|
8411
|
+
"interaction.resolved",
|
|
8412
|
+
"maintenance:started",
|
|
8413
|
+
"maintenance:completed",
|
|
8414
|
+
"maintenance:error",
|
|
8415
|
+
"webhook.subscription.created",
|
|
8416
|
+
"webhook.subscription.deleted"
|
|
8417
|
+
];
|
|
8418
|
+
function newEventId2() {
|
|
8419
|
+
return `evt_${(0, import_node_crypto13.randomBytes)(8).toString("hex")}`;
|
|
8420
|
+
}
|
|
8421
|
+
function wireWebhookFanout({ bus, store, delivery }) {
|
|
8422
|
+
const handlers = [];
|
|
8423
|
+
for (const topic of WEBHOOK_TOPICS) {
|
|
8424
|
+
const eventType = topic.replace(":", ".");
|
|
8425
|
+
const fn = (data) => {
|
|
8426
|
+
void (async () => {
|
|
8427
|
+
const subs = await store.listForEvent(eventType);
|
|
8428
|
+
if (subs.length === 0) return;
|
|
8429
|
+
const event = {
|
|
8430
|
+
id: newEventId2(),
|
|
8431
|
+
type: eventType,
|
|
8432
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
8433
|
+
data
|
|
8434
|
+
};
|
|
8435
|
+
for (const sub of subs) {
|
|
8436
|
+
delivery.enqueue(sub, event);
|
|
8437
|
+
}
|
|
8438
|
+
})();
|
|
8439
|
+
};
|
|
8440
|
+
bus.on(topic, fn);
|
|
8441
|
+
handlers.push({ topic, fn });
|
|
8442
|
+
}
|
|
8443
|
+
return () => {
|
|
8444
|
+
for (const { topic, fn } of handlers) bus.removeListener(topic, fn);
|
|
8445
|
+
};
|
|
8446
|
+
}
|
|
8447
|
+
|
|
8448
|
+
// src/gateway/telemetry/fanout.ts
|
|
8449
|
+
var import_node_crypto14 = require("crypto");
|
|
8450
|
+
var import_core9 = require("@harness-engineering/core");
|
|
8451
|
+
var TOPICS = {
|
|
8452
|
+
MAINTENANCE_STARTED: "maintenance:started",
|
|
8453
|
+
MAINTENANCE_COMPLETED: "maintenance:completed",
|
|
8454
|
+
MAINTENANCE_ERROR: "maintenance:error",
|
|
8455
|
+
DISPATCH_DECISION: "dispatch:decision",
|
|
8456
|
+
SKILL_INVOCATION: "skill_invocation"
|
|
8457
|
+
};
|
|
8458
|
+
var TELEMETRY_TYPE = {
|
|
8459
|
+
[TOPICS.MAINTENANCE_STARTED]: "telemetry.maintenance_run",
|
|
8460
|
+
[TOPICS.MAINTENANCE_COMPLETED]: "telemetry.maintenance_run",
|
|
8461
|
+
[TOPICS.MAINTENANCE_ERROR]: "telemetry.maintenance_run",
|
|
8462
|
+
[TOPICS.DISPATCH_DECISION]: "telemetry.dispatch_decision",
|
|
8463
|
+
[TOPICS.SKILL_INVOCATION]: "telemetry.skill_invocation"
|
|
8464
|
+
};
|
|
8465
|
+
var SPAN_NAME = {
|
|
8466
|
+
[TOPICS.MAINTENANCE_STARTED]: "maintenance_run",
|
|
8467
|
+
[TOPICS.MAINTENANCE_COMPLETED]: "maintenance_run",
|
|
8468
|
+
[TOPICS.MAINTENANCE_ERROR]: "maintenance_run",
|
|
8469
|
+
[TOPICS.DISPATCH_DECISION]: "dispatch_decision",
|
|
8470
|
+
[TOPICS.SKILL_INVOCATION]: "skill_invocation"
|
|
8471
|
+
};
|
|
8472
|
+
function newEventId3() {
|
|
8473
|
+
return `evt_${(0, import_node_crypto14.randomBytes)(8).toString("hex")}`;
|
|
8474
|
+
}
|
|
8475
|
+
function newTraceId() {
|
|
8476
|
+
return (0, import_node_crypto14.randomBytes)(16).toString("hex");
|
|
8477
|
+
}
|
|
8478
|
+
function newSpanId() {
|
|
8479
|
+
return (0, import_node_crypto14.randomBytes)(8).toString("hex");
|
|
8480
|
+
}
|
|
8481
|
+
function nowNs() {
|
|
8482
|
+
return BigInt(Date.now()) * 1000000n;
|
|
8483
|
+
}
|
|
8484
|
+
var MAX_ACTIVE_RUNS = 256;
|
|
8485
|
+
var ActiveRunRegistry = class {
|
|
8486
|
+
byKey = /* @__PURE__ */ new Map();
|
|
8487
|
+
open(key, ids) {
|
|
8488
|
+
if (this.byKey.has(key)) {
|
|
8489
|
+
this.byKey.set(key, ids);
|
|
8490
|
+
return;
|
|
8491
|
+
}
|
|
8492
|
+
if (this.byKey.size >= MAX_ACTIVE_RUNS) {
|
|
8493
|
+
const oldest = this.byKey.keys().next().value;
|
|
8494
|
+
if (oldest !== void 0) this.byKey.delete(oldest);
|
|
8495
|
+
}
|
|
8496
|
+
this.byKey.set(key, ids);
|
|
8497
|
+
}
|
|
8498
|
+
/**
|
|
8499
|
+
* Look up an active run; tries `correlationId`, then `taskId`. Returns
|
|
8500
|
+
* `undefined` when neither matches — the caller should treat the event as
|
|
8501
|
+
* a root span rather than guessing a parent.
|
|
8502
|
+
*/
|
|
8503
|
+
resolve(args) {
|
|
8504
|
+
if (args.correlationId && this.byKey.has(args.correlationId)) {
|
|
8505
|
+
return this.byKey.get(args.correlationId);
|
|
8506
|
+
}
|
|
8507
|
+
if (args.taskId && this.byKey.has(args.taskId)) {
|
|
8508
|
+
return this.byKey.get(args.taskId);
|
|
8509
|
+
}
|
|
8510
|
+
return void 0;
|
|
8511
|
+
}
|
|
8512
|
+
close(key) {
|
|
8513
|
+
this.byKey.delete(key);
|
|
8514
|
+
}
|
|
8515
|
+
/** Number of currently tracked runs. Exposed for tests + diagnostics. */
|
|
8516
|
+
get size() {
|
|
8517
|
+
return this.byKey.size;
|
|
8518
|
+
}
|
|
8519
|
+
};
|
|
8520
|
+
function buildAttributes(payload, extras = {}) {
|
|
8521
|
+
const attrs = { ...extras };
|
|
8522
|
+
for (const [k, v] of Object.entries(payload)) {
|
|
8523
|
+
if (v === null || v === void 0) continue;
|
|
8524
|
+
if (typeof v === "string" || typeof v === "number" || typeof v === "boolean") {
|
|
8525
|
+
attrs[k] = v;
|
|
8526
|
+
}
|
|
8527
|
+
}
|
|
8528
|
+
return attrs;
|
|
8529
|
+
}
|
|
8530
|
+
function wireTelemetryFanout(params) {
|
|
8531
|
+
const { bus, exporter, webhookDelivery, store } = params;
|
|
8532
|
+
const registry = new ActiveRunRegistry();
|
|
8533
|
+
const handlers = [];
|
|
8534
|
+
const enqueueToMatchingSubs = async (event) => {
|
|
8535
|
+
const subs = await store.listForEvent(event.type);
|
|
8536
|
+
if (subs.length === 0) return;
|
|
8537
|
+
const filtered = subs.filter((sub) => sub.events.some((p) => eventMatches(p, event.type)));
|
|
8538
|
+
for (const sub of filtered) {
|
|
8539
|
+
webhookDelivery.enqueue(sub, event);
|
|
8540
|
+
}
|
|
8541
|
+
};
|
|
8542
|
+
const makeHandler = (topic) => (data) => {
|
|
8543
|
+
const payload = data ?? {};
|
|
8544
|
+
const correlationId = typeof payload["correlationId"] === "string" ? payload["correlationId"] : void 0;
|
|
8545
|
+
const taskId = typeof payload["taskId"] === "string" ? payload["taskId"] : void 0;
|
|
8546
|
+
let traceId;
|
|
8547
|
+
let spanId;
|
|
8548
|
+
let parentSpanId;
|
|
8549
|
+
let statusCode;
|
|
8550
|
+
if (topic === TOPICS.MAINTENANCE_STARTED) {
|
|
8551
|
+
traceId = newTraceId();
|
|
8552
|
+
spanId = newSpanId();
|
|
8553
|
+
const key = correlationId ?? taskId ?? `run_${spanId}`;
|
|
8554
|
+
registry.open(key, { traceId, spanId });
|
|
8555
|
+
} else if (topic === TOPICS.MAINTENANCE_COMPLETED || topic === TOPICS.MAINTENANCE_ERROR) {
|
|
8556
|
+
const existing = registry.resolve({
|
|
8557
|
+
...correlationId !== void 0 ? { correlationId } : {},
|
|
8558
|
+
...taskId !== void 0 ? { taskId } : {}
|
|
8559
|
+
});
|
|
8560
|
+
if (existing !== void 0) {
|
|
8561
|
+
traceId = existing.traceId;
|
|
8562
|
+
spanId = existing.spanId;
|
|
8563
|
+
} else {
|
|
8564
|
+
traceId = newTraceId();
|
|
8565
|
+
spanId = newSpanId();
|
|
8566
|
+
}
|
|
8567
|
+
statusCode = topic === TOPICS.MAINTENANCE_ERROR ? 2 : 1;
|
|
8568
|
+
const key = correlationId ?? taskId ?? "";
|
|
8569
|
+
if (key) registry.close(key);
|
|
8570
|
+
} else {
|
|
8571
|
+
const parent = registry.resolve({
|
|
8572
|
+
...correlationId !== void 0 ? { correlationId } : {},
|
|
8573
|
+
...taskId !== void 0 ? { taskId } : {}
|
|
8574
|
+
});
|
|
8575
|
+
traceId = parent?.traceId ?? newTraceId();
|
|
8576
|
+
spanId = newSpanId();
|
|
8577
|
+
parentSpanId = parent?.spanId;
|
|
8578
|
+
}
|
|
8579
|
+
const startNs = nowNs();
|
|
8580
|
+
const span = {
|
|
8581
|
+
traceId,
|
|
8582
|
+
spanId,
|
|
8583
|
+
...parentSpanId !== void 0 ? { parentSpanId } : {},
|
|
8584
|
+
name: SPAN_NAME[topic],
|
|
8585
|
+
kind: import_core9.SpanKind.INTERNAL,
|
|
8586
|
+
startTimeNs: startNs,
|
|
8587
|
+
endTimeNs: startNs,
|
|
8588
|
+
attributes: buildAttributes(payload, { "harness.topic": topic }),
|
|
8589
|
+
...statusCode !== void 0 ? { statusCode } : {}
|
|
8590
|
+
};
|
|
8591
|
+
exporter.push(span);
|
|
8592
|
+
const gatewayEvent = {
|
|
8593
|
+
id: newEventId3(),
|
|
8594
|
+
type: TELEMETRY_TYPE[topic],
|
|
8595
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
8596
|
+
data: payload,
|
|
8597
|
+
...correlationId !== void 0 ? { correlationId } : {}
|
|
8598
|
+
};
|
|
8599
|
+
void enqueueToMatchingSubs(gatewayEvent);
|
|
8600
|
+
};
|
|
8601
|
+
for (const topic of Object.values(TOPICS)) {
|
|
8602
|
+
const fn = makeHandler(topic);
|
|
8603
|
+
bus.on(topic, fn);
|
|
8604
|
+
handlers.push({ topic, fn });
|
|
8605
|
+
}
|
|
8606
|
+
return () => {
|
|
8607
|
+
for (const { topic, fn } of handlers) bus.removeListener(topic, fn);
|
|
8608
|
+
};
|
|
8609
|
+
}
|
|
8610
|
+
|
|
8611
|
+
// src/orchestrator.ts
|
|
8612
|
+
var import_core13 = require("@harness-engineering/core");
|
|
8613
|
+
|
|
7132
8614
|
// src/logging/logger.ts
|
|
7133
8615
|
var StructuredLogger = class {
|
|
7134
8616
|
debug(message, context) {
|
|
@@ -7168,8 +8650,8 @@ var StructuredLogger = class {
|
|
|
7168
8650
|
|
|
7169
8651
|
// src/workspace/config-scanner.ts
|
|
7170
8652
|
var import_node_fs = require("fs");
|
|
7171
|
-
var
|
|
7172
|
-
var
|
|
8653
|
+
var import_node_path4 = require("path");
|
|
8654
|
+
var import_core10 = require("@harness-engineering/core");
|
|
7173
8655
|
var CONFIG_FILES = ["CLAUDE.md", "AGENTS.md", ".gemini/settings.json", "skill.yaml"];
|
|
7174
8656
|
var BLOCKING_INJECTION_PREFIXES = ["INJ-UNI-", "INJ-REROL-"];
|
|
7175
8657
|
var DOWNGRADED_SECURITY_RULES = /* @__PURE__ */ new Set(["SEC-AGT-006"]);
|
|
@@ -7191,25 +8673,25 @@ async function scanSingleFile(filePath, targetDir, scanner) {
|
|
|
7191
8673
|
} catch {
|
|
7192
8674
|
return null;
|
|
7193
8675
|
}
|
|
7194
|
-
const injectionFindings = (0,
|
|
7195
|
-
const findings = (0,
|
|
8676
|
+
const injectionFindings = (0, import_core10.scanForInjection)(content);
|
|
8677
|
+
const findings = (0, import_core10.mapInjectionFindings)(injectionFindings);
|
|
7196
8678
|
const secFindings = await scanner.scanFile(filePath);
|
|
7197
|
-
findings.push(...(0,
|
|
8679
|
+
findings.push(...(0, import_core10.mapSecurityFindings)(secFindings, findings));
|
|
7198
8680
|
const adjusted = adjustFindingSeverity(findings);
|
|
7199
8681
|
return {
|
|
7200
|
-
file: (0,
|
|
8682
|
+
file: (0, import_node_path4.relative)(targetDir, filePath).replaceAll("\\", "/"),
|
|
7201
8683
|
findings: adjusted,
|
|
7202
|
-
overallSeverity: (0,
|
|
8684
|
+
overallSeverity: (0, import_core10.computeOverallSeverity)(adjusted)
|
|
7203
8685
|
};
|
|
7204
8686
|
}
|
|
7205
8687
|
async function scanWorkspaceConfig(workspacePath) {
|
|
7206
|
-
const scanner = new
|
|
8688
|
+
const scanner = new import_core10.SecurityScanner((0, import_core10.parseSecurityConfig)({}));
|
|
7207
8689
|
const results = [];
|
|
7208
8690
|
for (const configFile of CONFIG_FILES) {
|
|
7209
|
-
const result = await scanSingleFile((0,
|
|
8691
|
+
const result = await scanSingleFile((0, import_node_path4.join)(workspacePath, configFile), workspacePath, scanner);
|
|
7210
8692
|
if (result) results.push(result);
|
|
7211
8693
|
}
|
|
7212
|
-
return { exitCode: (0,
|
|
8694
|
+
return { exitCode: (0, import_core10.computeScanExitCode)(results), results };
|
|
7213
8695
|
}
|
|
7214
8696
|
|
|
7215
8697
|
// src/maintenance/task-registry.ts
|
|
@@ -7674,27 +9156,27 @@ var MaintenanceScheduler = class {
|
|
|
7674
9156
|
};
|
|
7675
9157
|
|
|
7676
9158
|
// src/maintenance/leader-elector.ts
|
|
7677
|
-
var
|
|
9159
|
+
var import_types26 = require("@harness-engineering/types");
|
|
7678
9160
|
var SingleProcessLeaderElector = class {
|
|
7679
9161
|
async electLeader() {
|
|
7680
|
-
return (0,
|
|
9162
|
+
return (0, import_types26.Ok)("claimed");
|
|
7681
9163
|
}
|
|
7682
9164
|
};
|
|
7683
9165
|
|
|
7684
9166
|
// src/maintenance/reporter.ts
|
|
7685
9167
|
var fs14 = __toESM(require("fs"));
|
|
7686
9168
|
var path15 = __toESM(require("path"));
|
|
7687
|
-
var
|
|
7688
|
-
var RunResultSchema =
|
|
7689
|
-
taskId:
|
|
7690
|
-
startedAt:
|
|
7691
|
-
completedAt:
|
|
7692
|
-
status:
|
|
7693
|
-
findings:
|
|
7694
|
-
fixed:
|
|
7695
|
-
prUrl:
|
|
7696
|
-
prUpdated:
|
|
7697
|
-
error:
|
|
9169
|
+
var import_zod15 = require("zod");
|
|
9170
|
+
var RunResultSchema = import_zod15.z.object({
|
|
9171
|
+
taskId: import_zod15.z.string(),
|
|
9172
|
+
startedAt: import_zod15.z.string(),
|
|
9173
|
+
completedAt: import_zod15.z.string(),
|
|
9174
|
+
status: import_zod15.z.enum(["success", "failure", "skipped", "no-issues"]),
|
|
9175
|
+
findings: import_zod15.z.number(),
|
|
9176
|
+
fixed: import_zod15.z.number(),
|
|
9177
|
+
prUrl: import_zod15.z.string().nullable(),
|
|
9178
|
+
prUpdated: import_zod15.z.boolean(),
|
|
9179
|
+
error: import_zod15.z.string().optional()
|
|
7698
9180
|
});
|
|
7699
9181
|
var MAX_HISTORY = 500;
|
|
7700
9182
|
var fallbackLogger = {
|
|
@@ -7721,7 +9203,7 @@ var MaintenanceReporter = class {
|
|
|
7721
9203
|
await fs14.promises.mkdir(this.persistDir, { recursive: true });
|
|
7722
9204
|
const filePath = path15.join(this.persistDir, "history.json");
|
|
7723
9205
|
const data = await fs14.promises.readFile(filePath, "utf-8");
|
|
7724
|
-
const parsed =
|
|
9206
|
+
const parsed = import_zod15.z.array(RunResultSchema).safeParse(JSON.parse(data));
|
|
7725
9207
|
if (parsed.success) {
|
|
7726
9208
|
this.history = parsed.data.slice(0, MAX_HISTORY);
|
|
7727
9209
|
}
|
|
@@ -8138,6 +9620,25 @@ var Orchestrator = class extends import_node_events.EventEmitter {
|
|
|
8138
9620
|
prDetector;
|
|
8139
9621
|
maintenanceScheduler = null;
|
|
8140
9622
|
maintenanceReporter = null;
|
|
9623
|
+
// Phase 3 webhooks. `webhookStore` is constructed at server-start and held
|
|
9624
|
+
// only as a local; it's passed into `ServerDependencies` and
|
|
9625
|
+
// `wireWebhookFanout` once and never re-read on `this`. The fan-out
|
|
9626
|
+
// teardown handle is kept on the instance so `stop()` can detach listeners.
|
|
9627
|
+
//
|
|
9628
|
+
// Phase 4 delivery durability: the WebhookQueue (SQLite at
|
|
9629
|
+
// `.harness/webhook-queue.sqlite`) and the WebhookDelivery worker are
|
|
9630
|
+
// retained as instance fields so `stop()` can drain in-flight deliveries
|
|
9631
|
+
// (await worker.stop()) and close the SQLite handle (queue.close()).
|
|
9632
|
+
webhookFanoutOff;
|
|
9633
|
+
webhookQueue;
|
|
9634
|
+
webhookDeliveryWorker;
|
|
9635
|
+
// Phase 5: prompt-cache metrics + OTLP trace export. Both are constructed
|
|
9636
|
+
// unconditionally so non-telemetry call sites can reference them safely; the
|
|
9637
|
+
// OTLPExporter is only handed a fanout subscription when config supplies an
|
|
9638
|
+
// endpoint, and `enabled: false` keeps push() a constant-time no-op.
|
|
9639
|
+
cacheMetrics;
|
|
9640
|
+
otlpExporter;
|
|
9641
|
+
telemetryFanoutOff;
|
|
8141
9642
|
orchestratorIdPromise;
|
|
8142
9643
|
recorder;
|
|
8143
9644
|
intelligenceRunner;
|
|
@@ -8172,6 +9673,7 @@ var Orchestrator = class extends import_node_events.EventEmitter {
|
|
|
8172
9673
|
*/
|
|
8173
9674
|
constructor(config, promptTemplate, overrides) {
|
|
8174
9675
|
super();
|
|
9676
|
+
this.setMaxListeners(50);
|
|
8175
9677
|
this.config = config;
|
|
8176
9678
|
this.promptTemplate = promptTemplate;
|
|
8177
9679
|
this.state = createEmptyState(config);
|
|
@@ -8198,7 +9700,8 @@ var Orchestrator = class extends import_node_events.EventEmitter {
|
|
|
8198
9700
|
this.renderer = new PromptRenderer();
|
|
8199
9701
|
this.overrideBackend = overrides?.backend ?? null;
|
|
8200
9702
|
this.interactionQueue = new InteractionQueue(
|
|
8201
|
-
path16.join(config.workspace.root, "..", "interactions")
|
|
9703
|
+
path16.join(config.workspace.root, "..", "interactions"),
|
|
9704
|
+
this
|
|
8202
9705
|
);
|
|
8203
9706
|
this.analysisArchive = new AnalysisArchive(path16.join(config.workspace.root, "..", "analyses"));
|
|
8204
9707
|
const backendsMap = this.config.agent.backends ?? {};
|
|
@@ -8214,6 +9717,7 @@ var Orchestrator = class extends import_node_events.EventEmitter {
|
|
|
8214
9717
|
this.localResolvers.set(name, new LocalModelResolver(resolverOpts));
|
|
8215
9718
|
}
|
|
8216
9719
|
}
|
|
9720
|
+
this.cacheMetrics = new import_core13.CacheMetricsRecorder();
|
|
8217
9721
|
if (this.config.agent.backends !== void 0 && Object.keys(this.config.agent.backends).length > 0) {
|
|
8218
9722
|
const sandboxPolicy = this.config.agent.sandboxPolicy === "docker" ? "docker" : "none";
|
|
8219
9723
|
const firstBackendName = Object.keys(this.config.agent.backends)[0];
|
|
@@ -8226,6 +9730,7 @@ var Orchestrator = class extends import_node_events.EventEmitter {
|
|
|
8226
9730
|
sandboxPolicy,
|
|
8227
9731
|
...this.config.agent.container !== void 0 ? { container: this.config.agent.container } : {},
|
|
8228
9732
|
...this.config.agent.secrets !== void 0 ? { secrets: this.config.agent.secrets } : {},
|
|
9733
|
+
cacheMetrics: this.cacheMetrics,
|
|
8229
9734
|
getResolverModelFor: (name) => {
|
|
8230
9735
|
const resolver = this.localResolvers.get(name);
|
|
8231
9736
|
return resolver ? () => resolver.resolveModel() : void 0;
|
|
@@ -8272,8 +9777,47 @@ var Orchestrator = class extends import_node_events.EventEmitter {
|
|
|
8272
9777
|
this.intelligenceRunner = new IntelligencePipelineRunner(ctx);
|
|
8273
9778
|
this.completionHandler = new CompletionHandler(ctx, this.postLifecycleComment.bind(this));
|
|
8274
9779
|
if (config.server?.port) {
|
|
9780
|
+
const webhookStore = new WebhookStore(
|
|
9781
|
+
path16.join(this.projectRoot, ".harness", "webhooks.json")
|
|
9782
|
+
);
|
|
9783
|
+
this.webhookQueue = new WebhookQueue(
|
|
9784
|
+
path16.join(this.projectRoot, ".harness", "webhook-queue.sqlite")
|
|
9785
|
+
);
|
|
9786
|
+
const webhookDelivery = new WebhookDelivery({
|
|
9787
|
+
queue: this.webhookQueue,
|
|
9788
|
+
store: webhookStore
|
|
9789
|
+
});
|
|
9790
|
+
this.webhookDeliveryWorker = webhookDelivery;
|
|
9791
|
+
this.webhookFanoutOff = wireWebhookFanout({
|
|
9792
|
+
bus: this,
|
|
9793
|
+
store: webhookStore,
|
|
9794
|
+
delivery: webhookDelivery
|
|
9795
|
+
});
|
|
9796
|
+
webhookDelivery.start();
|
|
9797
|
+
const otlpCfg = config.telemetry?.export?.otlp;
|
|
9798
|
+
if (otlpCfg) {
|
|
9799
|
+
this.otlpExporter = new import_core13.OTLPExporter({
|
|
9800
|
+
endpoint: otlpCfg.endpoint,
|
|
9801
|
+
...otlpCfg.enabled !== void 0 ? { enabled: otlpCfg.enabled } : {},
|
|
9802
|
+
...otlpCfg.headers !== void 0 ? { headers: otlpCfg.headers } : {},
|
|
9803
|
+
...otlpCfg.flushIntervalMs !== void 0 ? { flushIntervalMs: otlpCfg.flushIntervalMs } : {},
|
|
9804
|
+
...otlpCfg.batchSize !== void 0 ? { batchSize: otlpCfg.batchSize } : {}
|
|
9805
|
+
});
|
|
9806
|
+
this.telemetryFanoutOff = wireTelemetryFanout({
|
|
9807
|
+
bus: this,
|
|
9808
|
+
exporter: this.otlpExporter,
|
|
9809
|
+
webhookDelivery,
|
|
9810
|
+
store: webhookStore
|
|
9811
|
+
});
|
|
9812
|
+
}
|
|
8275
9813
|
this.server = new OrchestratorServer(this, config.server.port, {
|
|
8276
9814
|
interactionQueue: this.interactionQueue,
|
|
9815
|
+
webhooks: {
|
|
9816
|
+
store: webhookStore,
|
|
9817
|
+
delivery: webhookDelivery,
|
|
9818
|
+
queue: this.webhookQueue
|
|
9819
|
+
},
|
|
9820
|
+
cacheMetrics: this.cacheMetrics,
|
|
8277
9821
|
plansDir: path16.resolve(config.workspace.root, "..", "docs", "plans"),
|
|
8278
9822
|
pipeline: this.pipeline,
|
|
8279
9823
|
analysisArchive: this.analysisArchive,
|
|
@@ -8312,7 +9856,7 @@ var Orchestrator = class extends import_node_events.EventEmitter {
|
|
|
8312
9856
|
...this.config.tracker.apiKey ? { token: this.config.tracker.apiKey } : {},
|
|
8313
9857
|
...this.config.tracker.endpoint ? { apiBase: this.config.tracker.endpoint } : {}
|
|
8314
9858
|
};
|
|
8315
|
-
const clientResult = (0,
|
|
9859
|
+
const clientResult = (0, import_core12.createTrackerClient)(trackerCfg);
|
|
8316
9860
|
if (!clientResult.ok) throw clientResult.error;
|
|
8317
9861
|
return new GitHubIssuesIssueTrackerAdapter(clientResult.value, this.config.tracker);
|
|
8318
9862
|
}
|
|
@@ -8664,7 +10208,7 @@ var Orchestrator = class extends import_node_events.EventEmitter {
|
|
|
8664
10208
|
{ issueId }
|
|
8665
10209
|
);
|
|
8666
10210
|
await this.interactionQueue.push({
|
|
8667
|
-
id: `interaction-${(0,
|
|
10211
|
+
id: `interaction-${(0, import_node_crypto15.randomUUID)()}`,
|
|
8668
10212
|
issueId,
|
|
8669
10213
|
type: "needs-human",
|
|
8670
10214
|
reasons: [`Agent pushed branch "${branch}" but did not create a PR. Worktree preserved.`],
|
|
@@ -8740,7 +10284,7 @@ var Orchestrator = class extends import_node_events.EventEmitter {
|
|
|
8740
10284
|
{ issueId: effect.issueId }
|
|
8741
10285
|
);
|
|
8742
10286
|
await this.interactionQueue.push({
|
|
8743
|
-
id: `interaction-${(0,
|
|
10287
|
+
id: `interaction-${(0, import_node_crypto15.randomUUID)()}`,
|
|
8744
10288
|
issueId: effect.issueId,
|
|
8745
10289
|
type: "needs-human",
|
|
8746
10290
|
reasons: effect.reasons,
|
|
@@ -8836,12 +10380,12 @@ var Orchestrator = class extends import_node_events.EventEmitter {
|
|
|
8836
10380
|
async postLifecycleComment(identifier, externalId, event) {
|
|
8837
10381
|
try {
|
|
8838
10382
|
if (!externalId) return;
|
|
8839
|
-
const trackerConfig = (0,
|
|
10383
|
+
const trackerConfig = (0, import_core12.loadTrackerSyncConfig)(this.projectRoot);
|
|
8840
10384
|
if (!trackerConfig) return;
|
|
8841
10385
|
const token = process.env.GITHUB_TOKEN;
|
|
8842
10386
|
if (!token) return;
|
|
8843
10387
|
const orchestratorId = await this.orchestratorIdPromise;
|
|
8844
|
-
const adapter = new
|
|
10388
|
+
const adapter = new import_core12.GitHubIssuesSyncAdapter({ token, config: trackerConfig });
|
|
8845
10389
|
const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace("T", " ").slice(0, 19);
|
|
8846
10390
|
const actionMap = {
|
|
8847
10391
|
claimed: "Dispatching agent for autonomous execution",
|
|
@@ -8914,7 +10458,7 @@ var Orchestrator = class extends import_node_events.EventEmitter {
|
|
|
8914
10458
|
...f.line !== void 0 ? { line: f.line } : {}
|
|
8915
10459
|
}))
|
|
8916
10460
|
);
|
|
8917
|
-
(0,
|
|
10461
|
+
(0, import_core11.writeTaint)(
|
|
8918
10462
|
workspacePath,
|
|
8919
10463
|
issue.id,
|
|
8920
10464
|
"Medium-severity injection patterns found in workspace config files",
|
|
@@ -9182,6 +10726,9 @@ var Orchestrator = class extends import_node_events.EventEmitter {
|
|
|
9182
10726
|
if (this.server) {
|
|
9183
10727
|
void this.server.start();
|
|
9184
10728
|
}
|
|
10729
|
+
if (this.otlpExporter) {
|
|
10730
|
+
this.otlpExporter.start();
|
|
10731
|
+
}
|
|
9185
10732
|
await this.initLocalModelAndPipeline();
|
|
9186
10733
|
await this.ensureClaimManager();
|
|
9187
10734
|
const runningIssueIds = new Set(this.state.running.keys());
|
|
@@ -9245,6 +10792,26 @@ var Orchestrator = class extends import_node_events.EventEmitter {
|
|
|
9245
10792
|
this.maintenanceScheduler.stop();
|
|
9246
10793
|
this.maintenanceScheduler = null;
|
|
9247
10794
|
}
|
|
10795
|
+
if (this.webhookFanoutOff) {
|
|
10796
|
+
this.webhookFanoutOff();
|
|
10797
|
+
delete this.webhookFanoutOff;
|
|
10798
|
+
}
|
|
10799
|
+
if (this.telemetryFanoutOff) {
|
|
10800
|
+
this.telemetryFanoutOff();
|
|
10801
|
+
delete this.telemetryFanoutOff;
|
|
10802
|
+
}
|
|
10803
|
+
if (this.otlpExporter) {
|
|
10804
|
+
await this.otlpExporter.stop();
|
|
10805
|
+
delete this.otlpExporter;
|
|
10806
|
+
}
|
|
10807
|
+
if (this.webhookDeliveryWorker) {
|
|
10808
|
+
await this.webhookDeliveryWorker.stop();
|
|
10809
|
+
delete this.webhookDeliveryWorker;
|
|
10810
|
+
}
|
|
10811
|
+
if (this.webhookQueue) {
|
|
10812
|
+
this.webhookQueue.close();
|
|
10813
|
+
delete this.webhookQueue;
|
|
10814
|
+
}
|
|
9248
10815
|
if (this.server) {
|
|
9249
10816
|
this.server.stop();
|
|
9250
10817
|
}
|
|
@@ -9666,14 +11233,18 @@ async function syncMain(repoRoot, opts = {}) {
|
|
|
9666
11233
|
ClaimManager,
|
|
9667
11234
|
InteractionQueue,
|
|
9668
11235
|
LinearGraphQLStub,
|
|
11236
|
+
MAX_ATTEMPTS,
|
|
9669
11237
|
MockBackend,
|
|
9670
11238
|
ORCHESTRATOR_IDENTITY_FILE,
|
|
9671
11239
|
Orchestrator,
|
|
9672
11240
|
OrchestratorBackendFactory,
|
|
9673
11241
|
PRDetector,
|
|
9674
11242
|
PromptRenderer,
|
|
11243
|
+
RETRY_DELAYS_MS,
|
|
9675
11244
|
RoadmapTrackerAdapter,
|
|
9676
11245
|
StreamRecorder,
|
|
11246
|
+
TokenStore,
|
|
11247
|
+
WebhookQueue,
|
|
9677
11248
|
WorkflowLoader,
|
|
9678
11249
|
WorkspaceHooks,
|
|
9679
11250
|
WorkspaceManager,
|