@harness-engineering/orchestrator 0.10.0 → 0.11.2
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 +9 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +300 -65
- package/dist/index.mjs +270 -33
- package/package.json +8 -8
package/dist/index.d.mts
CHANGED
|
@@ -1993,6 +1993,15 @@ declare class Orchestrator extends EventEmitter {
|
|
|
1993
1993
|
* `store.load()`.
|
|
1994
1994
|
*/
|
|
1995
1995
|
private initModelPool;
|
|
1996
|
+
/**
|
|
1997
|
+
* LMLM Phase 7 wiring: apply the operator's configured pool bounds (disk
|
|
1998
|
+
* budget + org/family allowlist) from `localModels.pool` to the live pool.
|
|
1999
|
+
* Called after `PoolStateStore.load()` so config wins over persisted bounds
|
|
2000
|
+
* (D2, declarative precedence). No-op when the pool is null (LMLM disabled)
|
|
2001
|
+
* or no `pool` block is configured, so it is safe to call unconditionally
|
|
2002
|
+
* on the startup path.
|
|
2003
|
+
*/
|
|
2004
|
+
private applyConfiguredPoolBounds;
|
|
1996
2005
|
/**
|
|
1997
2006
|
* LMLM Phase 6: arm the single background refresh scheduler over the live
|
|
1998
2007
|
* pool. No-op when LMLM is disabled (`modelPool` null). Each tick runs
|
package/dist/index.d.ts
CHANGED
|
@@ -1993,6 +1993,15 @@ declare class Orchestrator extends EventEmitter {
|
|
|
1993
1993
|
* `store.load()`.
|
|
1994
1994
|
*/
|
|
1995
1995
|
private initModelPool;
|
|
1996
|
+
/**
|
|
1997
|
+
* LMLM Phase 7 wiring: apply the operator's configured pool bounds (disk
|
|
1998
|
+
* budget + org/family allowlist) from `localModels.pool` to the live pool.
|
|
1999
|
+
* Called after `PoolStateStore.load()` so config wins over persisted bounds
|
|
2000
|
+
* (D2, declarative precedence). No-op when the pool is null (LMLM disabled)
|
|
2001
|
+
* or no `pool` block is configured, so it is safe to call unconditionally
|
|
2002
|
+
* on the startup path.
|
|
2003
|
+
*/
|
|
2004
|
+
private applyConfiguredPoolBounds;
|
|
1996
2005
|
/**
|
|
1997
2006
|
* LMLM Phase 6: arm the single background refresh scheduler over the live
|
|
1998
2007
|
* pool. No-op when LMLM is disabled (`modelPool` null). Each tick runs
|
package/dist/index.js
CHANGED
|
@@ -3109,7 +3109,7 @@ var PromptRenderer = class {
|
|
|
3109
3109
|
var import_node_events = require("events");
|
|
3110
3110
|
var path21 = __toESM(require("path"));
|
|
3111
3111
|
var import_node_crypto15 = require("crypto");
|
|
3112
|
-
var
|
|
3112
|
+
var import_core16 = require("@harness-engineering/core");
|
|
3113
3113
|
|
|
3114
3114
|
// src/core/stall-detector.ts
|
|
3115
3115
|
function detectStalledIssues(running, nowMs, stallTimeoutMs) {
|
|
@@ -3685,7 +3685,7 @@ var CompletionHandler = class {
|
|
|
3685
3685
|
};
|
|
3686
3686
|
|
|
3687
3687
|
// src/orchestrator.ts
|
|
3688
|
-
var
|
|
3688
|
+
var import_core17 = require("@harness-engineering/core");
|
|
3689
3689
|
|
|
3690
3690
|
// src/tracker/adapters/github-issues-issue-tracker.ts
|
|
3691
3691
|
var import_types9 = require("@harness-engineering/types");
|
|
@@ -4084,8 +4084,8 @@ var LocalModelResolver = class {
|
|
|
4084
4084
|
};
|
|
4085
4085
|
|
|
4086
4086
|
// src/orchestrator.ts
|
|
4087
|
-
var
|
|
4088
|
-
var
|
|
4087
|
+
var import_local_models5 = require("@harness-engineering/local-models");
|
|
4088
|
+
var import_core18 = require("@harness-engineering/core");
|
|
4089
4089
|
|
|
4090
4090
|
// src/agent/config-migration.ts
|
|
4091
4091
|
var MIGRATION_GUIDE = "docs/guides/multi-backend-routing.md";
|
|
@@ -7049,7 +7049,7 @@ function buildRoutingUseCase(issue, backendParam, catalog) {
|
|
|
7049
7049
|
// src/server/http.ts
|
|
7050
7050
|
var http = __toESM(require("http"));
|
|
7051
7051
|
var path17 = __toESM(require("path"));
|
|
7052
|
-
var
|
|
7052
|
+
var import_core12 = require("@harness-engineering/core");
|
|
7053
7053
|
|
|
7054
7054
|
// src/server/websocket.ts
|
|
7055
7055
|
var import_ws = require("ws");
|
|
@@ -9186,17 +9186,196 @@ async function runForceRefresh(res, scheduler, deps) {
|
|
|
9186
9186
|
});
|
|
9187
9187
|
}
|
|
9188
9188
|
|
|
9189
|
+
// src/server/routes/v1/local-models-pool-mutation.ts
|
|
9190
|
+
var import_core11 = require("@harness-engineering/core");
|
|
9191
|
+
var import_local_models3 = require("@harness-engineering/local-models");
|
|
9192
|
+
var INSTALL_RE = /^\/api\/v1\/local-models\/pool\/install(?:\?.*)?$/;
|
|
9193
|
+
var REMOVE_RE = /^\/api\/v1\/local-models\/pool\/remove(?:\?.*)?$/;
|
|
9194
|
+
var RESOLVE_TOP = 50;
|
|
9195
|
+
function sendJSON10(res, status, body) {
|
|
9196
|
+
res.writeHead(status, { "Content-Type": "application/json" });
|
|
9197
|
+
res.end(JSON.stringify(body));
|
|
9198
|
+
}
|
|
9199
|
+
function decidedByOf(deps, req) {
|
|
9200
|
+
if (deps.getDecidedBy) return deps.getDecidedBy(req);
|
|
9201
|
+
const token = req._authToken;
|
|
9202
|
+
return token?.id ?? "operator";
|
|
9203
|
+
}
|
|
9204
|
+
function handlerDeps(deps, req, pool) {
|
|
9205
|
+
return {
|
|
9206
|
+
pool,
|
|
9207
|
+
bus: deps.bus,
|
|
9208
|
+
updateProposal: (id, patch) => (0, import_core11.updateProposal)(deps.projectPath, id, patch),
|
|
9209
|
+
decidedBy: decidedByOf(deps, req),
|
|
9210
|
+
...deps.isModelInUse ? { isModelInUse: deps.isModelInUse } : {}
|
|
9211
|
+
};
|
|
9212
|
+
}
|
|
9213
|
+
async function readJsonBody(req) {
|
|
9214
|
+
try {
|
|
9215
|
+
const raw = await readBody(req);
|
|
9216
|
+
if (raw.length === 0) return {};
|
|
9217
|
+
const parsed = JSON.parse(raw);
|
|
9218
|
+
return typeof parsed === "object" && parsed !== null ? parsed : void 0;
|
|
9219
|
+
} catch {
|
|
9220
|
+
return void 0;
|
|
9221
|
+
}
|
|
9222
|
+
}
|
|
9223
|
+
async function handleInstall(req, res, deps) {
|
|
9224
|
+
const pool = deps.getModelPool();
|
|
9225
|
+
if (pool === null) return sendJSON10(res, 503, { error: "LMLM disabled" });
|
|
9226
|
+
const body = await readJsonBody(req);
|
|
9227
|
+
if (body === void 0) return sendJSON10(res, 400, { error: "invalid JSON body" });
|
|
9228
|
+
const hfRepoId = typeof body.hfRepoId === "string" ? body.hfRepoId : "";
|
|
9229
|
+
if (hfRepoId.length === 0) return sendJSON10(res, 400, { error: "hfRepoId is required" });
|
|
9230
|
+
const quant = typeof body.quant === "string" ? body.quant : void 0;
|
|
9231
|
+
if (!deps.getRecommendations) {
|
|
9232
|
+
return sendJSON10(res, 503, { error: "recommendations unavailable" });
|
|
9233
|
+
}
|
|
9234
|
+
const ranked = await deps.getRecommendations({ top: RESOLVE_TOP, profile: "general" });
|
|
9235
|
+
const match = ranked.find(
|
|
9236
|
+
(r) => r.hfRepoId === hfRepoId && (quant === void 0 || r.quant === quant)
|
|
9237
|
+
);
|
|
9238
|
+
if (!match) {
|
|
9239
|
+
return sendJSON10(res, 404, {
|
|
9240
|
+
error: `no recommendation for ${hfRepoId}${quant ? ` @ ${quant}` : ""}`
|
|
9241
|
+
});
|
|
9242
|
+
}
|
|
9243
|
+
if (!match.ollamaName) {
|
|
9244
|
+
return sendJSON10(res, 422, { error: `${hfRepoId} has no known Ollama mirror to install` });
|
|
9245
|
+
}
|
|
9246
|
+
const content = {
|
|
9247
|
+
action: "add",
|
|
9248
|
+
target: { hfRepoId: match.hfRepoId, ollamaName: match.ollamaName },
|
|
9249
|
+
scoreDelta: match.score,
|
|
9250
|
+
justification: {
|
|
9251
|
+
summary: `Operator-initiated install of ${match.ollamaName} (${match.quant}).`,
|
|
9252
|
+
benchmarkBasis: [],
|
|
9253
|
+
hardwareFit: match.fitsHardware ? "fits detected hardware" : "may not fit detected hardware",
|
|
9254
|
+
evidence: match.evidence,
|
|
9255
|
+
freshness: `snapshot ${match.benchmarkSnapshot}`
|
|
9256
|
+
},
|
|
9257
|
+
// Estimate the on-disk size from the candidate's params + quant (the same
|
|
9258
|
+
// `estimateDiskGb` the automated proposal engine uses). The pool needs a
|
|
9259
|
+
// size for its pre-commit budget check *before* the pull; it cannot inspect
|
|
9260
|
+
// the target first, because ollama `/api/show` 404s for a model that is not
|
|
9261
|
+
// yet pulled locally — which would surface as a spurious "no longer
|
|
9262
|
+
// available on HuggingFace" 404 on every operator install.
|
|
9263
|
+
diskImpactGb: (0, import_local_models3.estimateDiskGb)({
|
|
9264
|
+
sizeB: match.sizeB,
|
|
9265
|
+
quant: match.quant,
|
|
9266
|
+
...match.activeB !== void 0 ? { activeB: match.activeB } : {}
|
|
9267
|
+
})
|
|
9268
|
+
};
|
|
9269
|
+
const record = await (0, import_core11.createModelProposal)(deps.projectPath, content, {
|
|
9270
|
+
proposedBy: decidedByOf(deps, req)
|
|
9271
|
+
});
|
|
9272
|
+
const outcome = await onApproveModelProposal(handlerDeps(deps, req, pool), record);
|
|
9273
|
+
if (outcome.status === "approved") {
|
|
9274
|
+
const result = {
|
|
9275
|
+
disposition: "installed",
|
|
9276
|
+
proposalId: record.id,
|
|
9277
|
+
evicted: outcome.evicted.map((e) => e.ollamaName)
|
|
9278
|
+
};
|
|
9279
|
+
return sendJSON10(res, 200, result);
|
|
9280
|
+
}
|
|
9281
|
+
if (outcome.status === "failed_target_missing") {
|
|
9282
|
+
return sendJSON10(res, 404, {
|
|
9283
|
+
error: `${hfRepoId} is no longer available on HuggingFace`,
|
|
9284
|
+
proposalId: record.id
|
|
9285
|
+
});
|
|
9286
|
+
}
|
|
9287
|
+
const status = outcome.code === "not_allowed" || outcome.code === "budget_exceeded" ? 409 : 502;
|
|
9288
|
+
return sendJSON10(res, status, {
|
|
9289
|
+
error: outcome.message,
|
|
9290
|
+
code: outcome.code,
|
|
9291
|
+
proposalId: record.id
|
|
9292
|
+
});
|
|
9293
|
+
}
|
|
9294
|
+
async function handleRemove(req, res, deps) {
|
|
9295
|
+
const pool = deps.getModelPool();
|
|
9296
|
+
if (pool === null) return sendJSON10(res, 503, { error: "LMLM disabled" });
|
|
9297
|
+
const body = await readJsonBody(req);
|
|
9298
|
+
if (body === void 0) return sendJSON10(res, 400, { error: "invalid JSON body" });
|
|
9299
|
+
const ollamaName = typeof body.ollamaName === "string" ? body.ollamaName : "";
|
|
9300
|
+
if (ollamaName.length === 0) return sendJSON10(res, 400, { error: "ollamaName is required" });
|
|
9301
|
+
const entry = pool.snapshot().entries.find((e) => e.ollamaName === ollamaName);
|
|
9302
|
+
if (!entry) return sendJSON10(res, 404, { error: `${ollamaName} is not in the pool` });
|
|
9303
|
+
const content = {
|
|
9304
|
+
action: "evict",
|
|
9305
|
+
target: { hfRepoId: entry.hfRepoId, ollamaName },
|
|
9306
|
+
scoreDelta: 0,
|
|
9307
|
+
justification: {
|
|
9308
|
+
summary: `Operator-initiated removal of ${ollamaName}.`,
|
|
9309
|
+
benchmarkBasis: [],
|
|
9310
|
+
hardwareFit: "",
|
|
9311
|
+
evidence: "operator",
|
|
9312
|
+
freshness: ""
|
|
9313
|
+
},
|
|
9314
|
+
diskImpactGb: entry.sizeOnDiskGb
|
|
9315
|
+
};
|
|
9316
|
+
const record = await (0, import_core11.createModelProposal)(deps.projectPath, content, {
|
|
9317
|
+
proposedBy: decidedByOf(deps, req)
|
|
9318
|
+
});
|
|
9319
|
+
const outcome = await onApproveModelProposal(handlerDeps(deps, req, pool), record);
|
|
9320
|
+
if (outcome.status === "error") {
|
|
9321
|
+
return sendJSON10(res, 502, {
|
|
9322
|
+
error: outcome.message,
|
|
9323
|
+
code: outcome.code,
|
|
9324
|
+
proposalId: record.id
|
|
9325
|
+
});
|
|
9326
|
+
}
|
|
9327
|
+
if (outcome.status === "failed_target_missing") {
|
|
9328
|
+
return sendJSON10(res, 500, {
|
|
9329
|
+
error: "unexpected failed_target_missing on evict",
|
|
9330
|
+
proposalId: record.id
|
|
9331
|
+
});
|
|
9332
|
+
}
|
|
9333
|
+
if (outcome.evicted.length > 0) {
|
|
9334
|
+
const result2 = {
|
|
9335
|
+
disposition: "removed",
|
|
9336
|
+
proposalId: record.id,
|
|
9337
|
+
evicted: outcome.evicted.map((e) => e.ollamaName)
|
|
9338
|
+
};
|
|
9339
|
+
return sendJSON10(res, 200, result2);
|
|
9340
|
+
}
|
|
9341
|
+
const stillPresent = pool.snapshot().entries.some((e) => e.ollamaName === ollamaName);
|
|
9342
|
+
if (stillPresent) {
|
|
9343
|
+
const result2 = {
|
|
9344
|
+
disposition: "deferred",
|
|
9345
|
+
proposalId: record.id,
|
|
9346
|
+
evicted: [],
|
|
9347
|
+
message: "model is in use; it will be removed after the current run"
|
|
9348
|
+
};
|
|
9349
|
+
return sendJSON10(res, 202, result2);
|
|
9350
|
+
}
|
|
9351
|
+
const result = { disposition: "removed", proposalId: record.id, evicted: [] };
|
|
9352
|
+
return sendJSON10(res, 200, result);
|
|
9353
|
+
}
|
|
9354
|
+
function handleV1LocalModelsMutationRoute(req, res, deps) {
|
|
9355
|
+
const url = req.url ?? "";
|
|
9356
|
+
if ((req.method ?? "GET") !== "POST") return false;
|
|
9357
|
+
if (INSTALL_RE.test(url)) {
|
|
9358
|
+
void handleInstall(req, res, deps);
|
|
9359
|
+
return true;
|
|
9360
|
+
}
|
|
9361
|
+
if (REMOVE_RE.test(url)) {
|
|
9362
|
+
void handleRemove(req, res, deps);
|
|
9363
|
+
return true;
|
|
9364
|
+
}
|
|
9365
|
+
return false;
|
|
9366
|
+
}
|
|
9367
|
+
|
|
9189
9368
|
// src/server/routes/v1/routing.ts
|
|
9190
9369
|
var import_zod14 = require("zod");
|
|
9191
9370
|
var CONFIG_RE = /^\/api\/v1\/routing\/config(?:\?.*)?$/;
|
|
9192
9371
|
var DECISIONS_RE = /^\/api\/v1\/routing\/decisions(?:\?.*)?$/;
|
|
9193
9372
|
var TRACE_RE = /^\/api\/v1\/routing\/trace(?:\?.*)?$/;
|
|
9194
|
-
function
|
|
9373
|
+
function sendJSON11(res, status, body) {
|
|
9195
9374
|
res.writeHead(status, { "Content-Type": "application/json" });
|
|
9196
9375
|
res.end(JSON.stringify(body));
|
|
9197
9376
|
}
|
|
9198
9377
|
function unavailable(res) {
|
|
9199
|
-
|
|
9378
|
+
sendJSON11(res, 503, { error: "BackendRouter not available" });
|
|
9200
9379
|
return true;
|
|
9201
9380
|
}
|
|
9202
9381
|
function resolveChain(value, backends) {
|
|
@@ -9233,7 +9412,7 @@ function buildResolvedChains(routing, backends) {
|
|
|
9233
9412
|
}
|
|
9234
9413
|
function handleConfig(res, deps) {
|
|
9235
9414
|
if (!deps.router || !deps.routing || !deps.backends) return unavailable(res);
|
|
9236
|
-
|
|
9415
|
+
sendJSON11(res, 200, {
|
|
9237
9416
|
routing: deps.routing,
|
|
9238
9417
|
resolvedChains: buildResolvedChains(deps.routing, deps.backends),
|
|
9239
9418
|
backends: Object.keys(deps.backends)
|
|
@@ -9261,7 +9440,7 @@ function parseDecisionsQuery(url) {
|
|
|
9261
9440
|
function handleDecisions(req, res, deps) {
|
|
9262
9441
|
if (!deps.bus) return unavailable(res);
|
|
9263
9442
|
const filter = parseDecisionsQuery(req.url ?? "");
|
|
9264
|
-
|
|
9443
|
+
sendJSON11(res, 200, { decisions: deps.bus.recent(filter) });
|
|
9265
9444
|
return true;
|
|
9266
9445
|
}
|
|
9267
9446
|
var UseCaseSchema = import_zod14.z.discriminatedUnion("kind", [
|
|
@@ -9293,19 +9472,19 @@ async function handleTrace(req, res, deps) {
|
|
|
9293
9472
|
try {
|
|
9294
9473
|
raw = await readBody(req);
|
|
9295
9474
|
} catch {
|
|
9296
|
-
|
|
9475
|
+
sendJSON11(res, 400, { error: "body read failed" });
|
|
9297
9476
|
return true;
|
|
9298
9477
|
}
|
|
9299
9478
|
let parsed;
|
|
9300
9479
|
try {
|
|
9301
9480
|
parsed = JSON.parse(raw);
|
|
9302
9481
|
} catch {
|
|
9303
|
-
|
|
9482
|
+
sendJSON11(res, 400, { error: "invalid JSON body" });
|
|
9304
9483
|
return true;
|
|
9305
9484
|
}
|
|
9306
9485
|
const r = TraceBodySchema.safeParse(parsed);
|
|
9307
9486
|
if (!r.success) {
|
|
9308
|
-
|
|
9487
|
+
sendJSON11(res, 400, { error: r.error.message });
|
|
9309
9488
|
return true;
|
|
9310
9489
|
}
|
|
9311
9490
|
const opts = r.data.invocationOverride !== void 0 ? { invocationOverride: r.data.invocationOverride } : void 0;
|
|
@@ -9318,9 +9497,9 @@ async function handleTrace(req, res, deps) {
|
|
|
9318
9497
|
r.data.useCase,
|
|
9319
9498
|
opts
|
|
9320
9499
|
);
|
|
9321
|
-
|
|
9500
|
+
sendJSON11(res, 200, { decision, def: { type: def.type } });
|
|
9322
9501
|
} catch (err) {
|
|
9323
|
-
|
|
9502
|
+
sendJSON11(res, 500, { error: String(err) });
|
|
9324
9503
|
}
|
|
9325
9504
|
return true;
|
|
9326
9505
|
}
|
|
@@ -9557,7 +9736,7 @@ var CreateBodySchema = import_zod16.z.object({
|
|
|
9557
9736
|
tenantId: import_zod16.z.string().optional(),
|
|
9558
9737
|
expiresAt: import_zod16.z.string().datetime().optional()
|
|
9559
9738
|
});
|
|
9560
|
-
function
|
|
9739
|
+
function sendJSON12(res, status, body) {
|
|
9561
9740
|
res.writeHead(status, { "Content-Type": "application/json" });
|
|
9562
9741
|
res.end(JSON.stringify(body));
|
|
9563
9742
|
}
|
|
@@ -9567,19 +9746,19 @@ async function handlePost(req, res, store) {
|
|
|
9567
9746
|
raw = await readBody(req);
|
|
9568
9747
|
} catch (err) {
|
|
9569
9748
|
const msg = err instanceof Error ? err.message : "Failed to read body";
|
|
9570
|
-
|
|
9749
|
+
sendJSON12(res, 413, { error: msg });
|
|
9571
9750
|
return;
|
|
9572
9751
|
}
|
|
9573
9752
|
let json;
|
|
9574
9753
|
try {
|
|
9575
9754
|
json = JSON.parse(raw);
|
|
9576
9755
|
} catch {
|
|
9577
|
-
|
|
9756
|
+
sendJSON12(res, 400, { error: "Invalid JSON body" });
|
|
9578
9757
|
return;
|
|
9579
9758
|
}
|
|
9580
9759
|
const parsed = CreateBodySchema.safeParse(json);
|
|
9581
9760
|
if (!parsed.success) {
|
|
9582
|
-
|
|
9761
|
+
sendJSON12(res, 422, { error: "Invalid body", issues: parsed.error.issues });
|
|
9583
9762
|
return;
|
|
9584
9763
|
}
|
|
9585
9764
|
try {
|
|
@@ -9592,37 +9771,37 @@ async function handlePost(req, res, store) {
|
|
|
9592
9771
|
if (parsed.data.expiresAt !== void 0) input.expiresAt = parsed.data.expiresAt;
|
|
9593
9772
|
const result = await store.create(input);
|
|
9594
9773
|
const publicRecord = import_types25.AuthTokenPublicSchema.parse(result.record);
|
|
9595
|
-
|
|
9774
|
+
sendJSON12(res, 200, {
|
|
9596
9775
|
...publicRecord,
|
|
9597
9776
|
token: result.token
|
|
9598
9777
|
});
|
|
9599
9778
|
} catch (err) {
|
|
9600
9779
|
const msg = err instanceof Error ? err.message : "Failed to create token";
|
|
9601
9780
|
if (msg.includes("already exists")) {
|
|
9602
|
-
|
|
9781
|
+
sendJSON12(res, 409, { error: msg });
|
|
9603
9782
|
return;
|
|
9604
9783
|
}
|
|
9605
|
-
|
|
9784
|
+
sendJSON12(res, 500, { error: "Internal error creating token" });
|
|
9606
9785
|
}
|
|
9607
9786
|
}
|
|
9608
9787
|
async function handleList3(res, store) {
|
|
9609
9788
|
try {
|
|
9610
9789
|
const list = await store.list();
|
|
9611
|
-
|
|
9790
|
+
sendJSON12(res, 200, list);
|
|
9612
9791
|
} catch {
|
|
9613
|
-
|
|
9792
|
+
sendJSON12(res, 500, { error: "Internal error listing tokens" });
|
|
9614
9793
|
}
|
|
9615
9794
|
}
|
|
9616
9795
|
async function handleDelete2(res, store, id) {
|
|
9617
9796
|
try {
|
|
9618
9797
|
const ok = await store.revoke(id);
|
|
9619
9798
|
if (!ok) {
|
|
9620
|
-
|
|
9799
|
+
sendJSON12(res, 404, { error: "Token not found" });
|
|
9621
9800
|
return;
|
|
9622
9801
|
}
|
|
9623
|
-
|
|
9802
|
+
sendJSON12(res, 200, { deleted: true });
|
|
9624
9803
|
} catch {
|
|
9625
|
-
|
|
9804
|
+
sendJSON12(res, 500, { error: "Internal error revoking token" });
|
|
9626
9805
|
}
|
|
9627
9806
|
}
|
|
9628
9807
|
var DELETE_PATH_RE2 = /^\/api\/v1\/auth\/tokens\/([^/?]+)(?:\?.*)?$/;
|
|
@@ -9647,12 +9826,12 @@ function handleAuthRoute(req, res, store) {
|
|
|
9647
9826
|
return true;
|
|
9648
9827
|
}
|
|
9649
9828
|
}
|
|
9650
|
-
|
|
9829
|
+
sendJSON12(res, 405, { error: "Method not allowed" });
|
|
9651
9830
|
return true;
|
|
9652
9831
|
}
|
|
9653
9832
|
|
|
9654
9833
|
// src/server/routes/local-model.ts
|
|
9655
|
-
function
|
|
9834
|
+
function sendJSON13(res, status, body) {
|
|
9656
9835
|
res.writeHead(status, { "Content-Type": "application/json" });
|
|
9657
9836
|
res.end(JSON.stringify(body));
|
|
9658
9837
|
}
|
|
@@ -9660,30 +9839,30 @@ function handleLocalModelRoute(req, res, getStatus) {
|
|
|
9660
9839
|
const { method, url } = req;
|
|
9661
9840
|
if (url !== "/api/v1/local-model/status") return false;
|
|
9662
9841
|
if (method !== "GET") {
|
|
9663
|
-
|
|
9842
|
+
sendJSON13(res, 405, { error: "Method not allowed" });
|
|
9664
9843
|
return true;
|
|
9665
9844
|
}
|
|
9666
9845
|
if (!getStatus) {
|
|
9667
|
-
|
|
9846
|
+
sendJSON13(res, 503, { error: "Local backend not configured" });
|
|
9668
9847
|
return true;
|
|
9669
9848
|
}
|
|
9670
9849
|
const status = getStatus();
|
|
9671
9850
|
if (!status) {
|
|
9672
|
-
|
|
9851
|
+
sendJSON13(res, 503, { error: "Local backend not configured" });
|
|
9673
9852
|
return true;
|
|
9674
9853
|
}
|
|
9675
|
-
|
|
9854
|
+
sendJSON13(res, 200, status);
|
|
9676
9855
|
return true;
|
|
9677
9856
|
}
|
|
9678
9857
|
function handleLocalModelsRoute(req, res, getStatuses) {
|
|
9679
9858
|
const { method, url } = req;
|
|
9680
9859
|
if (url !== "/api/v1/local-models/status") return false;
|
|
9681
9860
|
if (method !== "GET") {
|
|
9682
|
-
|
|
9861
|
+
sendJSON13(res, 405, { error: "Method not allowed" });
|
|
9683
9862
|
return true;
|
|
9684
9863
|
}
|
|
9685
9864
|
const statuses = getStatuses ? getStatuses() : [];
|
|
9686
|
-
|
|
9865
|
+
sendJSON13(res, 200, statuses);
|
|
9687
9866
|
return true;
|
|
9688
9867
|
}
|
|
9689
9868
|
|
|
@@ -10056,6 +10235,21 @@ var V1_BRIDGE_ROUTES = [
|
|
|
10056
10235
|
scope: "manage-proposals",
|
|
10057
10236
|
description: "Force a background-scheduler refresh tick and return emitted proposals (O4)."
|
|
10058
10237
|
},
|
|
10238
|
+
// ── LMLM dashboard pool mutation — operator-initiated install/remove ──
|
|
10239
|
+
// Modeled as auto-approved model proposals, so the same `manage-proposals`
|
|
10240
|
+
// write scope that governs approve/reject governs these too.
|
|
10241
|
+
{
|
|
10242
|
+
method: "POST",
|
|
10243
|
+
pattern: /^\/api\/v1\/local-models\/pool\/install(?:\?.*)?$/,
|
|
10244
|
+
scope: "manage-proposals",
|
|
10245
|
+
description: "Install a recommended model into the local pool (auto-approved proposal)."
|
|
10246
|
+
},
|
|
10247
|
+
{
|
|
10248
|
+
method: "POST",
|
|
10249
|
+
pattern: /^\/api\/v1\/local-models\/pool\/remove(?:\?.*)?$/,
|
|
10250
|
+
scope: "manage-proposals",
|
|
10251
|
+
description: "Remove a model from the local pool (auto-approved proposal)."
|
|
10252
|
+
},
|
|
10059
10253
|
// ── LMLM Phase 7 — read surface (hardware/pool/recommendations/proposals) ──
|
|
10060
10254
|
// Load-bearing: `local-models` is in `V1_WRAPPABLE`, so without these entries
|
|
10061
10255
|
// the `/api/v1` rewrite shim would rewrite `/api/v1/local-models/<name>` →
|
|
@@ -10490,6 +10684,17 @@ var OrchestratorServer = class {
|
|
|
10490
10684
|
...this.isModelInUseFn ? { isModelInUse: this.isModelInUseFn } : {}
|
|
10491
10685
|
});
|
|
10492
10686
|
},
|
|
10687
|
+
// LMLM dashboard pool mutation — POST /pool/install + /pool/remove.
|
|
10688
|
+
// Operator-initiated install/remove via auto-approved proposals; reuses
|
|
10689
|
+
// the same mutation pool + bus + in-use probe the proposals route uses.
|
|
10690
|
+
// Registered before the read surface so /pool/install|remove match first.
|
|
10691
|
+
(req, res) => handleV1LocalModelsMutationRoute(req, res, {
|
|
10692
|
+
projectPath: this.projectPath,
|
|
10693
|
+
bus: this.orchestrator,
|
|
10694
|
+
getModelPool: () => this.getModelPoolFn?.() ?? null,
|
|
10695
|
+
...this.getRecommendationsFn ? { getRecommendations: this.getRecommendationsFn } : {},
|
|
10696
|
+
...this.isModelInUseFn ? { isModelInUse: this.isModelInUseFn } : {}
|
|
10697
|
+
}),
|
|
10493
10698
|
// LMLM Phase 6/7 — POST /refresh + the GET read surface
|
|
10494
10699
|
// (hardware/pool/recommendations/proposals). Registered before the
|
|
10495
10700
|
// chat-proxy fallback so it owns the path. Each accessor is spread in
|
|
@@ -10583,7 +10788,7 @@ var OrchestratorServer = class {
|
|
|
10583
10788
|
return this.broadcaster.clientCount;
|
|
10584
10789
|
}
|
|
10585
10790
|
async start() {
|
|
10586
|
-
(0,
|
|
10791
|
+
(0, import_core12.assertPortUsable)(this.port, "orchestrator");
|
|
10587
10792
|
if (this.interactionQueue) {
|
|
10588
10793
|
this.planWatcher = new PlanWatcher(this.plansDir, this.interactionQueue);
|
|
10589
10794
|
this.planWatcher.start();
|
|
@@ -11085,7 +11290,7 @@ function wireWebhookFanout({ bus, store, delivery }) {
|
|
|
11085
11290
|
|
|
11086
11291
|
// src/gateway/telemetry/fanout.ts
|
|
11087
11292
|
var import_node_crypto13 = require("crypto");
|
|
11088
|
-
var
|
|
11293
|
+
var import_core13 = require("@harness-engineering/core");
|
|
11089
11294
|
var TOPICS = {
|
|
11090
11295
|
MAINTENANCE_STARTED: "maintenance:started",
|
|
11091
11296
|
MAINTENANCE_COMPLETED: "maintenance:completed",
|
|
@@ -11217,7 +11422,7 @@ function buildSpan(topic, plan, payload) {
|
|
|
11217
11422
|
spanId: plan.spanId,
|
|
11218
11423
|
...plan.parentSpanId !== void 0 ? { parentSpanId: plan.parentSpanId } : {},
|
|
11219
11424
|
name: SPAN_NAME[topic],
|
|
11220
|
-
kind:
|
|
11425
|
+
kind: import_core13.SpanKind.INTERNAL,
|
|
11221
11426
|
startTimeNs: startNs,
|
|
11222
11427
|
endTimeNs: startNs,
|
|
11223
11428
|
attributes: buildAttributes(payload, { "harness.topic": topic }),
|
|
@@ -11674,7 +11879,7 @@ function wireNotificationSinks({ bus, registry }) {
|
|
|
11674
11879
|
}
|
|
11675
11880
|
|
|
11676
11881
|
// src/orchestrator.ts
|
|
11677
|
-
var
|
|
11882
|
+
var import_core19 = require("@harness-engineering/core");
|
|
11678
11883
|
|
|
11679
11884
|
// src/logging/logger.ts
|
|
11680
11885
|
var StructuredLogger = class {
|
|
@@ -11716,7 +11921,7 @@ var StructuredLogger = class {
|
|
|
11716
11921
|
// src/workspace/config-scanner.ts
|
|
11717
11922
|
var import_node_fs = require("fs");
|
|
11718
11923
|
var import_node_path4 = require("path");
|
|
11719
|
-
var
|
|
11924
|
+
var import_core14 = require("@harness-engineering/core");
|
|
11720
11925
|
var CONFIG_FILES = ["CLAUDE.md", "AGENTS.md", ".gemini/settings.json", "skill.yaml"];
|
|
11721
11926
|
var BLOCKING_INJECTION_PREFIXES = ["INJ-UNI-", "INJ-REROL-"];
|
|
11722
11927
|
var DOWNGRADED_SECURITY_RULES = /* @__PURE__ */ new Set(["SEC-AGT-006"]);
|
|
@@ -11738,29 +11943,29 @@ async function scanSingleFile(filePath, targetDir, scanner) {
|
|
|
11738
11943
|
} catch {
|
|
11739
11944
|
return null;
|
|
11740
11945
|
}
|
|
11741
|
-
const injectionFindings = (0,
|
|
11742
|
-
const findings = (0,
|
|
11946
|
+
const injectionFindings = (0, import_core14.scanForInjection)(content);
|
|
11947
|
+
const findings = (0, import_core14.mapInjectionFindings)(injectionFindings);
|
|
11743
11948
|
const secFindings = await scanner.scanFile(filePath);
|
|
11744
|
-
findings.push(...(0,
|
|
11949
|
+
findings.push(...(0, import_core14.mapSecurityFindings)(secFindings, findings));
|
|
11745
11950
|
const adjusted = adjustFindingSeverity(findings);
|
|
11746
11951
|
return {
|
|
11747
11952
|
file: (0, import_node_path4.relative)(targetDir, filePath).replaceAll("\\", "/"),
|
|
11748
11953
|
findings: adjusted,
|
|
11749
|
-
overallSeverity: (0,
|
|
11954
|
+
overallSeverity: (0, import_core14.computeOverallSeverity)(adjusted)
|
|
11750
11955
|
};
|
|
11751
11956
|
}
|
|
11752
11957
|
async function scanWorkspaceConfig(workspacePath) {
|
|
11753
|
-
const scanner = new
|
|
11958
|
+
const scanner = new import_core14.SecurityScanner((0, import_core14.parseSecurityConfig)({}));
|
|
11754
11959
|
const results = [];
|
|
11755
11960
|
for (const configFile of CONFIG_FILES) {
|
|
11756
11961
|
const result = await scanSingleFile((0, import_node_path4.join)(workspacePath, configFile), workspacePath, scanner);
|
|
11757
11962
|
if (result) results.push(result);
|
|
11758
11963
|
}
|
|
11759
|
-
return { exitCode: (0,
|
|
11964
|
+
return { exitCode: (0, import_core14.computeScanExitCode)(results), results };
|
|
11760
11965
|
}
|
|
11761
11966
|
|
|
11762
11967
|
// src/core/lane-persistence.ts
|
|
11763
|
-
var
|
|
11968
|
+
var import_core15 = require("@harness-engineering/core");
|
|
11764
11969
|
var import_types29 = require("@harness-engineering/types");
|
|
11765
11970
|
var SIGNAL_TO_LANE = {
|
|
11766
11971
|
claim: "claimed",
|
|
@@ -11774,16 +11979,16 @@ function mapOrchestratorLane(signal) {
|
|
|
11774
11979
|
}
|
|
11775
11980
|
async function persistLane(projectPath, issueId, signal) {
|
|
11776
11981
|
try {
|
|
11777
|
-
const reg = await
|
|
11982
|
+
const reg = await import_core15.eventSourcing.registerTask(projectPath, issueId, []);
|
|
11778
11983
|
if (!reg.ok) return reg;
|
|
11779
|
-
return await
|
|
11984
|
+
return await import_core15.eventSourcing.transitionLane(projectPath, issueId, mapOrchestratorLane(signal));
|
|
11780
11985
|
} catch (err) {
|
|
11781
11986
|
return (0, import_types29.Err)(err instanceof Error ? err : new Error(String(err)));
|
|
11782
11987
|
}
|
|
11783
11988
|
}
|
|
11784
11989
|
async function readPersistedLanes(projectPath) {
|
|
11785
11990
|
try {
|
|
11786
|
-
const snap = await
|
|
11991
|
+
const snap = await import_core15.eventSourcing.readSnapshot(projectPath);
|
|
11787
11992
|
return snap.ok ? snap.value.lanes : { tasks: {} };
|
|
11788
11993
|
} catch {
|
|
11789
11994
|
return { tasks: {} };
|
|
@@ -13884,7 +14089,7 @@ var Orchestrator = class extends import_node_events.EventEmitter {
|
|
|
13884
14089
|
if (overrides?.poolState) {
|
|
13885
14090
|
this.poolStateProvider = overrides.poolState;
|
|
13886
14091
|
} else if (localModelsEnabled) {
|
|
13887
|
-
this.poolStateStore = new
|
|
14092
|
+
this.poolStateStore = new import_local_models5.PoolStateStore({
|
|
13888
14093
|
onWarn: (message, cause) => this.logger.warn(message, cause !== void 0 ? { cause } : void 0)
|
|
13889
14094
|
});
|
|
13890
14095
|
this.poolStateProvider = this.poolStateStore;
|
|
@@ -13904,7 +14109,7 @@ var Orchestrator = class extends import_node_events.EventEmitter {
|
|
|
13904
14109
|
this.localResolvers.set(name, new LocalModelResolver(resolverOpts));
|
|
13905
14110
|
}
|
|
13906
14111
|
}
|
|
13907
|
-
this.cacheMetrics = new
|
|
14112
|
+
this.cacheMetrics = new import_core19.CacheMetricsRecorder();
|
|
13908
14113
|
if (this.config.agent.backends !== void 0 && Object.keys(this.config.agent.backends).length > 0) {
|
|
13909
14114
|
const sandboxPolicy = this.config.agent.sandboxPolicy === "docker" ? "docker" : "none";
|
|
13910
14115
|
const firstBackendName = Object.keys(this.config.agent.backends)[0];
|
|
@@ -14031,7 +14236,7 @@ var Orchestrator = class extends import_node_events.EventEmitter {
|
|
|
14031
14236
|
const { ranked } = await this.modelRecommender(hardware);
|
|
14032
14237
|
return ranked.slice(0, top);
|
|
14033
14238
|
},
|
|
14034
|
-
listModelProposals: () => (0,
|
|
14239
|
+
listModelProposals: () => (0, import_core18.listProposals)(this.projectRoot, { status: "open", kind: "model" })
|
|
14035
14240
|
});
|
|
14036
14241
|
this.server.setRecorder(this.recorder);
|
|
14037
14242
|
this.interactionQueue.onPush((interaction) => {
|
|
@@ -14048,7 +14253,7 @@ var Orchestrator = class extends import_node_events.EventEmitter {
|
|
|
14048
14253
|
setupTelemetryExport(config, webhookStore, webhookDelivery) {
|
|
14049
14254
|
const otlpCfg = config.telemetry?.export?.otlp;
|
|
14050
14255
|
if (!otlpCfg) return;
|
|
14051
|
-
this.otlpExporter = new
|
|
14256
|
+
this.otlpExporter = new import_core19.OTLPExporter({
|
|
14052
14257
|
endpoint: otlpCfg.endpoint,
|
|
14053
14258
|
...otlpCfg.enabled !== void 0 ? { enabled: otlpCfg.enabled } : {},
|
|
14054
14259
|
...otlpCfg.headers !== void 0 ? { headers: otlpCfg.headers } : {},
|
|
@@ -14151,7 +14356,7 @@ var Orchestrator = class extends import_node_events.EventEmitter {
|
|
|
14151
14356
|
...this.config.tracker.apiKey ? { token: this.config.tracker.apiKey } : {},
|
|
14152
14357
|
...this.config.tracker.endpoint ? { apiBase: this.config.tracker.endpoint } : {}
|
|
14153
14358
|
};
|
|
14154
|
-
const clientResult = (0,
|
|
14359
|
+
const clientResult = (0, import_core17.createTrackerClient)(trackerCfg);
|
|
14155
14360
|
if (!clientResult.ok) throw clientResult.error;
|
|
14156
14361
|
return new GitHubIssuesIssueTrackerAdapter(clientResult.value, this.config.tracker);
|
|
14157
14362
|
}
|
|
@@ -14730,12 +14935,12 @@ ${messages}`);
|
|
|
14730
14935
|
async postLifecycleComment(identifier, externalId, event) {
|
|
14731
14936
|
try {
|
|
14732
14937
|
if (!externalId) return;
|
|
14733
|
-
const trackerConfig = (0,
|
|
14938
|
+
const trackerConfig = (0, import_core17.loadTrackerSyncConfig)(this.projectRoot);
|
|
14734
14939
|
if (!trackerConfig) return;
|
|
14735
14940
|
const token = process.env.GITHUB_TOKEN;
|
|
14736
14941
|
if (!token) return;
|
|
14737
14942
|
const orchestratorId = await this.orchestratorIdPromise;
|
|
14738
|
-
const adapter = new
|
|
14943
|
+
const adapter = new import_core17.GitHubIssuesSyncAdapter({ token, config: trackerConfig });
|
|
14739
14944
|
const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace("T", " ").slice(0, 19);
|
|
14740
14945
|
const actionMap = {
|
|
14741
14946
|
claimed: "Dispatching agent for autonomous execution",
|
|
@@ -14809,7 +15014,7 @@ ${messages}`);
|
|
|
14809
15014
|
...f.line !== void 0 ? { line: f.line } : {}
|
|
14810
15015
|
}))
|
|
14811
15016
|
);
|
|
14812
|
-
(0,
|
|
15017
|
+
(0, import_core16.writeTaint)(
|
|
14813
15018
|
workspacePath,
|
|
14814
15019
|
issue.id,
|
|
14815
15020
|
"Medium-severity injection patterns found in workspace config files",
|
|
@@ -15087,11 +15292,29 @@ ${messages}`);
|
|
|
15087
15292
|
initModelPool(store) {
|
|
15088
15293
|
const onWarn = (message, cause) => this.logger.warn(message, cause !== void 0 ? { cause } : void 0);
|
|
15089
15294
|
const installerCfg = this.config.localModels?.installer;
|
|
15090
|
-
this.modelInstaller = new
|
|
15295
|
+
this.modelInstaller = new import_local_models5.OllamaInstallAdapter({
|
|
15091
15296
|
baseUrl: installerCfg?.ollamaEndpoint ?? "http://localhost:11434",
|
|
15092
15297
|
onWarn
|
|
15093
15298
|
});
|
|
15094
|
-
this.modelPool = new
|
|
15299
|
+
this.modelPool = new import_local_models5.PoolManager({ store, installer: this.modelInstaller, onWarn });
|
|
15300
|
+
}
|
|
15301
|
+
/**
|
|
15302
|
+
* LMLM Phase 7 wiring: apply the operator's configured pool bounds (disk
|
|
15303
|
+
* budget + org/family allowlist) from `localModels.pool` to the live pool.
|
|
15304
|
+
* Called after `PoolStateStore.load()` so config wins over persisted bounds
|
|
15305
|
+
* (D2, declarative precedence). No-op when the pool is null (LMLM disabled)
|
|
15306
|
+
* or no `pool` block is configured, so it is safe to call unconditionally
|
|
15307
|
+
* on the startup path.
|
|
15308
|
+
*/
|
|
15309
|
+
async applyConfiguredPoolBounds() {
|
|
15310
|
+
const pool = this.modelPool;
|
|
15311
|
+
const bounds = this.config.localModels?.pool;
|
|
15312
|
+
if (pool === null || !bounds) return;
|
|
15313
|
+
await pool.configurePool({
|
|
15314
|
+
diskBudgetGb: bounds.diskBudgetGb,
|
|
15315
|
+
allowedOrgs: bounds.allowedOrgs,
|
|
15316
|
+
allowedFamilies: bounds.allowedFamilies
|
|
15317
|
+
});
|
|
15095
15318
|
}
|
|
15096
15319
|
/**
|
|
15097
15320
|
* LMLM Phase 6: arm the single background refresh scheduler over the live
|
|
@@ -15109,10 +15332,21 @@ ${messages}`);
|
|
|
15109
15332
|
if (this.modelPool === null) return;
|
|
15110
15333
|
const pool = this.modelPool;
|
|
15111
15334
|
const refreshCfg = this.config.localModels?.refresh;
|
|
15112
|
-
const
|
|
15335
|
+
const frozen = (0, import_local_models5.loadFrozenCandidates)();
|
|
15336
|
+
const candidates = (0, import_local_models5.selectCandidates)(frozen.candidates, this.config.localModels?.pool);
|
|
15337
|
+
for (const warning of frozen.warnings) {
|
|
15338
|
+
this.logger.warn("LMLM frozen candidate snapshot degraded", { warning });
|
|
15339
|
+
}
|
|
15340
|
+
if (candidates.length === 0) {
|
|
15341
|
+
this.logger.warn("LMLM recommender has no candidates", {
|
|
15342
|
+
frozenCount: frozen.candidates.length,
|
|
15343
|
+
allowedOrgs: this.config.localModels?.pool?.allowedOrgs ?? []
|
|
15344
|
+
});
|
|
15345
|
+
}
|
|
15346
|
+
const recommend = (0, import_local_models5.createNativeRecommender)({ candidates });
|
|
15113
15347
|
this.modelRecommender = recommend;
|
|
15114
|
-
this.refreshScheduler = new
|
|
15115
|
-
runTick: () => (0,
|
|
15348
|
+
this.refreshScheduler = new import_local_models5.RefreshScheduler({
|
|
15349
|
+
runTick: () => (0, import_local_models5.runRefreshTick)({
|
|
15116
15350
|
detectHardware: () => this.detectLmlmHardware(),
|
|
15117
15351
|
recommend,
|
|
15118
15352
|
poolManager: pool,
|
|
@@ -15120,7 +15354,7 @@ ${messages}`);
|
|
|
15120
15354
|
// Phase 7: after persisting the proposal, emit `local-models:proposal`
|
|
15121
15355
|
// (== MODEL_PROPOSAL_TOPIC) on the bus so it fans out to WS clients and
|
|
15122
15356
|
// notification sinks. Literal to avoid a proposals/model-handlers cycle.
|
|
15123
|
-
emitProposal: (c) => (0,
|
|
15357
|
+
emitProposal: (c) => (0, import_core18.createModelProposal)(this.projectRoot, c).then((record) => {
|
|
15124
15358
|
this.emit("local-models:proposal", {
|
|
15125
15359
|
id: record.id,
|
|
15126
15360
|
status: "created",
|
|
@@ -15143,12 +15377,12 @@ ${messages}`);
|
|
|
15143
15377
|
/** Resolve the hardware profile for a refresh tick (operator override wins). */
|
|
15144
15378
|
async detectLmlmHardware() {
|
|
15145
15379
|
const override = this.config.localModels?.hardware?.override;
|
|
15146
|
-
const detector = new
|
|
15380
|
+
const detector = new import_local_models5.HardwareDetector(override !== void 0 ? { override } : {});
|
|
15147
15381
|
return (await detector.detect()).profile;
|
|
15148
15382
|
}
|
|
15149
15383
|
/** Map the on-disk model-proposal queue to F7 dedup pairs (open→pending, rejected→rejected). */
|
|
15150
15384
|
async lmlmDedupSource() {
|
|
15151
|
-
const proposals = await (0,
|
|
15385
|
+
const proposals = await (0, import_core18.listProposals)(this.projectRoot, { kind: "model" });
|
|
15152
15386
|
const pending = [];
|
|
15153
15387
|
const rejected = [];
|
|
15154
15388
|
for (const p of proposals) {
|
|
@@ -15186,6 +15420,7 @@ ${messages}`);
|
|
|
15186
15420
|
}
|
|
15187
15421
|
if (this.poolStateStore !== null) {
|
|
15188
15422
|
await this.poolStateStore.load();
|
|
15423
|
+
await this.applyConfiguredPoolBounds();
|
|
15189
15424
|
}
|
|
15190
15425
|
for (const resolver of this.localResolvers.values()) {
|
|
15191
15426
|
await resolver.start();
|
package/dist/index.mjs
CHANGED
|
@@ -3976,9 +3976,11 @@ import {
|
|
|
3976
3976
|
HardwareDetector,
|
|
3977
3977
|
RefreshScheduler,
|
|
3978
3978
|
runRefreshTick,
|
|
3979
|
-
createNativeRecommender
|
|
3979
|
+
createNativeRecommender,
|
|
3980
|
+
loadFrozenCandidates,
|
|
3981
|
+
selectCandidates as selectCandidates2
|
|
3980
3982
|
} from "@harness-engineering/local-models";
|
|
3981
|
-
import { createModelProposal, listProposals as listProposals2 } from "@harness-engineering/core";
|
|
3983
|
+
import { createModelProposal as createModelProposal2, listProposals as listProposals2 } from "@harness-engineering/core";
|
|
3982
3984
|
|
|
3983
3985
|
// src/agent/config-migration.ts
|
|
3984
3986
|
var MIGRATION_GUIDE = "docs/guides/multi-backend-routing.md";
|
|
@@ -9139,17 +9141,196 @@ async function runForceRefresh(res, scheduler, deps) {
|
|
|
9139
9141
|
});
|
|
9140
9142
|
}
|
|
9141
9143
|
|
|
9144
|
+
// src/server/routes/v1/local-models-pool-mutation.ts
|
|
9145
|
+
import { createModelProposal, updateProposal as updateProposal4 } from "@harness-engineering/core";
|
|
9146
|
+
import { estimateDiskGb } from "@harness-engineering/local-models";
|
|
9147
|
+
var INSTALL_RE = /^\/api\/v1\/local-models\/pool\/install(?:\?.*)?$/;
|
|
9148
|
+
var REMOVE_RE = /^\/api\/v1\/local-models\/pool\/remove(?:\?.*)?$/;
|
|
9149
|
+
var RESOLVE_TOP = 50;
|
|
9150
|
+
function sendJSON10(res, status, body) {
|
|
9151
|
+
res.writeHead(status, { "Content-Type": "application/json" });
|
|
9152
|
+
res.end(JSON.stringify(body));
|
|
9153
|
+
}
|
|
9154
|
+
function decidedByOf(deps, req) {
|
|
9155
|
+
if (deps.getDecidedBy) return deps.getDecidedBy(req);
|
|
9156
|
+
const token = req._authToken;
|
|
9157
|
+
return token?.id ?? "operator";
|
|
9158
|
+
}
|
|
9159
|
+
function handlerDeps(deps, req, pool) {
|
|
9160
|
+
return {
|
|
9161
|
+
pool,
|
|
9162
|
+
bus: deps.bus,
|
|
9163
|
+
updateProposal: (id, patch) => updateProposal4(deps.projectPath, id, patch),
|
|
9164
|
+
decidedBy: decidedByOf(deps, req),
|
|
9165
|
+
...deps.isModelInUse ? { isModelInUse: deps.isModelInUse } : {}
|
|
9166
|
+
};
|
|
9167
|
+
}
|
|
9168
|
+
async function readJsonBody(req) {
|
|
9169
|
+
try {
|
|
9170
|
+
const raw = await readBody(req);
|
|
9171
|
+
if (raw.length === 0) return {};
|
|
9172
|
+
const parsed = JSON.parse(raw);
|
|
9173
|
+
return typeof parsed === "object" && parsed !== null ? parsed : void 0;
|
|
9174
|
+
} catch {
|
|
9175
|
+
return void 0;
|
|
9176
|
+
}
|
|
9177
|
+
}
|
|
9178
|
+
async function handleInstall(req, res, deps) {
|
|
9179
|
+
const pool = deps.getModelPool();
|
|
9180
|
+
if (pool === null) return sendJSON10(res, 503, { error: "LMLM disabled" });
|
|
9181
|
+
const body = await readJsonBody(req);
|
|
9182
|
+
if (body === void 0) return sendJSON10(res, 400, { error: "invalid JSON body" });
|
|
9183
|
+
const hfRepoId = typeof body.hfRepoId === "string" ? body.hfRepoId : "";
|
|
9184
|
+
if (hfRepoId.length === 0) return sendJSON10(res, 400, { error: "hfRepoId is required" });
|
|
9185
|
+
const quant = typeof body.quant === "string" ? body.quant : void 0;
|
|
9186
|
+
if (!deps.getRecommendations) {
|
|
9187
|
+
return sendJSON10(res, 503, { error: "recommendations unavailable" });
|
|
9188
|
+
}
|
|
9189
|
+
const ranked = await deps.getRecommendations({ top: RESOLVE_TOP, profile: "general" });
|
|
9190
|
+
const match = ranked.find(
|
|
9191
|
+
(r) => r.hfRepoId === hfRepoId && (quant === void 0 || r.quant === quant)
|
|
9192
|
+
);
|
|
9193
|
+
if (!match) {
|
|
9194
|
+
return sendJSON10(res, 404, {
|
|
9195
|
+
error: `no recommendation for ${hfRepoId}${quant ? ` @ ${quant}` : ""}`
|
|
9196
|
+
});
|
|
9197
|
+
}
|
|
9198
|
+
if (!match.ollamaName) {
|
|
9199
|
+
return sendJSON10(res, 422, { error: `${hfRepoId} has no known Ollama mirror to install` });
|
|
9200
|
+
}
|
|
9201
|
+
const content = {
|
|
9202
|
+
action: "add",
|
|
9203
|
+
target: { hfRepoId: match.hfRepoId, ollamaName: match.ollamaName },
|
|
9204
|
+
scoreDelta: match.score,
|
|
9205
|
+
justification: {
|
|
9206
|
+
summary: `Operator-initiated install of ${match.ollamaName} (${match.quant}).`,
|
|
9207
|
+
benchmarkBasis: [],
|
|
9208
|
+
hardwareFit: match.fitsHardware ? "fits detected hardware" : "may not fit detected hardware",
|
|
9209
|
+
evidence: match.evidence,
|
|
9210
|
+
freshness: `snapshot ${match.benchmarkSnapshot}`
|
|
9211
|
+
},
|
|
9212
|
+
// Estimate the on-disk size from the candidate's params + quant (the same
|
|
9213
|
+
// `estimateDiskGb` the automated proposal engine uses). The pool needs a
|
|
9214
|
+
// size for its pre-commit budget check *before* the pull; it cannot inspect
|
|
9215
|
+
// the target first, because ollama `/api/show` 404s for a model that is not
|
|
9216
|
+
// yet pulled locally — which would surface as a spurious "no longer
|
|
9217
|
+
// available on HuggingFace" 404 on every operator install.
|
|
9218
|
+
diskImpactGb: estimateDiskGb({
|
|
9219
|
+
sizeB: match.sizeB,
|
|
9220
|
+
quant: match.quant,
|
|
9221
|
+
...match.activeB !== void 0 ? { activeB: match.activeB } : {}
|
|
9222
|
+
})
|
|
9223
|
+
};
|
|
9224
|
+
const record = await createModelProposal(deps.projectPath, content, {
|
|
9225
|
+
proposedBy: decidedByOf(deps, req)
|
|
9226
|
+
});
|
|
9227
|
+
const outcome = await onApproveModelProposal(handlerDeps(deps, req, pool), record);
|
|
9228
|
+
if (outcome.status === "approved") {
|
|
9229
|
+
const result = {
|
|
9230
|
+
disposition: "installed",
|
|
9231
|
+
proposalId: record.id,
|
|
9232
|
+
evicted: outcome.evicted.map((e) => e.ollamaName)
|
|
9233
|
+
};
|
|
9234
|
+
return sendJSON10(res, 200, result);
|
|
9235
|
+
}
|
|
9236
|
+
if (outcome.status === "failed_target_missing") {
|
|
9237
|
+
return sendJSON10(res, 404, {
|
|
9238
|
+
error: `${hfRepoId} is no longer available on HuggingFace`,
|
|
9239
|
+
proposalId: record.id
|
|
9240
|
+
});
|
|
9241
|
+
}
|
|
9242
|
+
const status = outcome.code === "not_allowed" || outcome.code === "budget_exceeded" ? 409 : 502;
|
|
9243
|
+
return sendJSON10(res, status, {
|
|
9244
|
+
error: outcome.message,
|
|
9245
|
+
code: outcome.code,
|
|
9246
|
+
proposalId: record.id
|
|
9247
|
+
});
|
|
9248
|
+
}
|
|
9249
|
+
async function handleRemove(req, res, deps) {
|
|
9250
|
+
const pool = deps.getModelPool();
|
|
9251
|
+
if (pool === null) return sendJSON10(res, 503, { error: "LMLM disabled" });
|
|
9252
|
+
const body = await readJsonBody(req);
|
|
9253
|
+
if (body === void 0) return sendJSON10(res, 400, { error: "invalid JSON body" });
|
|
9254
|
+
const ollamaName = typeof body.ollamaName === "string" ? body.ollamaName : "";
|
|
9255
|
+
if (ollamaName.length === 0) return sendJSON10(res, 400, { error: "ollamaName is required" });
|
|
9256
|
+
const entry = pool.snapshot().entries.find((e) => e.ollamaName === ollamaName);
|
|
9257
|
+
if (!entry) return sendJSON10(res, 404, { error: `${ollamaName} is not in the pool` });
|
|
9258
|
+
const content = {
|
|
9259
|
+
action: "evict",
|
|
9260
|
+
target: { hfRepoId: entry.hfRepoId, ollamaName },
|
|
9261
|
+
scoreDelta: 0,
|
|
9262
|
+
justification: {
|
|
9263
|
+
summary: `Operator-initiated removal of ${ollamaName}.`,
|
|
9264
|
+
benchmarkBasis: [],
|
|
9265
|
+
hardwareFit: "",
|
|
9266
|
+
evidence: "operator",
|
|
9267
|
+
freshness: ""
|
|
9268
|
+
},
|
|
9269
|
+
diskImpactGb: entry.sizeOnDiskGb
|
|
9270
|
+
};
|
|
9271
|
+
const record = await createModelProposal(deps.projectPath, content, {
|
|
9272
|
+
proposedBy: decidedByOf(deps, req)
|
|
9273
|
+
});
|
|
9274
|
+
const outcome = await onApproveModelProposal(handlerDeps(deps, req, pool), record);
|
|
9275
|
+
if (outcome.status === "error") {
|
|
9276
|
+
return sendJSON10(res, 502, {
|
|
9277
|
+
error: outcome.message,
|
|
9278
|
+
code: outcome.code,
|
|
9279
|
+
proposalId: record.id
|
|
9280
|
+
});
|
|
9281
|
+
}
|
|
9282
|
+
if (outcome.status === "failed_target_missing") {
|
|
9283
|
+
return sendJSON10(res, 500, {
|
|
9284
|
+
error: "unexpected failed_target_missing on evict",
|
|
9285
|
+
proposalId: record.id
|
|
9286
|
+
});
|
|
9287
|
+
}
|
|
9288
|
+
if (outcome.evicted.length > 0) {
|
|
9289
|
+
const result2 = {
|
|
9290
|
+
disposition: "removed",
|
|
9291
|
+
proposalId: record.id,
|
|
9292
|
+
evicted: outcome.evicted.map((e) => e.ollamaName)
|
|
9293
|
+
};
|
|
9294
|
+
return sendJSON10(res, 200, result2);
|
|
9295
|
+
}
|
|
9296
|
+
const stillPresent = pool.snapshot().entries.some((e) => e.ollamaName === ollamaName);
|
|
9297
|
+
if (stillPresent) {
|
|
9298
|
+
const result2 = {
|
|
9299
|
+
disposition: "deferred",
|
|
9300
|
+
proposalId: record.id,
|
|
9301
|
+
evicted: [],
|
|
9302
|
+
message: "model is in use; it will be removed after the current run"
|
|
9303
|
+
};
|
|
9304
|
+
return sendJSON10(res, 202, result2);
|
|
9305
|
+
}
|
|
9306
|
+
const result = { disposition: "removed", proposalId: record.id, evicted: [] };
|
|
9307
|
+
return sendJSON10(res, 200, result);
|
|
9308
|
+
}
|
|
9309
|
+
function handleV1LocalModelsMutationRoute(req, res, deps) {
|
|
9310
|
+
const url = req.url ?? "";
|
|
9311
|
+
if ((req.method ?? "GET") !== "POST") return false;
|
|
9312
|
+
if (INSTALL_RE.test(url)) {
|
|
9313
|
+
void handleInstall(req, res, deps);
|
|
9314
|
+
return true;
|
|
9315
|
+
}
|
|
9316
|
+
if (REMOVE_RE.test(url)) {
|
|
9317
|
+
void handleRemove(req, res, deps);
|
|
9318
|
+
return true;
|
|
9319
|
+
}
|
|
9320
|
+
return false;
|
|
9321
|
+
}
|
|
9322
|
+
|
|
9142
9323
|
// src/server/routes/v1/routing.ts
|
|
9143
9324
|
import { z as z14 } from "zod";
|
|
9144
9325
|
var CONFIG_RE = /^\/api\/v1\/routing\/config(?:\?.*)?$/;
|
|
9145
9326
|
var DECISIONS_RE = /^\/api\/v1\/routing\/decisions(?:\?.*)?$/;
|
|
9146
9327
|
var TRACE_RE = /^\/api\/v1\/routing\/trace(?:\?.*)?$/;
|
|
9147
|
-
function
|
|
9328
|
+
function sendJSON11(res, status, body) {
|
|
9148
9329
|
res.writeHead(status, { "Content-Type": "application/json" });
|
|
9149
9330
|
res.end(JSON.stringify(body));
|
|
9150
9331
|
}
|
|
9151
9332
|
function unavailable(res) {
|
|
9152
|
-
|
|
9333
|
+
sendJSON11(res, 503, { error: "BackendRouter not available" });
|
|
9153
9334
|
return true;
|
|
9154
9335
|
}
|
|
9155
9336
|
function resolveChain(value, backends) {
|
|
@@ -9186,7 +9367,7 @@ function buildResolvedChains(routing, backends) {
|
|
|
9186
9367
|
}
|
|
9187
9368
|
function handleConfig(res, deps) {
|
|
9188
9369
|
if (!deps.router || !deps.routing || !deps.backends) return unavailable(res);
|
|
9189
|
-
|
|
9370
|
+
sendJSON11(res, 200, {
|
|
9190
9371
|
routing: deps.routing,
|
|
9191
9372
|
resolvedChains: buildResolvedChains(deps.routing, deps.backends),
|
|
9192
9373
|
backends: Object.keys(deps.backends)
|
|
@@ -9214,7 +9395,7 @@ function parseDecisionsQuery(url) {
|
|
|
9214
9395
|
function handleDecisions(req, res, deps) {
|
|
9215
9396
|
if (!deps.bus) return unavailable(res);
|
|
9216
9397
|
const filter = parseDecisionsQuery(req.url ?? "");
|
|
9217
|
-
|
|
9398
|
+
sendJSON11(res, 200, { decisions: deps.bus.recent(filter) });
|
|
9218
9399
|
return true;
|
|
9219
9400
|
}
|
|
9220
9401
|
var UseCaseSchema = z14.discriminatedUnion("kind", [
|
|
@@ -9246,19 +9427,19 @@ async function handleTrace(req, res, deps) {
|
|
|
9246
9427
|
try {
|
|
9247
9428
|
raw = await readBody(req);
|
|
9248
9429
|
} catch {
|
|
9249
|
-
|
|
9430
|
+
sendJSON11(res, 400, { error: "body read failed" });
|
|
9250
9431
|
return true;
|
|
9251
9432
|
}
|
|
9252
9433
|
let parsed;
|
|
9253
9434
|
try {
|
|
9254
9435
|
parsed = JSON.parse(raw);
|
|
9255
9436
|
} catch {
|
|
9256
|
-
|
|
9437
|
+
sendJSON11(res, 400, { error: "invalid JSON body" });
|
|
9257
9438
|
return true;
|
|
9258
9439
|
}
|
|
9259
9440
|
const r = TraceBodySchema.safeParse(parsed);
|
|
9260
9441
|
if (!r.success) {
|
|
9261
|
-
|
|
9442
|
+
sendJSON11(res, 400, { error: r.error.message });
|
|
9262
9443
|
return true;
|
|
9263
9444
|
}
|
|
9264
9445
|
const opts = r.data.invocationOverride !== void 0 ? { invocationOverride: r.data.invocationOverride } : void 0;
|
|
@@ -9271,9 +9452,9 @@ async function handleTrace(req, res, deps) {
|
|
|
9271
9452
|
r.data.useCase,
|
|
9272
9453
|
opts
|
|
9273
9454
|
);
|
|
9274
|
-
|
|
9455
|
+
sendJSON11(res, 200, { decision, def: { type: def.type } });
|
|
9275
9456
|
} catch (err) {
|
|
9276
|
-
|
|
9457
|
+
sendJSON11(res, 500, { error: String(err) });
|
|
9277
9458
|
}
|
|
9278
9459
|
return true;
|
|
9279
9460
|
}
|
|
@@ -9514,7 +9695,7 @@ var CreateBodySchema = z16.object({
|
|
|
9514
9695
|
tenantId: z16.string().optional(),
|
|
9515
9696
|
expiresAt: z16.string().datetime().optional()
|
|
9516
9697
|
});
|
|
9517
|
-
function
|
|
9698
|
+
function sendJSON12(res, status, body) {
|
|
9518
9699
|
res.writeHead(status, { "Content-Type": "application/json" });
|
|
9519
9700
|
res.end(JSON.stringify(body));
|
|
9520
9701
|
}
|
|
@@ -9524,19 +9705,19 @@ async function handlePost(req, res, store) {
|
|
|
9524
9705
|
raw = await readBody(req);
|
|
9525
9706
|
} catch (err) {
|
|
9526
9707
|
const msg = err instanceof Error ? err.message : "Failed to read body";
|
|
9527
|
-
|
|
9708
|
+
sendJSON12(res, 413, { error: msg });
|
|
9528
9709
|
return;
|
|
9529
9710
|
}
|
|
9530
9711
|
let json;
|
|
9531
9712
|
try {
|
|
9532
9713
|
json = JSON.parse(raw);
|
|
9533
9714
|
} catch {
|
|
9534
|
-
|
|
9715
|
+
sendJSON12(res, 400, { error: "Invalid JSON body" });
|
|
9535
9716
|
return;
|
|
9536
9717
|
}
|
|
9537
9718
|
const parsed = CreateBodySchema.safeParse(json);
|
|
9538
9719
|
if (!parsed.success) {
|
|
9539
|
-
|
|
9720
|
+
sendJSON12(res, 422, { error: "Invalid body", issues: parsed.error.issues });
|
|
9540
9721
|
return;
|
|
9541
9722
|
}
|
|
9542
9723
|
try {
|
|
@@ -9549,37 +9730,37 @@ async function handlePost(req, res, store) {
|
|
|
9549
9730
|
if (parsed.data.expiresAt !== void 0) input.expiresAt = parsed.data.expiresAt;
|
|
9550
9731
|
const result = await store.create(input);
|
|
9551
9732
|
const publicRecord = AuthTokenPublicSchema.parse(result.record);
|
|
9552
|
-
|
|
9733
|
+
sendJSON12(res, 200, {
|
|
9553
9734
|
...publicRecord,
|
|
9554
9735
|
token: result.token
|
|
9555
9736
|
});
|
|
9556
9737
|
} catch (err) {
|
|
9557
9738
|
const msg = err instanceof Error ? err.message : "Failed to create token";
|
|
9558
9739
|
if (msg.includes("already exists")) {
|
|
9559
|
-
|
|
9740
|
+
sendJSON12(res, 409, { error: msg });
|
|
9560
9741
|
return;
|
|
9561
9742
|
}
|
|
9562
|
-
|
|
9743
|
+
sendJSON12(res, 500, { error: "Internal error creating token" });
|
|
9563
9744
|
}
|
|
9564
9745
|
}
|
|
9565
9746
|
async function handleList3(res, store) {
|
|
9566
9747
|
try {
|
|
9567
9748
|
const list = await store.list();
|
|
9568
|
-
|
|
9749
|
+
sendJSON12(res, 200, list);
|
|
9569
9750
|
} catch {
|
|
9570
|
-
|
|
9751
|
+
sendJSON12(res, 500, { error: "Internal error listing tokens" });
|
|
9571
9752
|
}
|
|
9572
9753
|
}
|
|
9573
9754
|
async function handleDelete2(res, store, id) {
|
|
9574
9755
|
try {
|
|
9575
9756
|
const ok = await store.revoke(id);
|
|
9576
9757
|
if (!ok) {
|
|
9577
|
-
|
|
9758
|
+
sendJSON12(res, 404, { error: "Token not found" });
|
|
9578
9759
|
return;
|
|
9579
9760
|
}
|
|
9580
|
-
|
|
9761
|
+
sendJSON12(res, 200, { deleted: true });
|
|
9581
9762
|
} catch {
|
|
9582
|
-
|
|
9763
|
+
sendJSON12(res, 500, { error: "Internal error revoking token" });
|
|
9583
9764
|
}
|
|
9584
9765
|
}
|
|
9585
9766
|
var DELETE_PATH_RE2 = /^\/api\/v1\/auth\/tokens\/([^/?]+)(?:\?.*)?$/;
|
|
@@ -9604,12 +9785,12 @@ function handleAuthRoute(req, res, store) {
|
|
|
9604
9785
|
return true;
|
|
9605
9786
|
}
|
|
9606
9787
|
}
|
|
9607
|
-
|
|
9788
|
+
sendJSON12(res, 405, { error: "Method not allowed" });
|
|
9608
9789
|
return true;
|
|
9609
9790
|
}
|
|
9610
9791
|
|
|
9611
9792
|
// src/server/routes/local-model.ts
|
|
9612
|
-
function
|
|
9793
|
+
function sendJSON13(res, status, body) {
|
|
9613
9794
|
res.writeHead(status, { "Content-Type": "application/json" });
|
|
9614
9795
|
res.end(JSON.stringify(body));
|
|
9615
9796
|
}
|
|
@@ -9617,30 +9798,30 @@ function handleLocalModelRoute(req, res, getStatus) {
|
|
|
9617
9798
|
const { method, url } = req;
|
|
9618
9799
|
if (url !== "/api/v1/local-model/status") return false;
|
|
9619
9800
|
if (method !== "GET") {
|
|
9620
|
-
|
|
9801
|
+
sendJSON13(res, 405, { error: "Method not allowed" });
|
|
9621
9802
|
return true;
|
|
9622
9803
|
}
|
|
9623
9804
|
if (!getStatus) {
|
|
9624
|
-
|
|
9805
|
+
sendJSON13(res, 503, { error: "Local backend not configured" });
|
|
9625
9806
|
return true;
|
|
9626
9807
|
}
|
|
9627
9808
|
const status = getStatus();
|
|
9628
9809
|
if (!status) {
|
|
9629
|
-
|
|
9810
|
+
sendJSON13(res, 503, { error: "Local backend not configured" });
|
|
9630
9811
|
return true;
|
|
9631
9812
|
}
|
|
9632
|
-
|
|
9813
|
+
sendJSON13(res, 200, status);
|
|
9633
9814
|
return true;
|
|
9634
9815
|
}
|
|
9635
9816
|
function handleLocalModelsRoute(req, res, getStatuses) {
|
|
9636
9817
|
const { method, url } = req;
|
|
9637
9818
|
if (url !== "/api/v1/local-models/status") return false;
|
|
9638
9819
|
if (method !== "GET") {
|
|
9639
|
-
|
|
9820
|
+
sendJSON13(res, 405, { error: "Method not allowed" });
|
|
9640
9821
|
return true;
|
|
9641
9822
|
}
|
|
9642
9823
|
const statuses = getStatuses ? getStatuses() : [];
|
|
9643
|
-
|
|
9824
|
+
sendJSON13(res, 200, statuses);
|
|
9644
9825
|
return true;
|
|
9645
9826
|
}
|
|
9646
9827
|
|
|
@@ -10016,6 +10197,21 @@ var V1_BRIDGE_ROUTES = [
|
|
|
10016
10197
|
scope: "manage-proposals",
|
|
10017
10198
|
description: "Force a background-scheduler refresh tick and return emitted proposals (O4)."
|
|
10018
10199
|
},
|
|
10200
|
+
// ── LMLM dashboard pool mutation — operator-initiated install/remove ──
|
|
10201
|
+
// Modeled as auto-approved model proposals, so the same `manage-proposals`
|
|
10202
|
+
// write scope that governs approve/reject governs these too.
|
|
10203
|
+
{
|
|
10204
|
+
method: "POST",
|
|
10205
|
+
pattern: /^\/api\/v1\/local-models\/pool\/install(?:\?.*)?$/,
|
|
10206
|
+
scope: "manage-proposals",
|
|
10207
|
+
description: "Install a recommended model into the local pool (auto-approved proposal)."
|
|
10208
|
+
},
|
|
10209
|
+
{
|
|
10210
|
+
method: "POST",
|
|
10211
|
+
pattern: /^\/api\/v1\/local-models\/pool\/remove(?:\?.*)?$/,
|
|
10212
|
+
scope: "manage-proposals",
|
|
10213
|
+
description: "Remove a model from the local pool (auto-approved proposal)."
|
|
10214
|
+
},
|
|
10019
10215
|
// ── LMLM Phase 7 — read surface (hardware/pool/recommendations/proposals) ──
|
|
10020
10216
|
// Load-bearing: `local-models` is in `V1_WRAPPABLE`, so without these entries
|
|
10021
10217
|
// the `/api/v1` rewrite shim would rewrite `/api/v1/local-models/<name>` →
|
|
@@ -10450,6 +10646,17 @@ var OrchestratorServer = class {
|
|
|
10450
10646
|
...this.isModelInUseFn ? { isModelInUse: this.isModelInUseFn } : {}
|
|
10451
10647
|
});
|
|
10452
10648
|
},
|
|
10649
|
+
// LMLM dashboard pool mutation — POST /pool/install + /pool/remove.
|
|
10650
|
+
// Operator-initiated install/remove via auto-approved proposals; reuses
|
|
10651
|
+
// the same mutation pool + bus + in-use probe the proposals route uses.
|
|
10652
|
+
// Registered before the read surface so /pool/install|remove match first.
|
|
10653
|
+
(req, res) => handleV1LocalModelsMutationRoute(req, res, {
|
|
10654
|
+
projectPath: this.projectPath,
|
|
10655
|
+
bus: this.orchestrator,
|
|
10656
|
+
getModelPool: () => this.getModelPoolFn?.() ?? null,
|
|
10657
|
+
...this.getRecommendationsFn ? { getRecommendations: this.getRecommendationsFn } : {},
|
|
10658
|
+
...this.isModelInUseFn ? { isModelInUse: this.isModelInUseFn } : {}
|
|
10659
|
+
}),
|
|
10453
10660
|
// LMLM Phase 6/7 — POST /refresh + the GET read surface
|
|
10454
10661
|
// (hardware/pool/recommendations/proposals). Registered before the
|
|
10455
10662
|
// chat-proxy fallback so it owns the path. Each accessor is spread in
|
|
@@ -15061,6 +15268,24 @@ ${messages}`);
|
|
|
15061
15268
|
});
|
|
15062
15269
|
this.modelPool = new PoolManager({ store, installer: this.modelInstaller, onWarn });
|
|
15063
15270
|
}
|
|
15271
|
+
/**
|
|
15272
|
+
* LMLM Phase 7 wiring: apply the operator's configured pool bounds (disk
|
|
15273
|
+
* budget + org/family allowlist) from `localModels.pool` to the live pool.
|
|
15274
|
+
* Called after `PoolStateStore.load()` so config wins over persisted bounds
|
|
15275
|
+
* (D2, declarative precedence). No-op when the pool is null (LMLM disabled)
|
|
15276
|
+
* or no `pool` block is configured, so it is safe to call unconditionally
|
|
15277
|
+
* on the startup path.
|
|
15278
|
+
*/
|
|
15279
|
+
async applyConfiguredPoolBounds() {
|
|
15280
|
+
const pool = this.modelPool;
|
|
15281
|
+
const bounds = this.config.localModels?.pool;
|
|
15282
|
+
if (pool === null || !bounds) return;
|
|
15283
|
+
await pool.configurePool({
|
|
15284
|
+
diskBudgetGb: bounds.diskBudgetGb,
|
|
15285
|
+
allowedOrgs: bounds.allowedOrgs,
|
|
15286
|
+
allowedFamilies: bounds.allowedFamilies
|
|
15287
|
+
});
|
|
15288
|
+
}
|
|
15064
15289
|
/**
|
|
15065
15290
|
* LMLM Phase 6: arm the single background refresh scheduler over the live
|
|
15066
15291
|
* pool. No-op when LMLM is disabled (`modelPool` null). Each tick runs
|
|
@@ -15077,7 +15302,18 @@ ${messages}`);
|
|
|
15077
15302
|
if (this.modelPool === null) return;
|
|
15078
15303
|
const pool = this.modelPool;
|
|
15079
15304
|
const refreshCfg = this.config.localModels?.refresh;
|
|
15080
|
-
const
|
|
15305
|
+
const frozen = loadFrozenCandidates();
|
|
15306
|
+
const candidates = selectCandidates2(frozen.candidates, this.config.localModels?.pool);
|
|
15307
|
+
for (const warning of frozen.warnings) {
|
|
15308
|
+
this.logger.warn("LMLM frozen candidate snapshot degraded", { warning });
|
|
15309
|
+
}
|
|
15310
|
+
if (candidates.length === 0) {
|
|
15311
|
+
this.logger.warn("LMLM recommender has no candidates", {
|
|
15312
|
+
frozenCount: frozen.candidates.length,
|
|
15313
|
+
allowedOrgs: this.config.localModels?.pool?.allowedOrgs ?? []
|
|
15314
|
+
});
|
|
15315
|
+
}
|
|
15316
|
+
const recommend = createNativeRecommender({ candidates });
|
|
15081
15317
|
this.modelRecommender = recommend;
|
|
15082
15318
|
this.refreshScheduler = new RefreshScheduler({
|
|
15083
15319
|
runTick: () => runRefreshTick({
|
|
@@ -15088,7 +15324,7 @@ ${messages}`);
|
|
|
15088
15324
|
// Phase 7: after persisting the proposal, emit `local-models:proposal`
|
|
15089
15325
|
// (== MODEL_PROPOSAL_TOPIC) on the bus so it fans out to WS clients and
|
|
15090
15326
|
// notification sinks. Literal to avoid a proposals/model-handlers cycle.
|
|
15091
|
-
emitProposal: (c) =>
|
|
15327
|
+
emitProposal: (c) => createModelProposal2(this.projectRoot, c).then((record) => {
|
|
15092
15328
|
this.emit("local-models:proposal", {
|
|
15093
15329
|
id: record.id,
|
|
15094
15330
|
status: "created",
|
|
@@ -15154,6 +15390,7 @@ ${messages}`);
|
|
|
15154
15390
|
}
|
|
15155
15391
|
if (this.poolStateStore !== null) {
|
|
15156
15392
|
await this.poolStateStore.load();
|
|
15393
|
+
await this.applyConfiguredPoolBounds();
|
|
15157
15394
|
}
|
|
15158
15395
|
for (const resolver of this.localResolvers.values()) {
|
|
15159
15396
|
await resolver.start();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@harness-engineering/orchestrator",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.2",
|
|
4
4
|
"description": "Orchestrator daemon for dispatching coding agents to issues",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"module": "./dist/index.mjs",
|
|
@@ -37,22 +37,22 @@
|
|
|
37
37
|
"homepage": "https://github.com/Intense-Visions/harness-engineering/tree/main/packages/orchestrator#readme",
|
|
38
38
|
"dependencies": {
|
|
39
39
|
"@anthropic-ai/sdk": "^0.95.1",
|
|
40
|
-
"@earendil-works/pi-coding-agent": "^0.
|
|
40
|
+
"@earendil-works/pi-coding-agent": "^0.79.0",
|
|
41
41
|
"@google/genai": "^2.0.4",
|
|
42
42
|
"bcryptjs": "^2.4.3",
|
|
43
43
|
"better-sqlite3": "^12.10.0",
|
|
44
44
|
"ink": "^4.4.1",
|
|
45
|
-
"liquidjs": "^10.
|
|
45
|
+
"liquidjs": "^10.26.0",
|
|
46
46
|
"openai": "^6.0.0",
|
|
47
47
|
"react": "^18.3.1",
|
|
48
48
|
"ws": "^8.21.0",
|
|
49
49
|
"yaml": "^2.8.3",
|
|
50
50
|
"zod": "^3.25.76",
|
|
51
|
-
"@harness-engineering/core": "0.34.
|
|
52
|
-
"@harness-engineering/graph": "0.11.
|
|
53
|
-
"@harness-engineering/
|
|
54
|
-
"@harness-engineering/
|
|
55
|
-
"@harness-engineering/types": "0.
|
|
51
|
+
"@harness-engineering/core": "0.34.1",
|
|
52
|
+
"@harness-engineering/graph": "0.11.5",
|
|
53
|
+
"@harness-engineering/local-models": "0.4.0",
|
|
54
|
+
"@harness-engineering/intelligence": "0.4.3",
|
|
55
|
+
"@harness-engineering/types": "0.19.0"
|
|
56
56
|
},
|
|
57
57
|
"devDependencies": {
|
|
58
58
|
"@asteasolutions/zod-to-openapi": "^7.3.0",
|