@indigoai-us/hq-cli 5.273.0 → 5.274.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +40 -0
- package/README.md +32 -1
- package/dist/command-catalog.generated.d.ts +72 -0
- package/dist/command-catalog.generated.js +92 -0
- package/dist/command-registration-plan.d.ts +12 -0
- package/dist/command-registration-plan.js +2 -0
- package/dist/commands/cloud.js +40 -0
- package/dist/commands/core-checkpoint.js +3 -1
- package/dist/commands/core-command-catalog.d.ts +32 -0
- package/dist/commands/core-command-catalog.js +58 -0
- package/dist/commands/core-ontology.js +9 -4
- package/dist/commands/core.d.ts +4 -0
- package/dist/commands/core.js +79 -75
- package/dist/commands/deploy-comments.d.ts +3 -0
- package/dist/commands/deploy-comments.js +159 -0
- package/dist/commands/lanes-move.d.ts +12 -0
- package/dist/commands/lanes-move.js +342 -0
- package/dist/commands/lanes-resume.js +3 -1
- package/dist/commands/lanes-shared.d.ts +36 -1
- package/dist/commands/lanes-shared.js +59 -0
- package/dist/commands/lanes.d.ts +1 -1
- package/dist/commands/lanes.js +204 -456
- package/dist/commands/report.js +2 -14
- package/dist/fast-core.js +1 -1
- package/dist/{commands/scaffold-fast.d.ts → fast-scaffold.d.ts} +1 -1
- package/dist/{commands/scaffold-fast.js → fast-scaffold.js} +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +2 -2
- package/dist/lib/checkpoint-ontology/apply.d.ts +8 -0
- package/dist/lib/checkpoint-ontology/apply.js +33 -0
- package/dist/lib/checkpoint-ontology/plan.d.ts +15 -0
- package/dist/lib/checkpoint-ontology/plan.js +15 -0
- package/dist/lib/deploy-api.d.ts +6 -0
- package/dist/lib/deploy-api.js +21 -0
- package/dist/lib/doctor/fix/apply.d.ts +17 -2
- package/dist/lib/doctor/fix/apply.js +111 -57
- package/dist/lib/doctor/fix/json-output.d.ts +9 -1
- package/dist/lib/doctor/fix/json-output.js +11 -0
- package/dist/lib/first-sync-next-step.d.ts +37 -0
- package/dist/lib/first-sync-next-step.js +189 -0
- package/dist/main.js +4 -1
- package/dist/utils/cli-telemetry.d.ts +7 -1
- package/dist/utils/cli-telemetry.js +84 -8
- package/dist/utils/contribution-table.d.ts +1 -1
- package/dist/utils/scaffold-fast-import-guard.d.ts +3 -0
- package/dist/utils/scaffold-fast-import-guard.js +28 -0
- package/dist/utils/scaffold-packlist-guard.d.ts +7 -0
- package/dist/utils/scaffold-packlist-guard.js +23 -0
- package/dist/utils/scaffold-sibling-references.d.ts +21 -0
- package/dist/utils/scaffold-sibling-references.js +156 -0
- package/package.json +1 -1
package/dist/commands/lanes.js
CHANGED
|
@@ -13,9 +13,12 @@ import * as path from "node:path";
|
|
|
13
13
|
import { Option } from "commander";
|
|
14
14
|
import { createLaneAccountsHandlers } from "./lanes-accounts.js";
|
|
15
15
|
import { createLaneMessageHandlers } from "./lanes-message.js";
|
|
16
|
+
import { createLaneMoveHandlers } from "./lanes-move.js";
|
|
16
17
|
import { createLaneQuestionsHandlers } from "./lanes-questions.js";
|
|
17
18
|
import { createLaneResumeHandlers } from "./lanes-resume.js";
|
|
18
19
|
import { createLaneWatchHandlers } from "./lanes-watch.js";
|
|
20
|
+
import { assertPlacementEnabled, defaultLanePlacementEnabled, parsePlacementPolicy, processLanePlacementCompany, readLanePlacementEnabled, } from "./lanes-shared.js";
|
|
21
|
+
export { LANE_OUTPOST_LOAD_BALANCING_FLAG } from "./lanes-shared.js";
|
|
19
22
|
export { resolveAnswerChoice } from "./lanes-questions.js";
|
|
20
23
|
import { registerOutpostPrunerCommand } from "./outpost-pruner.js";
|
|
21
24
|
import { registerLaneKpisCommand } from "./lanes-kpis.js";
|
|
@@ -27,13 +30,12 @@ import { ensureCognitoToken } from "../utils/cognito-session.js";
|
|
|
27
30
|
import { resolveLanesHqRoot } from "../lib/lanes/hq-root.js";
|
|
28
31
|
import { isSsmRunCommandProcess } from "../lib/lanes/ssm-command-context.js";
|
|
29
32
|
import { readLanesRegistryFlag } from "../lib/lanes/registry-flag.js";
|
|
30
|
-
import { resolveProcessFlagCompanyContext } from "../lib/flag-registry.js";
|
|
31
33
|
import { createOutpostHireGrantContext, parseOutpostHireGrantContext, sameHireGrantSenior, } from "../lib/lanes/hire-grant-dispatch.js";
|
|
32
34
|
import { createOutpostProfileRepinContext, parseOutpostProfileRepinContext, } from "../lib/lanes/outpost-profile-repin.js";
|
|
33
35
|
import { createOutpostRegistryWorkerApprovalContext, parseOutpostRegistryWorkerApprovalContext, } from "../lib/lanes/outpost-registry-worker-approval.js";
|
|
34
36
|
import { OUTPOST_DIR, OUTPOST_ID_PATH, readOutpostIdentity, resolveAdmissionHost, } from "../lib/lanes/admission-host.js";
|
|
35
37
|
export { OUTPOST_DIR, OUTPOST_ID_PATH, readOutpostIdentity } from "../lib/lanes/admission-host.js";
|
|
36
|
-
import { buildOutpostCapacityCommand, buildOutpostIdentityBackfillCommand, buildOutpostLaneCommand,
|
|
38
|
+
import { buildOutpostCapacityCommand, buildOutpostIdentityBackfillCommand, buildOutpostLaneCommand, buildOutpostQuestionListCommand, buildOutpostPickupCommand, createOutpostsClient, dispatchErrorMessage, dispatchOutpostPickup, buildOutpostLaneListCommand, outpostRejectedEffortOption, parseOutpostPickupPayload, stageOutpostPayload, } from "../lib/lanes/outpost-dispatch.js";
|
|
37
39
|
import { outpostHireSummaryCacheKey, readOutpostHireSummaryCache, writeOutpostHireSummaryCache, } from "../lib/lanes/outpost-hire-cache.js";
|
|
38
40
|
import { buildHostCapacitySnapshot, chooseHostForLane, parseHostCapacitySnapshot, } from "../lib/lanes/host-capacity.js";
|
|
39
41
|
import { BALANCE_MOVABLE_STATES, selectClaudeAccount, } from "../lib/lanes/claude-balance.js";
|
|
@@ -73,46 +75,6 @@ export { MAX_LANE_DEPTH };
|
|
|
73
75
|
export const LANES_RUNS_REL = path.join("workspace", "lanes-runs");
|
|
74
76
|
/** The lane → mesh seam. Defined in lib/lanes/mesh-events.ts; the watcher shares it. */
|
|
75
77
|
export { emitLaneDecisionEvents };
|
|
76
|
-
export const LANE_OUTPOST_LOAD_BALANCING_FLAG = "lanes.outpost-load-balancing";
|
|
77
|
-
function lanePlacementFlagErrorCode(error) {
|
|
78
|
-
if (error && typeof error === "object") {
|
|
79
|
-
for (const key of ["code", "error", "name"]) {
|
|
80
|
-
const value = error[key];
|
|
81
|
-
if (typeof value === "string" && value.trim())
|
|
82
|
-
return value;
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
return "UnknownError";
|
|
86
|
-
}
|
|
87
|
-
function reportLanePlacementFlagReadFailure(error) {
|
|
88
|
-
process.stderr.write(`${JSON.stringify({
|
|
89
|
-
event: "lanes.outpost_load_balancing.flag_read_failed",
|
|
90
|
-
flag: LANE_OUTPOST_LOAD_BALANCING_FLAG,
|
|
91
|
-
error: lanePlacementFlagErrorCode(error),
|
|
92
|
-
fallback: false,
|
|
93
|
-
})}\n`);
|
|
94
|
-
}
|
|
95
|
-
function defaultLanePlacementEnabled(company) {
|
|
96
|
-
return readLanesRegistryFlag(LANE_OUTPOST_LOAD_BALANCING_FLAG, {
|
|
97
|
-
company,
|
|
98
|
-
reportFailure: reportLanePlacementFlagReadFailure,
|
|
99
|
-
});
|
|
100
|
-
}
|
|
101
|
-
async function readLanePlacementEnabled(ctx, company) {
|
|
102
|
-
try {
|
|
103
|
-
return await ctx.lanePlacementEnabled(company);
|
|
104
|
-
}
|
|
105
|
-
catch (error) {
|
|
106
|
-
reportLanePlacementFlagReadFailure(error);
|
|
107
|
-
return false;
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
function processLanePlacementCompany(ctx) {
|
|
111
|
-
const context = resolveProcessFlagCompanyContext(ctx.env);
|
|
112
|
-
if (!context.companySlug)
|
|
113
|
-
return undefined;
|
|
114
|
-
return { slug: context.companySlug, uid: context.companyUid ?? "" };
|
|
115
|
-
}
|
|
116
78
|
function withoutProviderHome(value) {
|
|
117
79
|
if (Array.isArray(value))
|
|
118
80
|
return value.map(withoutProviderHome);
|
|
@@ -507,6 +469,12 @@ function mergeOwnerLaneHierarchy(supplied, ownerLanes) {
|
|
|
507
469
|
byId.set(lane.lane_id, lane);
|
|
508
470
|
return [...byId.values()];
|
|
509
471
|
}
|
|
472
|
+
function mergeOwnerLaneRecords(supplied, ownerLanes) {
|
|
473
|
+
const byId = new Map(supplied.map((lane) => [lane.lane_id, lane]));
|
|
474
|
+
for (const lane of ownerLanes)
|
|
475
|
+
byId.set(lane.lane_id, lane);
|
|
476
|
+
return [...byId.values()];
|
|
477
|
+
}
|
|
510
478
|
function outpostCliOverride(ctx, outpostId) {
|
|
511
479
|
const envKey = `HQ_LANES_OUTPOST_CLI_${outpostId.toUpperCase().replace(/[^A-Z0-9_]/g, "_")}`;
|
|
512
480
|
const configured = ctx.env[envKey];
|
|
@@ -857,8 +825,7 @@ async function mapWithConcurrency(values, concurrency, map) {
|
|
|
857
825
|
async function withResumeHireAdmission(ctx, lane, fn, grantContext) {
|
|
858
826
|
const hqRoot = ctx.hqRoot();
|
|
859
827
|
if (grantContext !== undefined && (!ctx.isSsmOwnerDispatch() ||
|
|
860
|
-
lane
|
|
861
|
-
!laneIsHostedHere(ctx, lane) ||
|
|
828
|
+
!laneGrantTargetHostedHere(ctx, lane) ||
|
|
862
829
|
lane.senior.kind !== "lane" ||
|
|
863
830
|
!sameHireGrantSenior(lane.senior, grantContext.expected_senior))) {
|
|
864
831
|
throw new LanesError("hire_grant_senior_changed", `Lane ${lane.lane_id}'s senior changed after the primary authorized its resume`, { lane_id: lane.lane_id, expected_senior: grantContext.expected_senior, current_senior: lane.senior });
|
|
@@ -1118,9 +1085,27 @@ async function resolveCommandLaneTarget(ctx, laneId, options = {}) {
|
|
|
1118
1085
|
new Set(outpostIds).size !== outpostIds.length) {
|
|
1119
1086
|
throw new LanesError("outpost_list_invalid", "Registered Outpost list is malformed or exceeds the lane lookup limit", { lane_id: laneId, count: outpostIds.length, maximum: MAX_PLACEMENT_OUTPOSTS });
|
|
1120
1087
|
}
|
|
1088
|
+
const requestedOutpostId = options.outpostId?.trim();
|
|
1089
|
+
if (options.outpostId !== undefined && !requestedOutpostId) {
|
|
1090
|
+
throw new LanesError("outpost_id_invalid", "--outpost must be a registered Outpost id", { lane_id: laneId });
|
|
1091
|
+
}
|
|
1092
|
+
if (requestedOutpostId !== undefined && !outpostIds.includes(requestedOutpostId)) {
|
|
1093
|
+
throw new LanesError("outpost_not_registered", `Outpost ${requestedOutpostId} is not registered`, { lane_id: laneId, outpost_id: requestedOutpostId });
|
|
1094
|
+
}
|
|
1121
1095
|
const matches = [];
|
|
1122
|
-
|
|
1123
|
-
|
|
1096
|
+
const failedOutpostIds = [];
|
|
1097
|
+
const lookupOutpostIds = requestedOutpostId === undefined ? outpostIds : [requestedOutpostId];
|
|
1098
|
+
for (const outpostId of lookupOutpostIds) {
|
|
1099
|
+
let ownerLanes;
|
|
1100
|
+
try {
|
|
1101
|
+
ownerLanes = await readOutpostLaneRecords(ctx, outpostId);
|
|
1102
|
+
}
|
|
1103
|
+
catch (err) {
|
|
1104
|
+
if (requestedOutpostId !== undefined)
|
|
1105
|
+
throw err;
|
|
1106
|
+
failedOutpostIds.push(outpostId);
|
|
1107
|
+
continue;
|
|
1108
|
+
}
|
|
1124
1109
|
for (const lane of ownerLanes) {
|
|
1125
1110
|
if (lane.lane_id === laneId &&
|
|
1126
1111
|
lane.host.kind === "local" &&
|
|
@@ -1130,7 +1115,7 @@ async function resolveCommandLaneTarget(ctx, laneId, options = {}) {
|
|
|
1130
1115
|
}
|
|
1131
1116
|
}
|
|
1132
1117
|
if (matches.length === 0) {
|
|
1133
|
-
throw commandLaneNotFound(laneId, options.outpostHint === true);
|
|
1118
|
+
throw commandLaneNotFound(laneId, options.outpostHint === true, failedOutpostIds.length > 0 ? { failed_outpost_ids: failedOutpostIds } : undefined);
|
|
1134
1119
|
}
|
|
1135
1120
|
if (matches.length !== 1) {
|
|
1136
1121
|
throw new LanesError("lane_outpost_ambiguous", `More than one registered Outpost reported lane ${laneId} with the same recorded host identity`, { lane_id: laneId, outpost_ids: matches.map((match) => match.outpostId) });
|
|
@@ -1143,11 +1128,11 @@ async function resolveCommandLaneTarget(ctx, laneId, options = {}) {
|
|
|
1143
1128
|
ownerHost: match.lane.host,
|
|
1144
1129
|
};
|
|
1145
1130
|
}
|
|
1146
|
-
function commandLaneNotFound(laneId, outpostHint) {
|
|
1131
|
+
function commandLaneNotFound(laneId, outpostHint, details = {}) {
|
|
1147
1132
|
const hint = outpostHint
|
|
1148
1133
|
? "; pass --outpost <id> to confirm a registered Outpost"
|
|
1149
1134
|
: "";
|
|
1150
|
-
return new LanesError("lane_not_found", `Lane ${laneId} was not found locally or on a registered Outpost with a matching host suffix${hint}`, { lane_id: laneId });
|
|
1135
|
+
return new LanesError("lane_not_found", `Lane ${laneId} was not found locally or on a registered Outpost with a matching host suffix${hint}`, { lane_id: laneId, ...details });
|
|
1151
1136
|
}
|
|
1152
1137
|
async function loadClaudeBalanceHostSnapshots(ctx, hqRoot, config) {
|
|
1153
1138
|
const localHost = laneHostFromCtx(ctx);
|
|
@@ -1274,29 +1259,18 @@ async function loadClaudeBalanceHostSnapshots(ctx, hqRoot, config) {
|
|
|
1274
1259
|
}
|
|
1275
1260
|
return { snapshots, unavailableHosts };
|
|
1276
1261
|
}
|
|
1277
|
-
function parsePlacementPolicy(raw) {
|
|
1278
|
-
const value = raw.trim();
|
|
1279
|
-
if (value === "local")
|
|
1280
|
-
return { kind: "local" };
|
|
1281
|
-
if (value === "auto")
|
|
1282
|
-
return { kind: "auto" };
|
|
1283
|
-
const match = /^outpost:(.+)$/.exec(value);
|
|
1284
|
-
const id = match?.[1]?.trim();
|
|
1285
|
-
if (id)
|
|
1286
|
-
return { kind: "outpost", id };
|
|
1287
|
-
throw new LanesError("lane_placement_invalid", `Invalid placement ${JSON.stringify(raw)}. Accepted: local, auto, outpost:<id>`, { value: raw, accepted: ["local", "auto", "outpost:<id>"] });
|
|
1288
|
-
}
|
|
1289
|
-
async function assertPlacementEnabled(ctx, company, readPlacementEnabled = () => readLanePlacementEnabled(ctx, company)) {
|
|
1290
|
-
if (await readPlacementEnabled())
|
|
1291
|
-
return;
|
|
1292
|
-
throw new LanesError("lane_placement_disabled", `Lane placement options require hq-flags gate ${LANE_OUTPOST_LOAD_BALANCING_FLAG}`, { flag: LANE_OUTPOST_LOAD_BALANCING_FLAG });
|
|
1293
|
-
}
|
|
1294
1262
|
function loadAllQuestions(hqRoot) {
|
|
1295
1263
|
return listQuestionIds(hqRoot).map((id) => readQuestion(hqRoot, id));
|
|
1296
1264
|
}
|
|
1297
1265
|
function sessionMetaPath(hqRoot, sessionId) {
|
|
1298
1266
|
return path.join(hqRoot, "workspace", "sessions", sessionId, "meta.yaml");
|
|
1299
1267
|
}
|
|
1268
|
+
function isSessionMetaDocument(value) {
|
|
1269
|
+
if (value === null || typeof value !== "object" || Array.isArray(value))
|
|
1270
|
+
return false;
|
|
1271
|
+
const prototype = Object.getPrototypeOf(value);
|
|
1272
|
+
return prototype === Object.prototype || prototype === null;
|
|
1273
|
+
}
|
|
1300
1274
|
function loadSessionMeta(hqRoot, sessionId, ctx) {
|
|
1301
1275
|
const filePath = sessionMetaPath(hqRoot, sessionId);
|
|
1302
1276
|
if (!ctx.existsSync(filePath))
|
|
@@ -1317,7 +1291,7 @@ function loadSessionMeta(hqRoot, sessionId, ctx) {
|
|
|
1317
1291
|
process.stderr.write(`hq lanes: failed to parse session meta ${filePath}: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
1318
1292
|
return { status: "malformed" };
|
|
1319
1293
|
}
|
|
1320
|
-
if (
|
|
1294
|
+
if (!isSessionMetaDocument(doc)) {
|
|
1321
1295
|
return { status: "malformed" };
|
|
1322
1296
|
}
|
|
1323
1297
|
return { status: "ok", doc: doc };
|
|
@@ -1328,11 +1302,21 @@ function readSessionMeta(hqRoot, sessionId, ctx) {
|
|
|
1328
1302
|
return null;
|
|
1329
1303
|
return loaded.doc;
|
|
1330
1304
|
}
|
|
1305
|
+
/**
|
|
1306
|
+
* Apply the documented session senior default only when the key was never
|
|
1307
|
+
* written. `core/scripts/hq-session.sh` section 3.1 defaults new sessions to
|
|
1308
|
+
* `user` and says authority walkers apply that default at the walk site.
|
|
1309
|
+
*/
|
|
1310
|
+
function sessionSeniorValue(meta) {
|
|
1311
|
+
return Object.prototype.hasOwnProperty.call(meta, "senior")
|
|
1312
|
+
? meta.senior
|
|
1313
|
+
: "user";
|
|
1314
|
+
}
|
|
1331
1315
|
function seniorOfSession(hqRoot, sessionId, ctx) {
|
|
1332
1316
|
const meta = readSessionMeta(hqRoot, sessionId, ctx);
|
|
1333
1317
|
if (!meta)
|
|
1334
1318
|
return { kind: "user" };
|
|
1335
|
-
const senior = meta
|
|
1319
|
+
const senior = sessionSeniorValue(meta);
|
|
1336
1320
|
if (typeof senior === "string" && senior.trim()) {
|
|
1337
1321
|
return parseSeniorRef(senior);
|
|
1338
1322
|
}
|
|
@@ -2319,13 +2303,16 @@ async function runCreate(ctx, opts) {
|
|
|
2319
2303
|
const company = companyFlag
|
|
2320
2304
|
? await ctx.resolveCompany(companyFlag)
|
|
2321
2305
|
: null;
|
|
2306
|
+
const callerAuthorization = requestedHireGrant !== undefined || ctx.env.HQ_LANE_ID?.trim()
|
|
2307
|
+
? callerSeniorAuthorization(ctx, senior)
|
|
2308
|
+
: { authorized: true };
|
|
2322
2309
|
if (requestedHireGrant !== undefined &&
|
|
2323
|
-
!
|
|
2324
|
-
throw new LanesError("hire_grant_unauthorized", `Only senior ${seniorKey(senior)} may set a lane's hire grant`, { senior, hire_grant: requestedHireGrant });
|
|
2310
|
+
!callerAuthorization.authorized) {
|
|
2311
|
+
throw new LanesError("hire_grant_unauthorized", appendCallerSeniorReason(`Only senior ${seniorKey(senior)} may set a lane's hire grant`, callerAuthorization), { senior, hire_grant: requestedHireGrant, reason: callerAuthorization.reason });
|
|
2325
2312
|
}
|
|
2326
2313
|
if (ctx.env.HQ_LANE_ID?.trim() &&
|
|
2327
|
-
!
|
|
2328
|
-
throw new LanesError("hire_grant_unauthorized", `Only senior lane ${senior.id} may create a direct child lane`, { senior_lane_id: senior.id });
|
|
2314
|
+
!callerAuthorization.authorized) {
|
|
2315
|
+
throw new LanesError("hire_grant_unauthorized", appendCallerSeniorReason(`Only senior lane ${senior.id} may create a direct child lane`, callerAuthorization), { senior_lane_id: senior.id, reason: callerAuthorization.reason });
|
|
2329
2316
|
}
|
|
2330
2317
|
const hireGrant = requestedHireGrant ?? 0;
|
|
2331
2318
|
const laneDepth = derivedLaneDepth(hqRoot, senior, ctx);
|
|
@@ -3349,7 +3336,7 @@ function authorizeFailedResume(ctx, lane, encodedContext) {
|
|
|
3349
3336
|
throw new LanesError("failed_resume_authorization_invalid", "Failed-resume authorization context is only valid on an Outpost owner dispatch", { lane_id: lane.lane_id });
|
|
3350
3337
|
}
|
|
3351
3338
|
const context = parseOutpostFailedResumeContext(encodedContext, lane.lane_id);
|
|
3352
|
-
if (lane
|
|
3339
|
+
if (!laneGrantTargetHostedHere(ctx, lane) ||
|
|
3353
3340
|
!sameHireGrantSenior(lane.senior, context.expectedSenior)) {
|
|
3354
3341
|
throw new LanesError("failed_resume_senior_changed", `Lane ${lane.lane_id}'s senior changed after its failed-resume request was authorized`, {
|
|
3355
3342
|
lane_id: lane.lane_id,
|
|
@@ -3359,18 +3346,25 @@ function authorizeFailedResume(ctx, lane, encodedContext) {
|
|
|
3359
3346
|
}
|
|
3360
3347
|
return context.actor;
|
|
3361
3348
|
}
|
|
3362
|
-
|
|
3363
|
-
|
|
3349
|
+
const callerAuthorization = callerSeniorAuthorization(ctx, lane.senior);
|
|
3350
|
+
if (!callerAuthorization.authorized) {
|
|
3351
|
+
throw new LanesError("failed_resume_unauthorized", appendCallerSeniorReason("Only the lane's senior may resume a failed lane with --from-failed", callerAuthorization), { lane_id: lane.lane_id, reason: callerAuthorization.reason });
|
|
3364
3352
|
}
|
|
3365
3353
|
return resumeProfileRepinActor(ctx);
|
|
3366
3354
|
}
|
|
3367
|
-
function workerProfileHashMismatch(lane, profile) {
|
|
3368
|
-
|
|
3355
|
+
function workerProfileHashMismatch(lane, profile, callerAuthorization) {
|
|
3356
|
+
const message = `Worker profile ${JSON.stringify(profile.id)} on Outpost ${lane.host.id} does not match the hash recorded by the lane`;
|
|
3357
|
+
return new LanesError("worker_profile_hash_mismatch", callerAuthorization && !callerAuthorization.authorized
|
|
3358
|
+
? appendCallerSeniorReason(message, callerAuthorization)
|
|
3359
|
+
: message, {
|
|
3369
3360
|
lane_id: lane.lane_id,
|
|
3370
3361
|
worker: profile.id,
|
|
3371
3362
|
outpost: lane.host.kind === "outpost" ? lane.host.id : null,
|
|
3372
3363
|
expected_hash: lane.worker_profile_hash ?? null,
|
|
3373
3364
|
actual_hash: profile.profile.contentHash,
|
|
3365
|
+
...(callerAuthorization && !callerAuthorization.authorized
|
|
3366
|
+
? { reason: callerAuthorization.reason }
|
|
3367
|
+
: {}),
|
|
3374
3368
|
});
|
|
3375
3369
|
}
|
|
3376
3370
|
function authorizeResumeProfileRepin(ctx, lane, profile, encodedContext) {
|
|
@@ -3390,7 +3384,7 @@ function authorizeResumeProfileRepin(ctx, lane, profile, encodedContext) {
|
|
|
3390
3384
|
? new LanesError("worker_profile_hash_mismatch", "Profile re-pin context is invalid", { lane_id: lane.lane_id })
|
|
3391
3385
|
: workerProfileHashMismatch(lane, profile);
|
|
3392
3386
|
}
|
|
3393
|
-
if (lane
|
|
3387
|
+
if (!laneGrantTargetHostedHere(ctx, lane) ||
|
|
3394
3388
|
!sameHireGrantSenior(lane.senior, context.expected_senior)) {
|
|
3395
3389
|
throw profile === undefined
|
|
3396
3390
|
? new LanesError("worker_profile_hash_mismatch", "Lane senior changed after profile re-pin authorization", { lane_id: lane.lane_id })
|
|
@@ -3398,7 +3392,7 @@ function authorizeResumeProfileRepin(ctx, lane, profile, encodedContext) {
|
|
|
3398
3392
|
}
|
|
3399
3393
|
}
|
|
3400
3394
|
if (profile === undefined ||
|
|
3401
|
-
lane.host.kind !== "outpost" ||
|
|
3395
|
+
(lane.host.kind !== "outpost" && !(ssmOwnerDispatch && laneGrantTargetHostedHere(ctx, lane))) ||
|
|
3402
3396
|
lane.worker_profile_hash === profile.profile.contentHash) {
|
|
3403
3397
|
return undefined;
|
|
3404
3398
|
}
|
|
@@ -3413,8 +3407,9 @@ function authorizeResumeProfileRepin(ctx, lane, profile, encodedContext) {
|
|
|
3413
3407
|
newHash: profile.profile.contentHash,
|
|
3414
3408
|
};
|
|
3415
3409
|
}
|
|
3416
|
-
|
|
3417
|
-
|
|
3410
|
+
const callerAuthorization = callerSeniorAuthorization(ctx, lane.senior);
|
|
3411
|
+
if (context !== undefined || !callerAuthorization.authorized) {
|
|
3412
|
+
throw workerProfileHashMismatch(lane, profile, context === undefined ? callerAuthorization : undefined);
|
|
3418
3413
|
}
|
|
3419
3414
|
return {
|
|
3420
3415
|
kind: "senior",
|
|
@@ -3425,18 +3420,26 @@ function authorizeResumeProfileRepin(ctx, lane, profile, encodedContext) {
|
|
|
3425
3420
|
};
|
|
3426
3421
|
}
|
|
3427
3422
|
function assertResumeProfileRepinStillAuthorized(ctx, current, authorization) {
|
|
3428
|
-
const
|
|
3429
|
-
|
|
3423
|
+
const callerAuthorization = authorization.kind === "senior"
|
|
3424
|
+
? callerSeniorAuthorization(ctx, current.senior)
|
|
3425
|
+
: undefined;
|
|
3426
|
+
const authorized = sameHireGrantSenior(current.senior, authorization.expectedSenior) &&
|
|
3430
3427
|
(authorization.kind === "ssm-owner-dispatch"
|
|
3431
|
-
? ctx.isSsmOwnerDispatch()
|
|
3432
|
-
:
|
|
3428
|
+
? ctx.isSsmOwnerDispatch() && laneGrantTargetHostedHere(ctx, current)
|
|
3429
|
+
: current.host.kind === "outpost" && callerAuthorization?.authorized === true);
|
|
3433
3430
|
if (!authorized) {
|
|
3434
|
-
|
|
3431
|
+
const message = `Lane ${current.lane_id}'s senior changed before its Outpost profile could be re-pinned`;
|
|
3432
|
+
throw new LanesError("worker_profile_hash_mismatch", callerAuthorization && !callerAuthorization.authorized
|
|
3433
|
+
? appendCallerSeniorReason(message, callerAuthorization)
|
|
3434
|
+
: message, {
|
|
3435
3435
|
lane_id: current.lane_id,
|
|
3436
3436
|
expected_senior: authorization.expectedSenior,
|
|
3437
3437
|
current_senior: current.senior,
|
|
3438
3438
|
expected_hash: current.worker_profile_hash ?? null,
|
|
3439
3439
|
actual_hash: authorization.newHash,
|
|
3440
|
+
...(callerAuthorization && !callerAuthorization.authorized
|
|
3441
|
+
? { reason: callerAuthorization.reason }
|
|
3442
|
+
: {}),
|
|
3440
3443
|
});
|
|
3441
3444
|
}
|
|
3442
3445
|
}
|
|
@@ -3479,19 +3482,14 @@ async function dispatchOutpostLaneCommand(ctx, lane, action, options, behavior =
|
|
|
3479
3482
|
let grantContext;
|
|
3480
3483
|
let grantHierarchy;
|
|
3481
3484
|
if (action === "grant") {
|
|
3482
|
-
|
|
3483
|
-
const ownerLanes = await readOutpostLaneRecords(ctx, outpostId);
|
|
3484
|
-
const byId = new Map(hierarchy.map((record) => [record.lane_id, record]));
|
|
3485
|
-
for (const ownerLane of ownerLanes) {
|
|
3486
|
-
byId.set(ownerLane.lane_id, ownerLane);
|
|
3487
|
-
}
|
|
3488
|
-
hierarchy = [...byId.values()];
|
|
3485
|
+
const hierarchy = mergeOwnerLaneRecords(loadAllLanes(ctx.hqRoot()), await readOutpostLaneRecords(ctx, outpostId));
|
|
3489
3486
|
grantHierarchy = hierarchy;
|
|
3490
3487
|
grantContext = createOutpostHireGrantContext(lane, hierarchy);
|
|
3491
3488
|
}
|
|
3492
3489
|
else if ((action === "resume" || action === "message") &&
|
|
3493
3490
|
lane.senior.kind === "lane") {
|
|
3494
|
-
|
|
3491
|
+
const hierarchy = mergeOwnerLaneRecords(loadAllLanes(ctx.hqRoot()), await readOutpostLaneRecords(ctx, outpostId));
|
|
3492
|
+
grantContext = createOutpostHireGrantContext(lane, hierarchy, action === "message" ? options.supersededMessageCallerId : undefined);
|
|
3495
3493
|
}
|
|
3496
3494
|
let dispatched;
|
|
3497
3495
|
let grantContextRejected = false;
|
|
@@ -4298,336 +4296,6 @@ async function runOutpostIdentityBackfillTarget(ctx, opts) {
|
|
|
4298
4296
|
marker_path: OUTPOST_ID_PATH,
|
|
4299
4297
|
});
|
|
4300
4298
|
}
|
|
4301
|
-
function laneCanMoveBeforeStart(lane) {
|
|
4302
|
-
return lane.state === "queued" &&
|
|
4303
|
-
lane.pickup_validation_pending !== true &&
|
|
4304
|
-
lane.admission_reserved_at === undefined &&
|
|
4305
|
-
lane.provider_session_id === undefined &&
|
|
4306
|
-
lane.provider_session_id_absent === undefined &&
|
|
4307
|
-
lane.provider_home_at_spawn === undefined &&
|
|
4308
|
-
lane.last_resume === undefined &&
|
|
4309
|
-
lane.last_run_usage === undefined &&
|
|
4310
|
-
lane.last_host_report_at === undefined &&
|
|
4311
|
-
lane.pending_resume === undefined;
|
|
4312
|
-
}
|
|
4313
|
-
function readLaneTimeout(hqRoot, laneId) {
|
|
4314
|
-
const raw = readJsonFile(timeoutPath(hqRoot, laneId));
|
|
4315
|
-
if (raw === null)
|
|
4316
|
-
return undefined;
|
|
4317
|
-
if (typeof raw !== "object" || Array.isArray(raw) ||
|
|
4318
|
-
typeof raw.timeout_secs !== "number" ||
|
|
4319
|
-
typeof raw.deadline_at !== "string" ||
|
|
4320
|
-
!Number.isSafeInteger(raw.timeout_secs) ||
|
|
4321
|
-
raw.timeout_secs <= 0 ||
|
|
4322
|
-
!Number.isFinite(Date.parse(raw.deadline_at))) {
|
|
4323
|
-
throw new LanesError("lane_timeout_invalid", `Lane ${laneId} has a malformed timeout record`, {
|
|
4324
|
-
lane_id: laneId,
|
|
4325
|
-
});
|
|
4326
|
-
}
|
|
4327
|
-
const record = raw;
|
|
4328
|
-
return { timeout_secs: record.timeout_secs, deadline_at: record.deadline_at };
|
|
4329
|
-
}
|
|
4330
|
-
async function runMove(ctx, laneId, opts) {
|
|
4331
|
-
const hqRoot = ctx.hqRoot();
|
|
4332
|
-
const policy = parsePlacementPolicy(opts.placement ?? "");
|
|
4333
|
-
const source = readLane(hqRoot, laneId);
|
|
4334
|
-
const placementCompany = resolveLaneCompany(hqRoot, source).company ?? undefined;
|
|
4335
|
-
let placementGate;
|
|
4336
|
-
const placementEnabled = () => {
|
|
4337
|
-
placementGate ??= readLanePlacementEnabled(ctx, placementCompany);
|
|
4338
|
-
return placementGate;
|
|
4339
|
-
};
|
|
4340
|
-
await assertPlacementEnabled(ctx, placementCompany, placementEnabled);
|
|
4341
|
-
if (!laneCanMoveBeforeStart(source)) {
|
|
4342
|
-
throw new LanesError(source.state === "queued" ? "lane_move_started" : "lane_not_queued", `Lane ${laneId} cannot move after it has started; current state is ${source.state}`, { lane_id: laneId, state: source.state });
|
|
4343
|
-
}
|
|
4344
|
-
const sourceHost = source.host;
|
|
4345
|
-
const config = loadLanesConfig(hqRoot);
|
|
4346
|
-
const capacity = await loadHostCapacitySnapshots(ctx, hqRoot, config);
|
|
4347
|
-
let targetSnapshots = capacity.snapshots.filter((snapshot) => snapshot.host.kind !== sourceHost.kind || snapshot.host.id !== sourceHost.id);
|
|
4348
|
-
if (policy.kind === "outpost") {
|
|
4349
|
-
targetSnapshots = targetSnapshots.filter((snapshot) => snapshot.host.kind === "outpost" && snapshot.host.id === policy.id);
|
|
4350
|
-
}
|
|
4351
|
-
else if (policy.kind === "local") {
|
|
4352
|
-
targetSnapshots = targetSnapshots.filter((snapshot) => snapshot.host.kind === "local");
|
|
4353
|
-
}
|
|
4354
|
-
const profileAccounts = source.worker_profile === undefined
|
|
4355
|
-
? undefined
|
|
4356
|
-
: loadWorkerProfile(hqRoot, source.worker_profile).meta.accounts;
|
|
4357
|
-
const allowedAccounts = source.admission_accounts ?? profileAccounts;
|
|
4358
|
-
const choice = chooseHostForLane(targetSnapshots, {
|
|
4359
|
-
provider: source.provider,
|
|
4360
|
-
...(source.account !== undefined ? { account: source.account } : {}),
|
|
4361
|
-
...(allowedAccounts !== undefined ? { allowedAccounts } : {}),
|
|
4362
|
-
});
|
|
4363
|
-
let moved;
|
|
4364
|
-
if (laneRunsHere(source, reconcileHostFromCtx(ctx))) {
|
|
4365
|
-
moved = await commitLaneMove(ctx, laneId, choice.host, choice.account, placementEnabled);
|
|
4366
|
-
}
|
|
4367
|
-
else if (source.host.kind === "outpost") {
|
|
4368
|
-
moved = await dispatchRemoteLaneMove(ctx, source, choice.host, choice.account);
|
|
4369
|
-
}
|
|
4370
|
-
else {
|
|
4371
|
-
throw new LanesError("lane_remote", `Cannot move lane ${laneId} from this host; its state is owned by ${source.host.kind}:${source.host.id}`, { lane_id: laneId, host: source.host, action: "move" });
|
|
4372
|
-
}
|
|
4373
|
-
printJson({
|
|
4374
|
-
ok: true,
|
|
4375
|
-
action: "move",
|
|
4376
|
-
lane_id: laneId,
|
|
4377
|
-
state: moved.state,
|
|
4378
|
-
host: moved.host,
|
|
4379
|
-
account: moved.account ?? null,
|
|
4380
|
-
lane: moved,
|
|
4381
|
-
});
|
|
4382
|
-
}
|
|
4383
|
-
async function commitLaneMove(ctx, laneId, target, account, readPlacementEnabled) {
|
|
4384
|
-
const hqRoot = ctx.hqRoot();
|
|
4385
|
-
const before = readLane(hqRoot, laneId);
|
|
4386
|
-
assertLaneRunsHereForOperatorMutation(ctx, before, "move-target");
|
|
4387
|
-
await assertPlacementEnabled(ctx, resolveLaneCompany(hqRoot, before).company ?? undefined, readPlacementEnabled);
|
|
4388
|
-
if (before.host.kind === target.kind && before.host.id === target.id) {
|
|
4389
|
-
throw new LanesError("lane_move_same_host", `Lane ${laneId} is already on ${target.kind}:${target.id}`, {
|
|
4390
|
-
lane_id: laneId,
|
|
4391
|
-
host: target,
|
|
4392
|
-
});
|
|
4393
|
-
}
|
|
4394
|
-
if (!laneCanMoveBeforeStart(before)) {
|
|
4395
|
-
throw new LanesError(before.state === "queued" ? "lane_move_started" : "lane_not_queued", `Lane ${laneId} cannot move after it has started; current state is ${before.state}`, { lane_id: laneId, state: before.state });
|
|
4396
|
-
}
|
|
4397
|
-
if (target.kind === "outpost") {
|
|
4398
|
-
let known;
|
|
4399
|
-
try {
|
|
4400
|
-
known = await ctx.listOutpostIds();
|
|
4401
|
-
}
|
|
4402
|
-
catch (err) {
|
|
4403
|
-
throw new LanesError("outpost_list_unavailable", `Failed to list Outpost ids: ${dispatchErrorMessage(err)}`, {});
|
|
4404
|
-
}
|
|
4405
|
-
assertKnownOutpost(target.id, known);
|
|
4406
|
-
}
|
|
4407
|
-
const accountConfig = loadLanesConfig(hqRoot).providers[before.provider].accounts[account];
|
|
4408
|
-
if (accountConfig === undefined || !accountConfig.enabled) {
|
|
4409
|
-
throw new LanesError("lane_move_account_invalid", `Account ${account} is unavailable for ${before.provider}`, {
|
|
4410
|
-
lane_id: laneId,
|
|
4411
|
-
provider: before.provider,
|
|
4412
|
-
account,
|
|
4413
|
-
});
|
|
4414
|
-
}
|
|
4415
|
-
const profileAccounts = before.worker_profile === undefined
|
|
4416
|
-
? undefined
|
|
4417
|
-
: loadWorkerProfile(hqRoot, before.worker_profile).meta.accounts;
|
|
4418
|
-
const allowedAccounts = before.admission_accounts ?? profileAccounts;
|
|
4419
|
-
if (allowedAccounts !== undefined &&
|
|
4420
|
-
!allowedAccounts.includes(account)) {
|
|
4421
|
-
throw new LanesError("lane_move_account_invalid", `Account ${account} is not allowed by lane ${laneId}`, {
|
|
4422
|
-
lane_id: laneId,
|
|
4423
|
-
account,
|
|
4424
|
-
allowed_accounts: allowedAccounts,
|
|
4425
|
-
});
|
|
4426
|
-
}
|
|
4427
|
-
const briefText = target.kind === "outpost"
|
|
4428
|
-
? (() => {
|
|
4429
|
-
const briefPath = path.join(runDir(hqRoot, laneId), "brief.md");
|
|
4430
|
-
if (!ctx.existsSync(briefPath)) {
|
|
4431
|
-
throw new LanesError("lane_move_brief_missing", `Lane ${laneId} has no stored brief to dispatch`, {
|
|
4432
|
-
lane_id: laneId,
|
|
4433
|
-
});
|
|
4434
|
-
}
|
|
4435
|
-
return ctx.readFileSync(briefPath, "utf8");
|
|
4436
|
-
})()
|
|
4437
|
-
: undefined;
|
|
4438
|
-
const timeout = target.kind === "outpost" ? readLaneTimeout(hqRoot, laneId) : undefined;
|
|
4439
|
-
const moved = await withLaneAdmissionLock(hqRoot, before.provider, async () => {
|
|
4440
|
-
const current = readLane(hqRoot, laneId);
|
|
4441
|
-
assertLaneRunsHereForOperatorMutation(ctx, current, "move-target");
|
|
4442
|
-
if (!laneCanMoveBeforeStart(current)) {
|
|
4443
|
-
throw new LanesError(current.state === "queued" ? "lane_move_started" : "lane_not_queued", `Lane ${laneId} started before the move could commit`, { lane_id: laneId, state: current.state });
|
|
4444
|
-
}
|
|
4445
|
-
return updateLane(hqRoot, laneId, current.version, "command", (live) => ({
|
|
4446
|
-
...live,
|
|
4447
|
-
host: target,
|
|
4448
|
-
account,
|
|
4449
|
-
queued_reason: undefined,
|
|
4450
|
-
admission_reserved_at: undefined,
|
|
4451
|
-
}));
|
|
4452
|
-
}, ctx.sleep);
|
|
4453
|
-
if (target.kind !== "outpost")
|
|
4454
|
-
return moved;
|
|
4455
|
-
try {
|
|
4456
|
-
return await dispatchCreatedOutpostLane(ctx, {
|
|
4457
|
-
hqRoot,
|
|
4458
|
-
lane: moved,
|
|
4459
|
-
outpostId: target.id,
|
|
4460
|
-
briefText: briefText ?? "",
|
|
4461
|
-
timeout,
|
|
4462
|
-
preserveQueuedOnFailure: true,
|
|
4463
|
-
});
|
|
4464
|
-
}
|
|
4465
|
-
catch (err) {
|
|
4466
|
-
const dispatchDetails = isLanesError(err) ? err.details : {};
|
|
4467
|
-
const commandId = typeof dispatchDetails.command_id === "string" &&
|
|
4468
|
-
dispatchDetails.command_id.trim() !== ""
|
|
4469
|
-
? dispatchDetails.command_id
|
|
4470
|
-
: undefined;
|
|
4471
|
-
const definitelyNotSubmitted = commandId === undefined &&
|
|
4472
|
-
dispatchDetails.outcome_unknown !== true &&
|
|
4473
|
-
dispatchDetails.done !== false;
|
|
4474
|
-
const knownNoPersistence = (isLanesError(err) && err.error === "outpost_self_target") ||
|
|
4475
|
-
(dispatchDetails.done === true && dispatchDetails.pickup_persisted === false);
|
|
4476
|
-
if (!definitelyNotSubmitted && !knownNoPersistence) {
|
|
4477
|
-
const checkCommand = `hq lanes show ${laneId}`;
|
|
4478
|
-
const message = `Failed to dispatch lane ${laneId} to Outpost ${target.id}; outcome is unknown and the lane remains assigned to Outpost ${target.id}. ` +
|
|
4479
|
-
`Check the target copy by running \`${checkCommand}\` on Outpost ${target.id}. ` +
|
|
4480
|
-
dispatchErrorMessage(err);
|
|
4481
|
-
throw new LanesError("lane_move_dispatch_failed", message, {
|
|
4482
|
-
lane_id: laneId,
|
|
4483
|
-
message,
|
|
4484
|
-
source_host: before.host,
|
|
4485
|
-
target_host: target,
|
|
4486
|
-
source_restored: false,
|
|
4487
|
-
outcome: "unknown",
|
|
4488
|
-
...(commandId !== undefined ? { command_id: commandId } : {}),
|
|
4489
|
-
check_command: checkCommand,
|
|
4490
|
-
...(dispatchDetails.owner_error !== undefined
|
|
4491
|
-
? { owner_error: dispatchDetails.owner_error }
|
|
4492
|
-
: {}),
|
|
4493
|
-
});
|
|
4494
|
-
}
|
|
4495
|
-
let restored = false;
|
|
4496
|
-
let rollbackError;
|
|
4497
|
-
try {
|
|
4498
|
-
restored = await withLaneAdmissionLock(hqRoot, before.provider, async () => {
|
|
4499
|
-
const current = readLane(hqRoot, laneId);
|
|
4500
|
-
if (current.version !== moved.version ||
|
|
4501
|
-
current.state !== "queued" ||
|
|
4502
|
-
current.host.kind !== target.kind ||
|
|
4503
|
-
current.host.id !== target.id ||
|
|
4504
|
-
current.account !== account) {
|
|
4505
|
-
return false;
|
|
4506
|
-
}
|
|
4507
|
-
updateLane(hqRoot, laneId, current.version, "command", (live) => ({
|
|
4508
|
-
...live,
|
|
4509
|
-
host: before.host,
|
|
4510
|
-
account: before.account,
|
|
4511
|
-
queued_reason: before.queued_reason,
|
|
4512
|
-
admission_reserved_at: before.admission_reserved_at,
|
|
4513
|
-
}));
|
|
4514
|
-
return true;
|
|
4515
|
-
}, ctx.sleep);
|
|
4516
|
-
}
|
|
4517
|
-
catch (restoreErr) {
|
|
4518
|
-
rollbackError = restoreErr;
|
|
4519
|
-
}
|
|
4520
|
-
const dispatchMessage = dispatchErrorMessage(err);
|
|
4521
|
-
const outcome = restored
|
|
4522
|
-
? "the lane stayed queued on its source"
|
|
4523
|
-
: rollbackError === undefined
|
|
4524
|
-
? "the lane changed after the move and was not overwritten"
|
|
4525
|
-
: `source restoration failed: ${dispatchErrorMessage(rollbackError)}`;
|
|
4526
|
-
const message = `Failed to dispatch lane ${laneId} to Outpost ${target.id}; ${outcome}: ${dispatchMessage}`;
|
|
4527
|
-
throw new LanesError("lane_move_dispatch_failed", message, {
|
|
4528
|
-
lane_id: laneId,
|
|
4529
|
-
message,
|
|
4530
|
-
source_host: before.host,
|
|
4531
|
-
target_host: target,
|
|
4532
|
-
source_restored: restored,
|
|
4533
|
-
...(dispatchDetails.owner_error !== undefined
|
|
4534
|
-
? { owner_error: dispatchDetails.owner_error }
|
|
4535
|
-
: {}),
|
|
4536
|
-
...(rollbackError !== undefined
|
|
4537
|
-
? { rollback_error: dispatchErrorMessage(rollbackError) }
|
|
4538
|
-
: {}),
|
|
4539
|
-
});
|
|
4540
|
-
}
|
|
4541
|
-
}
|
|
4542
|
-
async function dispatchRemoteLaneMove(ctx, source, target, account) {
|
|
4543
|
-
if (source.host.kind !== "outpost") {
|
|
4544
|
-
throw new LanesError("lane_move_wrong_host", `Lane ${source.lane_id} is not Outpost-hosted`, {
|
|
4545
|
-
lane_id: source.lane_id,
|
|
4546
|
-
host: source.host,
|
|
4547
|
-
});
|
|
4548
|
-
}
|
|
4549
|
-
let dispatched;
|
|
4550
|
-
try {
|
|
4551
|
-
dispatched = await ctx.dispatchOutpostExec({
|
|
4552
|
-
outpostId: source.host.id,
|
|
4553
|
-
command: buildOutpostMoveTargetCommand({
|
|
4554
|
-
laneId: source.lane_id,
|
|
4555
|
-
host: target,
|
|
4556
|
-
account,
|
|
4557
|
-
}),
|
|
4558
|
-
timeoutSeconds: 120,
|
|
4559
|
-
waitMs: 120_000,
|
|
4560
|
-
});
|
|
4561
|
-
}
|
|
4562
|
-
catch (err) {
|
|
4563
|
-
throw new LanesError("lane_move_dispatch_failed", `Could not ask Outpost ${source.host.id} to move lane ${source.lane_id}: ${dispatchErrorMessage(err)}`, { lane_id: source.lane_id, source_outpost: source.host.id });
|
|
4564
|
-
}
|
|
4565
|
-
const ownerError = ownerErrorFromOutput(dispatched.stdout);
|
|
4566
|
-
if (ownerError) {
|
|
4567
|
-
throw new LanesError("outpost_dispatch_failed", `Outpost ${source.host.id} rejected the lane move: ${ownerError.message}`, {
|
|
4568
|
-
lane_id: source.lane_id,
|
|
4569
|
-
source_outpost: source.host.id,
|
|
4570
|
-
command_id: dispatched.commandId,
|
|
4571
|
-
status: dispatched.status,
|
|
4572
|
-
done: dispatched.done ?? false,
|
|
4573
|
-
exit_code: dispatched.exitCode,
|
|
4574
|
-
owner_error: ownerError,
|
|
4575
|
-
});
|
|
4576
|
-
}
|
|
4577
|
-
if (dispatched.done !== true || dispatched.status !== "Success" || dispatched.exitCode !== 0) {
|
|
4578
|
-
throw new LanesError("lane_move_dispatch_failed", `Outpost ${source.host.id} did not confirm the lane move`, {
|
|
4579
|
-
lane_id: source.lane_id,
|
|
4580
|
-
source_outpost: source.host.id,
|
|
4581
|
-
command_id: dispatched.commandId,
|
|
4582
|
-
status: dispatched.status,
|
|
4583
|
-
done: dispatched.done ?? false,
|
|
4584
|
-
exit_code: dispatched.exitCode,
|
|
4585
|
-
});
|
|
4586
|
-
}
|
|
4587
|
-
const remote = parseRemoteJson(dispatched.stdout, "Outpost lane move");
|
|
4588
|
-
const remoteHost = remote?.host;
|
|
4589
|
-
if (remote?.ok !== true ||
|
|
4590
|
-
remote.action !== "move-target" ||
|
|
4591
|
-
remote.lane_id !== source.lane_id ||
|
|
4592
|
-
remote.state !== "queued" ||
|
|
4593
|
-
remote.account !== account ||
|
|
4594
|
-
remoteHost === null || typeof remoteHost !== "object" || Array.isArray(remoteHost) ||
|
|
4595
|
-
remoteHost.kind !== target.kind ||
|
|
4596
|
-
remoteHost.id !== target.id) {
|
|
4597
|
-
throw new LanesError("lane_move_unconfirmed", `Outpost ${source.host.id} returned an invalid lane move result`, {
|
|
4598
|
-
lane_id: source.lane_id,
|
|
4599
|
-
source_outpost: source.host.id,
|
|
4600
|
-
target,
|
|
4601
|
-
});
|
|
4602
|
-
}
|
|
4603
|
-
return {
|
|
4604
|
-
...source,
|
|
4605
|
-
state: "queued",
|
|
4606
|
-
host: target,
|
|
4607
|
-
account,
|
|
4608
|
-
};
|
|
4609
|
-
}
|
|
4610
|
-
async function runMoveTarget(ctx, laneId, opts) {
|
|
4611
|
-
const kind = opts.hostKind?.trim();
|
|
4612
|
-
const id = opts.hostId?.trim();
|
|
4613
|
-
const account = opts.account?.trim();
|
|
4614
|
-
if ((kind !== "local" && kind !== "outpost") || !id || !account) {
|
|
4615
|
-
throw new LanesError("lane_move_target_invalid", "Move target requires a valid host kind, host id, and account", {
|
|
4616
|
-
host_kind: kind ?? null,
|
|
4617
|
-
host_id: id ?? null,
|
|
4618
|
-
account: account ?? null,
|
|
4619
|
-
});
|
|
4620
|
-
}
|
|
4621
|
-
const moved = await commitLaneMove(ctx, laneId, { kind, id }, account);
|
|
4622
|
-
printJson({
|
|
4623
|
-
ok: true,
|
|
4624
|
-
action: "move-target",
|
|
4625
|
-
lane_id: moved.lane_id,
|
|
4626
|
-
state: moved.state,
|
|
4627
|
-
host: moved.host,
|
|
4628
|
-
account: moved.account ?? null,
|
|
4629
|
-
});
|
|
4630
|
-
}
|
|
4631
4299
|
async function refreshOutpostLaneCopiesForShow(ctx, lane, localLanes) {
|
|
4632
4300
|
if (lane.host.kind !== "outpost" || laneRunsHere(lane, reconcileHostFromCtx(ctx))) {
|
|
4633
4301
|
return lane;
|
|
@@ -4773,25 +4441,78 @@ async function runShow(ctx, laneId) {
|
|
|
4773
4441
|
: null,
|
|
4774
4442
|
});
|
|
4775
4443
|
}
|
|
4776
|
-
function
|
|
4444
|
+
function formatSessionSeniorValue(value) {
|
|
4445
|
+
if (value === undefined)
|
|
4446
|
+
return "<undefined>";
|
|
4447
|
+
try {
|
|
4448
|
+
return JSON.stringify(value) ?? String(value);
|
|
4449
|
+
}
|
|
4450
|
+
catch {
|
|
4451
|
+
return String(value);
|
|
4452
|
+
}
|
|
4453
|
+
}
|
|
4454
|
+
function callerSeniorAuthorization(ctx, seniorRef) {
|
|
4777
4455
|
const sessionId = sessionIdFromEnv(ctx.env);
|
|
4778
4456
|
const callerLaneId = ctx.env.HQ_LANE_ID?.trim() || undefined;
|
|
4779
4457
|
if (seniorRef.kind === "lane") {
|
|
4780
|
-
|
|
4458
|
+
if (callerLaneId === seniorRef.id)
|
|
4459
|
+
return { authorized: true };
|
|
4460
|
+
if (callerLaneId !== undefined)
|
|
4461
|
+
return { authorized: false, reason: "caller is a different lane" };
|
|
4462
|
+
if (!sessionId)
|
|
4463
|
+
return { authorized: false, reason: "no session id" };
|
|
4464
|
+
return { authorized: false, reason: "caller is not a lane" };
|
|
4465
|
+
}
|
|
4466
|
+
if (callerLaneId !== undefined)
|
|
4467
|
+
return { authorized: false, reason: "caller is a lane" };
|
|
4468
|
+
if (!sessionId)
|
|
4469
|
+
return { authorized: false, reason: "no session id" };
|
|
4470
|
+
if (seniorRef.kind === "session") {
|
|
4471
|
+
return sessionId === seniorRef.id
|
|
4472
|
+
? { authorized: true }
|
|
4473
|
+
: { authorized: false, reason: `session id ${sessionId} is not senior session ${seniorRef.id}` };
|
|
4781
4474
|
}
|
|
4782
|
-
if (!sessionId || callerLaneId !== undefined)
|
|
4783
|
-
return false;
|
|
4784
|
-
if (seniorRef.kind === "session")
|
|
4785
|
-
return sessionId === seniorRef.id;
|
|
4786
4475
|
if (seniorRef.kind !== "user")
|
|
4787
|
-
return false;
|
|
4476
|
+
return { authorized: false, reason: "senior is not a user" };
|
|
4788
4477
|
const loaded = loadSessionMeta(ctx.hqRoot(), sessionId, ctx);
|
|
4789
|
-
if (loaded.status
|
|
4790
|
-
return
|
|
4791
|
-
|
|
4792
|
-
|
|
4478
|
+
if (loaded.status === "missing") {
|
|
4479
|
+
return {
|
|
4480
|
+
authorized: false,
|
|
4481
|
+
reason: "session meta missing",
|
|
4482
|
+
repair: "if this is the owner's session, run bash core/scripts/hq-session.sh set senior user",
|
|
4483
|
+
};
|
|
4484
|
+
}
|
|
4485
|
+
if (loaded.status === "unreadable") {
|
|
4486
|
+
return { authorized: false, reason: "session meta unreadable" };
|
|
4487
|
+
}
|
|
4488
|
+
if (loaded.status === "malformed") {
|
|
4489
|
+
return { authorized: false, reason: "session meta invalid" };
|
|
4490
|
+
}
|
|
4491
|
+
const senior = sessionSeniorValue(loaded.doc);
|
|
4492
|
+
if (senior === "user" ||
|
|
4793
4493
|
(senior !== null && typeof senior === "object" && !Array.isArray(senior) &&
|
|
4794
|
-
senior.kind === "user")
|
|
4494
|
+
senior.kind === "user")) {
|
|
4495
|
+
return { authorized: true };
|
|
4496
|
+
}
|
|
4497
|
+
return {
|
|
4498
|
+
authorized: false,
|
|
4499
|
+
reason: `session senior is ${formatSessionSeniorValue(senior)}`,
|
|
4500
|
+
};
|
|
4501
|
+
}
|
|
4502
|
+
function callerSeniorReason(authorization) {
|
|
4503
|
+
if (authorization.authorized)
|
|
4504
|
+
return "";
|
|
4505
|
+
return authorization.repair
|
|
4506
|
+
? `${authorization.reason}; ${authorization.repair}`
|
|
4507
|
+
: authorization.reason;
|
|
4508
|
+
}
|
|
4509
|
+
function callerMatchesSenior(ctx, seniorRef) {
|
|
4510
|
+
return callerSeniorAuthorization(ctx, seniorRef).authorized;
|
|
4511
|
+
}
|
|
4512
|
+
function appendCallerSeniorReason(message, authorization) {
|
|
4513
|
+
return authorization.authorized
|
|
4514
|
+
? message
|
|
4515
|
+
: `${message}; authorization refused: ${callerSeniorReason(authorization)}`;
|
|
4795
4516
|
}
|
|
4796
4517
|
function laneGrantTargetHostedHere(ctx, lane) {
|
|
4797
4518
|
if (lane.host.kind === "local") {
|
|
@@ -4799,14 +4520,17 @@ function laneGrantTargetHostedHere(ctx, lane) {
|
|
|
4799
4520
|
}
|
|
4800
4521
|
return lane.host.kind === "outpost" && laneIsHostedHere(ctx, lane);
|
|
4801
4522
|
}
|
|
4802
|
-
function
|
|
4803
|
-
return
|
|
4523
|
+
function forceAdmissionAuthorization(ctx, lane) {
|
|
4524
|
+
return callerSeniorAuthorization(ctx, lane.senior);
|
|
4804
4525
|
}
|
|
4805
4526
|
async function applyHireGrant(ctx, laneId, hireGrant, authorization, grantContext) {
|
|
4806
4527
|
const hqRoot = ctx.hqRoot();
|
|
4807
4528
|
const initial = readLane(hqRoot, laneId);
|
|
4808
|
-
|
|
4809
|
-
|
|
4529
|
+
const initialCallerAuthorization = authorization === "senior"
|
|
4530
|
+
? callerSeniorAuthorization(ctx, initial.senior)
|
|
4531
|
+
: { authorized: true };
|
|
4532
|
+
if (!initialCallerAuthorization.authorized) {
|
|
4533
|
+
throw new LanesError("hire_grant_unauthorized", appendCallerSeniorReason(`Only senior ${seniorKey(initial.senior)} may change lane ${laneId}'s hire grant`, initialCallerAuthorization), { lane_id: laneId, senior: initial.senior, reason: initialCallerAuthorization.reason });
|
|
4810
4534
|
}
|
|
4811
4535
|
if (authorization === "ssm-owner-dispatch" &&
|
|
4812
4536
|
!laneGrantTargetHostedHere(ctx, initial)) {
|
|
@@ -4824,8 +4548,11 @@ async function applyHireGrant(ctx, laneId, hireGrant, authorization, grantContex
|
|
|
4824
4548
|
];
|
|
4825
4549
|
const updated = ctx.withGrantHireLocks(hqRoot, lockedLaneIds, () => {
|
|
4826
4550
|
const current = readLane(hqRoot, laneId);
|
|
4827
|
-
|
|
4828
|
-
|
|
4551
|
+
const currentCallerAuthorization = authorization === "senior"
|
|
4552
|
+
? callerSeniorAuthorization(ctx, current.senior)
|
|
4553
|
+
: { authorized: true };
|
|
4554
|
+
if (!currentCallerAuthorization.authorized) {
|
|
4555
|
+
throw new LanesError("hire_grant_unauthorized", appendCallerSeniorReason(`Only senior ${seniorKey(current.senior)} may change lane ${laneId}'s hire grant`, currentCallerAuthorization), { lane_id: laneId, senior: current.senior, reason: currentCallerAuthorization.reason });
|
|
4829
4556
|
}
|
|
4830
4557
|
if (authorization === "ssm-owner-dispatch" &&
|
|
4831
4558
|
!laneGrantTargetHostedHere(ctx, current)) {
|
|
@@ -4938,8 +4665,9 @@ async function runKeepAlive(ctx, laneId, opts) {
|
|
|
4938
4665
|
});
|
|
4939
4666
|
}
|
|
4940
4667
|
function assertKeepAliveChangeAllowed(ctx, lane, laneId) {
|
|
4941
|
-
|
|
4942
|
-
|
|
4668
|
+
const callerAuthorization = callerSeniorAuthorization(ctx, lane.senior);
|
|
4669
|
+
if (!callerAuthorization.authorized) {
|
|
4670
|
+
throw new LanesError("keep_alive_unauthorized", appendCallerSeniorReason("Only senior " + seniorKey(lane.senior) + " may change lane " + laneId + "'s keep-alive setting", callerAuthorization), { lane_id: laneId, senior: lane.senior, reason: callerAuthorization.reason });
|
|
4943
4671
|
}
|
|
4944
4672
|
if (!laneRunsHere(lane, reconcileHostFromCtx(ctx))) {
|
|
4945
4673
|
throw new LanesError("keep_alive_owner_host_required", "Run hq lanes keep-alive " + laneId + " on its owner host " + lane.host.kind + ":" + lane.host.id, { lane_id: laneId, host: lane.host });
|
|
@@ -4954,10 +4682,14 @@ async function runGrant(ctx, laneId, opts) {
|
|
|
4954
4682
|
}
|
|
4955
4683
|
const hireGrant = parseHireGrant(opts.hire);
|
|
4956
4684
|
const hqRoot = ctx.hqRoot();
|
|
4957
|
-
const target = await resolveCommandLaneTarget(ctx, laneId, {
|
|
4685
|
+
const target = await resolveCommandLaneTarget(ctx, laneId, {
|
|
4686
|
+
outpostHint: true,
|
|
4687
|
+
outpostId: opts.outpost,
|
|
4688
|
+
});
|
|
4958
4689
|
const initial = target.lane;
|
|
4959
|
-
|
|
4960
|
-
|
|
4690
|
+
const callerAuthorization = callerSeniorAuthorization(ctx, initial.senior);
|
|
4691
|
+
if (!callerAuthorization.authorized) {
|
|
4692
|
+
throw new LanesError("hire_grant_unauthorized", appendCallerSeniorReason(`Only senior ${seniorKey(initial.senior)} may change lane ${laneId}'s hire grant`, callerAuthorization), { lane_id: laneId, senior: initial.senior, reason: callerAuthorization.reason });
|
|
4961
4693
|
}
|
|
4962
4694
|
if (opts.outpost !== undefined) {
|
|
4963
4695
|
const requestedOutpost = opts.outpost.trim();
|
|
@@ -5012,8 +4744,9 @@ async function runApprove(ctx, laneId, opts) {
|
|
|
5012
4744
|
if (!allowed.includes(checkpoint)) {
|
|
5013
4745
|
throw new LanesError("registry_worker_checkpoint_unknown", `Checkpoint ${JSON.stringify(checkpoint)} is not declared by the lane's registry worker`, { lane_id: laneId, checkpoint, accepted: allowed });
|
|
5014
4746
|
}
|
|
5015
|
-
|
|
5016
|
-
|
|
4747
|
+
const callerAuthorization = callerSeniorAuthorization(ctx, initial.senior);
|
|
4748
|
+
if (!callerAuthorization.authorized) {
|
|
4749
|
+
throw new LanesError("registry_worker_approval_unauthorized", appendCallerSeniorReason(`Only senior ${seniorKey(initial.senior)} may approve lane ${laneId}'s registry worker checkpoint`, callerAuthorization), { lane_id: laneId, senior: initial.senior, reason: callerAuthorization.reason });
|
|
5017
4750
|
}
|
|
5018
4751
|
if (remoteOutpost) {
|
|
5019
4752
|
const approvalContext = createOutpostRegistryWorkerApprovalContext({
|
|
@@ -5039,8 +4772,9 @@ async function runApprove(ctx, laneId, opts) {
|
|
|
5039
4772
|
const actor = resumeProfileRepinActor(ctx);
|
|
5040
4773
|
const updated = retryUpdateLane(hqRoot, laneId, "command", (current) => {
|
|
5041
4774
|
assertLaneRunsHereForOperatorMutation(ctx, current, "approve");
|
|
5042
|
-
|
|
5043
|
-
|
|
4775
|
+
const currentCallerAuthorization = callerSeniorAuthorization(ctx, current.senior);
|
|
4776
|
+
if (!currentCallerAuthorization.authorized) {
|
|
4777
|
+
throw new LanesError("registry_worker_approval_unauthorized", appendCallerSeniorReason(`Only senior ${seniorKey(current.senior)} may approve lane ${laneId}'s registry worker checkpoint`, currentCallerAuthorization), { lane_id: laneId, senior: current.senior, reason: currentCallerAuthorization.reason });
|
|
5044
4778
|
}
|
|
5045
4779
|
if (JSON.stringify(current.registry_worker) !== JSON.stringify(initial.registry_worker) ||
|
|
5046
4780
|
!current.registry_worker ||
|
|
@@ -5206,8 +4940,9 @@ async function runAdmit(ctx, laneId, opts) {
|
|
|
5206
4940
|
}
|
|
5207
4941
|
let blockedBy;
|
|
5208
4942
|
if (opts.force) {
|
|
5209
|
-
|
|
5210
|
-
|
|
4943
|
+
const callerAuthorization = forceAdmissionAuthorization(ctx, lane);
|
|
4944
|
+
if (!callerAuthorization.authorized) {
|
|
4945
|
+
throw new LanesError("force_not_authorized", appendCallerSeniorReason(`Only lane ${lane.senior.kind === "lane" ? lane.senior.id : lane.senior.kind === "session" ? lane.senior.id : "senior/operator"} may force admission of ${laneId}`, callerAuthorization), { lane_id: laneId, senior: lane.senior, reason: callerAuthorization.reason });
|
|
5211
4946
|
}
|
|
5212
4947
|
const config = loadLanesConfig(hqRoot);
|
|
5213
4948
|
const lanes = loadAllLanes(hqRoot).filter((row) => row.lane_id !== laneId && laneRunsHere(row, host));
|
|
@@ -5335,7 +5070,7 @@ function applyResumeProfileAndEffort(current, next, input) {
|
|
|
5335
5070
|
? registryWorkerRecord(profile.registryWorker)
|
|
5336
5071
|
: undefined;
|
|
5337
5072
|
if (input.profileRepin !== undefined &&
|
|
5338
|
-
current.host.kind === "outpost" &&
|
|
5073
|
+
(current.host.kind === "outpost" || input.profileRepin.kind === "ssm-owner-dispatch") &&
|
|
5339
5074
|
current.worker_profile_hash !== profile.contentHash) {
|
|
5340
5075
|
const entry = {
|
|
5341
5076
|
old_hash: current.worker_profile_hash ?? null,
|
|
@@ -7074,6 +6809,19 @@ export function registerLanesCommand(parent, deps = {}) {
|
|
|
7074
6809
|
readOutpostCapacity,
|
|
7075
6810
|
};
|
|
7076
6811
|
const watchHandlers = createLaneWatchHandlers(watchHandlerDeps);
|
|
6812
|
+
const moveHandlers = createLaneMoveHandlers({
|
|
6813
|
+
assertKnownOutpost,
|
|
6814
|
+
assertLaneRunsHereForOperatorMutation,
|
|
6815
|
+
dispatchCreatedOutpostLane,
|
|
6816
|
+
loadHostCapacitySnapshots,
|
|
6817
|
+
ownerErrorFromOutput,
|
|
6818
|
+
parseRemoteJson,
|
|
6819
|
+
printJson,
|
|
6820
|
+
readJsonFile,
|
|
6821
|
+
reconcileHostFromCtx,
|
|
6822
|
+
runDir,
|
|
6823
|
+
timeoutPath,
|
|
6824
|
+
});
|
|
7077
6825
|
const lanes = parent
|
|
7078
6826
|
.command("lanes", { hidden: true })
|
|
7079
6827
|
.description("Create, supervise, and steer worker lanes; routed decision queue");
|
|
@@ -7242,7 +6990,7 @@ export function registerLanesCommand(parent, deps = {}) {
|
|
|
7242
6990
|
.argument("<lane-id>", "Lane id")
|
|
7243
6991
|
.requiredOption("--placement <mode>", "local | auto | outpost:<id>")
|
|
7244
6992
|
.action(wrap(async (laneId, opts) => {
|
|
7245
|
-
await runMove(ctx, laneId, opts);
|
|
6993
|
+
await moveHandlers.runMove(ctx, laneId, opts);
|
|
7246
6994
|
}));
|
|
7247
6995
|
lanes
|
|
7248
6996
|
.command("move-target", { hidden: true })
|
|
@@ -7251,7 +6999,7 @@ export function registerLanesCommand(parent, deps = {}) {
|
|
|
7251
6999
|
.requiredOption("--host-id <id>", "Target host id")
|
|
7252
7000
|
.requiredOption("--account <id>", "Provider account id")
|
|
7253
7001
|
.action(wrap(async (laneId, opts) => {
|
|
7254
|
-
await runMoveTarget(ctx, laneId, opts);
|
|
7002
|
+
await moveHandlers.runMoveTarget(ctx, laneId, opts);
|
|
7255
7003
|
}));
|
|
7256
7004
|
lanes
|
|
7257
7005
|
.command("balance-move-target", { hidden: true })
|