@indigoai-us/hq-cli 5.239.0 → 5.241.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 +36 -0
- package/dist/command-catalog.generated.d.ts +41 -0
- package/dist/command-catalog.generated.js +54 -0
- package/dist/commands/agent-enroll.d.ts +8 -0
- package/dist/commands/agent-enroll.js +19 -2
- package/dist/commands/lanes.js +194 -17
- package/dist/lib/agent-kit/creds.d.ts +30 -2
- package/dist/lib/agent-kit/creds.js +75 -6
- package/dist/lib/lanes/approval-gate.d.ts +8 -0
- package/dist/lib/lanes/approval-gate.js +90 -0
- package/dist/lib/lanes/claude-balance.d.ts +2 -0
- package/dist/lib/lanes/claude-balance.js +2 -1
- package/dist/lib/lanes/errors.d.ts +1 -1
- package/dist/lib/lanes/index.d.ts +3 -1
- package/dist/lib/lanes/index.js +2 -1
- package/dist/lib/lanes/outpost-dispatch.d.ts +2 -1
- package/dist/lib/lanes/outpost-dispatch.js +1 -0
- package/dist/lib/lanes/outpost-registry-worker-approval.d.ts +18 -0
- package/dist/lib/lanes/outpost-registry-worker-approval.js +109 -0
- package/dist/lib/lanes/profile.d.ts +10 -0
- package/dist/lib/lanes/profile.js +88 -0
- package/dist/lib/lanes/reconcile.js +38 -11
- package/dist/lib/lanes/registry-persona.d.ts +20 -0
- package/dist/lib/lanes/registry-persona.js +329 -0
- package/dist/lib/lanes/registry.d.ts +4 -1
- package/dist/lib/lanes/registry.js +8 -3
- package/dist/lib/lanes/spawn.d.ts +1 -1
- package/dist/lib/lanes/spawn.js +28 -7
- package/dist/lib/lanes/store.js +90 -0
- package/dist/lib/lanes/types.d.ts +29 -0
- package/dist/lib/lanes/types.js +1 -1
- package/dist/lib/lanes/watcher.d.ts +2 -0
- package/dist/lib/lanes/watcher.js +46 -15
- package/package.json +1 -1
|
@@ -11,6 +11,7 @@ import { parseUserYaml } from "../../utils/user-yaml-error.js";
|
|
|
11
11
|
import { compareLaneQueueOrder, hasActiveAdmissionReservation, hasActivePendingResume, laneAdmissionCapReason, laneRunsOnHost, withAllLaneAdmissionLocks, } from "./admission.js";
|
|
12
12
|
import { accountStatus, isCredentialProven, resolveAccount } from "./accounts.js";
|
|
13
13
|
import { effectiveAccountHome } from "./account-home.js";
|
|
14
|
+
import { enforceRegistryApprovalGate } from "./approval-gate.js";
|
|
14
15
|
import { loadLanesConfig, providerQuotaDefaultHoldMs } from "./config.js";
|
|
15
16
|
import { MAX_LANE_DEPTH } from "./constants.js";
|
|
16
17
|
import { AccountResolutionError, LanesError, isLaneVersionConflict } from "./errors.js";
|
|
@@ -1059,13 +1060,22 @@ async function heartbeatAndDeadHosts(hqRoot, lanes, summary, deps, now) {
|
|
|
1059
1060
|
process.stderr.write(`hq lanes reconcile: terminal ask refused for ${lane.lane_id}: ${questionRefusalCode}\n`);
|
|
1060
1061
|
}
|
|
1061
1062
|
}
|
|
1063
|
+
let terminalEnvelope = latest?.envelope;
|
|
1064
|
+
let terminalClassification = repairedClassification;
|
|
1062
1065
|
const result = casLaneIf(hqRoot, lane.lane_id, (current) => current.state === "running", (current) => {
|
|
1066
|
+
const approvalGate = latest?.envelope
|
|
1067
|
+
? enforceRegistryApprovalGate(current, latest.envelope)
|
|
1068
|
+
: undefined;
|
|
1069
|
+
terminalEnvelope = approvalGate?.envelope ?? latest?.envelope;
|
|
1070
|
+
terminalClassification = approvalGate?.violation
|
|
1071
|
+
? { state: "blocked", reason: "approval_gate_violated" }
|
|
1072
|
+
: repairedClassification;
|
|
1063
1073
|
const next = {
|
|
1064
1074
|
...current,
|
|
1065
|
-
state:
|
|
1075
|
+
state: terminalClassification.state,
|
|
1066
1076
|
queued_reason: undefined,
|
|
1067
|
-
terminal_reason:
|
|
1068
|
-
interrupted_at:
|
|
1077
|
+
terminal_reason: terminalClassification.reason ?? "host_lost",
|
|
1078
|
+
interrupted_at: terminalClassification.state === "interrupted" ? nowIso : undefined,
|
|
1069
1079
|
...(providerRejection
|
|
1070
1080
|
? { terminal_details: providerRejectionDetails(providerRejection, current) }
|
|
1071
1081
|
: {}),
|
|
@@ -1073,8 +1083,16 @@ async function heartbeatAndDeadHosts(hqRoot, lanes, summary, deps, now) {
|
|
|
1073
1083
|
...(quota?.marker ? { model_quota_marker: quota.marker } : {}),
|
|
1074
1084
|
last_host_report_at: nowIso,
|
|
1075
1085
|
};
|
|
1076
|
-
if (
|
|
1077
|
-
|
|
1086
|
+
if (approvalGate && !approvalGate.violation && approvalGate.approvedIdsToConsume.length > 0) {
|
|
1087
|
+
next.registry_worker_approval_history = approvalGate.approvedIdsToConsume.reduce((history, approvalId) => appendCappedHistory(history, {
|
|
1088
|
+
event: "consumed",
|
|
1089
|
+
approval_id: approvalId,
|
|
1090
|
+
envelope_ref: latest.ref,
|
|
1091
|
+
at: nowIso,
|
|
1092
|
+
}), current.registry_worker_approval_history ?? []);
|
|
1093
|
+
}
|
|
1094
|
+
if (terminalEnvelope && latest) {
|
|
1095
|
+
const envelope = terminalEnvelope;
|
|
1078
1096
|
next.story_status = envelope.story_status;
|
|
1079
1097
|
if (envelope.story_complete !== undefined) {
|
|
1080
1098
|
next.story_complete = envelope.story_complete;
|
|
@@ -1095,6 +1113,15 @@ async function heartbeatAndDeadHosts(hqRoot, lanes, summary, deps, now) {
|
|
|
1095
1113
|
: { blocked_reason: undefined }),
|
|
1096
1114
|
}
|
|
1097
1115
|
: story);
|
|
1116
|
+
if (approvalGate?.violation) {
|
|
1117
|
+
next.last_envelope = {
|
|
1118
|
+
ref: latest.ref,
|
|
1119
|
+
decision: envelope.decision,
|
|
1120
|
+
at: nowIso,
|
|
1121
|
+
artifacts: envelope.artifacts,
|
|
1122
|
+
blocked_reason: envelope.blocked_reason,
|
|
1123
|
+
};
|
|
1124
|
+
}
|
|
1098
1125
|
}
|
|
1099
1126
|
return next;
|
|
1100
1127
|
}, deps.dryRun);
|
|
@@ -1102,20 +1129,20 @@ async function heartbeatAndDeadHosts(hqRoot, lanes, summary, deps, now) {
|
|
|
1102
1129
|
summary.dead_host.push({
|
|
1103
1130
|
id: lane.lane_id,
|
|
1104
1131
|
from: "running",
|
|
1105
|
-
to:
|
|
1106
|
-
reason:
|
|
1132
|
+
to: terminalClassification.state,
|
|
1133
|
+
reason: terminalClassification.reason ?? "host_lost",
|
|
1107
1134
|
});
|
|
1108
|
-
if (latest
|
|
1135
|
+
if (terminalEnvelope && latest && questionRefusalCode === null) {
|
|
1109
1136
|
const mesh = deps.mesh === undefined ? createLaneMesh({ interactive: false }) : deps.mesh;
|
|
1110
1137
|
if (mesh) {
|
|
1111
1138
|
const meshEmitTimeoutMs = deps.meshEmitTimeoutMs ?? 10_000;
|
|
1112
1139
|
let timer;
|
|
1113
1140
|
try {
|
|
1114
|
-
const questionIds =
|
|
1115
|
-
? envelopeQuestionIds(lane.lane_id, latest.ref,
|
|
1141
|
+
const questionIds = terminalEnvelope.decision === "ask"
|
|
1142
|
+
? envelopeQuestionIds(lane.lane_id, latest.ref, terminalEnvelope)
|
|
1116
1143
|
: undefined;
|
|
1117
1144
|
await Promise.race([
|
|
1118
|
-
emitLaneDecisionEvents(mesh, result.record,
|
|
1145
|
+
emitLaneDecisionEvents(mesh, result.record, terminalEnvelope, { questionIds }),
|
|
1119
1146
|
new Promise((_resolve, reject) => {
|
|
1120
1147
|
timer = setTimeout(() => reject(new Error(`terminal mesh emit timed out after ${meshEmitTimeoutMs}ms`)), meshEmitTimeoutMs);
|
|
1121
1148
|
}),
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { RegistryWorkerRecord } from "./types.js";
|
|
2
|
+
export interface RegistryPersona {
|
|
3
|
+
id: string;
|
|
4
|
+
/** Registry-relative worker directory. Kept for stable resume resolution. */
|
|
5
|
+
path: string;
|
|
6
|
+
workerYamlSha256: string;
|
|
7
|
+
description: string;
|
|
8
|
+
instructions: string;
|
|
9
|
+
/** Skill files are path references only. Their contents are never inlined. */
|
|
10
|
+
skillFiles: string[];
|
|
11
|
+
postExecute: string[];
|
|
12
|
+
approvalRequired: boolean;
|
|
13
|
+
humanCheckpoints: string[];
|
|
14
|
+
}
|
|
15
|
+
/** Resolve an active registry worker for a lane without company fallback. */
|
|
16
|
+
export declare function resolveRegistryPersona(hqRoot: string, id: string, laneCompany: string | undefined): RegistryPersona;
|
|
17
|
+
/** Re-read the recorded path on resume; registry changes cannot redirect a lane. */
|
|
18
|
+
export declare function loadRegistryPersonaFromRecord(hqRoot: string, record: RegistryWorkerRecord): RegistryPersona;
|
|
19
|
+
export declare function registryWorkerRecord(persona: RegistryPersona): RegistryWorkerRecord;
|
|
20
|
+
//# sourceMappingURL=registry-persona.d.ts.map
|
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import * as fs from "node:fs";
|
|
3
|
+
import * as path from "node:path";
|
|
4
|
+
import { parseUserYaml } from "../../utils/user-yaml-error.js";
|
|
5
|
+
import { WorkerResolutionError } from "./errors.js";
|
|
6
|
+
const REGISTRY_RELATIVE_PATH = path.join("core", "workers", "registry.yaml");
|
|
7
|
+
function fail(code, message, details = {}) {
|
|
8
|
+
throw new WorkerResolutionError(code, message, details);
|
|
9
|
+
}
|
|
10
|
+
function isPlainObject(value) {
|
|
11
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
12
|
+
}
|
|
13
|
+
function withinRoot(root, target) {
|
|
14
|
+
const relative = path.relative(root, target);
|
|
15
|
+
return relative === "" || (!path.isAbsolute(relative) && relative !== ".." && !relative.startsWith(`..${path.sep}`));
|
|
16
|
+
}
|
|
17
|
+
function resolveWorkerDirectory(hqRoot, workerPath, id) {
|
|
18
|
+
const relative = workerPath.replaceAll("\\", "/").replace(/\/+$/, "");
|
|
19
|
+
if (relative.length === 0 ||
|
|
20
|
+
path.posix.isAbsolute(relative) ||
|
|
21
|
+
relative.split("/").some((part) => part === ".." || part === ".")) {
|
|
22
|
+
fail("registry_worker_invalid", `Registry worker ${JSON.stringify(id)} has an unsafe path`, {
|
|
23
|
+
worker: id,
|
|
24
|
+
path: workerPath,
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
const root = path.resolve(hqRoot);
|
|
28
|
+
const abs = path.resolve(root, ...relative.split("/"));
|
|
29
|
+
if (!withinRoot(root, abs)) {
|
|
30
|
+
fail("registry_worker_invalid", `Registry worker ${JSON.stringify(id)} escapes HQ_ROOT`, {
|
|
31
|
+
worker: id,
|
|
32
|
+
path: workerPath,
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
let realRoot;
|
|
36
|
+
let realWorkerDir;
|
|
37
|
+
try {
|
|
38
|
+
realRoot = fs.realpathSync(root);
|
|
39
|
+
realWorkerDir = fs.realpathSync(abs);
|
|
40
|
+
}
|
|
41
|
+
catch (err) {
|
|
42
|
+
fail("registry_worker_unreadable", `Cannot resolve registry worker ${JSON.stringify(id)} at ${abs}: ${err instanceof Error ? err.message : String(err)}`, {
|
|
43
|
+
worker: id,
|
|
44
|
+
path: abs,
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
if (!withinRoot(realRoot, realWorkerDir)) {
|
|
48
|
+
fail("registry_worker_invalid", `Registry worker ${JSON.stringify(id)} resolves outside HQ_ROOT`, {
|
|
49
|
+
worker: id,
|
|
50
|
+
path: workerPath,
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
return { abs: realWorkerDir, relative };
|
|
54
|
+
}
|
|
55
|
+
function readWorkerYaml(hqRoot, workerPath, id) {
|
|
56
|
+
const directory = resolveWorkerDirectory(hqRoot, workerPath, id);
|
|
57
|
+
const filePath = path.join(directory.abs, "worker.yaml");
|
|
58
|
+
let stat;
|
|
59
|
+
let text;
|
|
60
|
+
try {
|
|
61
|
+
stat = fs.lstatSync(filePath);
|
|
62
|
+
if (stat.isSymbolicLink() || !stat.isFile()) {
|
|
63
|
+
fail("registry_worker_invalid", `Registry worker ${JSON.stringify(id)} worker.yaml must be a regular file`, {
|
|
64
|
+
worker: id,
|
|
65
|
+
path: filePath,
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
text = fs.readFileSync(filePath, "utf8");
|
|
69
|
+
}
|
|
70
|
+
catch (err) {
|
|
71
|
+
if (err instanceof WorkerResolutionError)
|
|
72
|
+
throw err;
|
|
73
|
+
fail("registry_worker_unreadable", `Cannot read registry worker ${JSON.stringify(id)} at ${filePath}: ${err instanceof Error ? err.message : String(err)}`, {
|
|
74
|
+
worker: id,
|
|
75
|
+
path: filePath,
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
let parsed;
|
|
79
|
+
try {
|
|
80
|
+
parsed = parseUserYaml(text, filePath);
|
|
81
|
+
}
|
|
82
|
+
catch (err) {
|
|
83
|
+
fail("registry_worker_invalid", `Registry worker ${JSON.stringify(id)} has invalid worker.yaml at ${filePath}: ${err instanceof Error ? err.message : String(err)}`, {
|
|
84
|
+
worker: id,
|
|
85
|
+
path: filePath,
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
if (!isPlainObject(parsed)) {
|
|
89
|
+
fail("registry_worker_invalid", `Registry worker ${JSON.stringify(id)} worker.yaml must contain an object`, {
|
|
90
|
+
worker: id,
|
|
91
|
+
path: filePath,
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
const realFile = fs.realpathSync(filePath);
|
|
95
|
+
const realRoot = fs.realpathSync(path.resolve(hqRoot));
|
|
96
|
+
if (!withinRoot(realRoot, realFile)) {
|
|
97
|
+
fail("registry_worker_invalid", `Registry worker ${JSON.stringify(id)} worker.yaml resolves outside HQ_ROOT`, {
|
|
98
|
+
worker: id,
|
|
99
|
+
path: filePath,
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
return {
|
|
103
|
+
filePath,
|
|
104
|
+
text,
|
|
105
|
+
yaml: parsed,
|
|
106
|
+
sha256: `sha256:${createHash("sha256").update(text, "utf8").digest("hex")}`,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
function textValue(value, field, id) {
|
|
110
|
+
if (value === undefined || value === null)
|
|
111
|
+
return "";
|
|
112
|
+
if (typeof value === "string")
|
|
113
|
+
return value.trim();
|
|
114
|
+
if (Array.isArray(value) && value.every((item) => typeof item === "string")) {
|
|
115
|
+
return value.map((item) => item.trim()).filter(Boolean).join("\n");
|
|
116
|
+
}
|
|
117
|
+
fail("registry_worker_invalid", `Registry worker ${JSON.stringify(id)} ${field} must be a string or string array`, {
|
|
118
|
+
worker: id,
|
|
119
|
+
field,
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
function stringList(value, field, id) {
|
|
123
|
+
if (value === undefined || value === null)
|
|
124
|
+
return [];
|
|
125
|
+
const values = typeof value === "string" ? [value] : value;
|
|
126
|
+
if (!Array.isArray(values) || values.some((item) => typeof item !== "string" || item.trim().length === 0)) {
|
|
127
|
+
fail("registry_worker_invalid", `Registry worker ${JSON.stringify(id)} ${field} must be an array of non-empty strings`, {
|
|
128
|
+
worker: id,
|
|
129
|
+
field,
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
const cleaned = values.map((item) => item.trim());
|
|
133
|
+
if (new Set(cleaned).size !== cleaned.length) {
|
|
134
|
+
fail("registry_worker_invalid", `Registry worker ${JSON.stringify(id)} ${field} must not contain duplicates`, {
|
|
135
|
+
worker: id,
|
|
136
|
+
field,
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
return cleaned;
|
|
140
|
+
}
|
|
141
|
+
function verificationCommands(value, id) {
|
|
142
|
+
if (value === undefined || value === null)
|
|
143
|
+
return [];
|
|
144
|
+
if (!Array.isArray(value)) {
|
|
145
|
+
fail("registry_worker_invalid", `Registry worker ${JSON.stringify(id)} verification.post_execute must be an array`, {
|
|
146
|
+
worker: id,
|
|
147
|
+
field: "verification.post_execute",
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
const commands = value.map((entry, index) => {
|
|
151
|
+
if (typeof entry === "string" && entry.trim() !== "")
|
|
152
|
+
return entry.trim();
|
|
153
|
+
if (isPlainObject(entry) && typeof entry.command === "string" && entry.command.trim() !== "") {
|
|
154
|
+
return entry.command.trim();
|
|
155
|
+
}
|
|
156
|
+
fail("registry_worker_invalid", `Registry worker ${JSON.stringify(id)} verification.post_execute[${index}] must contain a non-empty command`, {
|
|
157
|
+
worker: id,
|
|
158
|
+
field: `verification.post_execute[${index}]`,
|
|
159
|
+
});
|
|
160
|
+
});
|
|
161
|
+
if (new Set(commands).size !== commands.length) {
|
|
162
|
+
fail("registry_worker_invalid", `Registry worker ${JSON.stringify(id)} verification.post_execute must not contain duplicate commands`, {
|
|
163
|
+
worker: id,
|
|
164
|
+
field: "verification.post_execute",
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
return commands;
|
|
168
|
+
}
|
|
169
|
+
function personaFromYaml(hqRoot, id, workerPath, yaml, sha256, registryDescription, pinnedApproval) {
|
|
170
|
+
if (yaml.worker?.id !== undefined && yaml.worker.id !== id) {
|
|
171
|
+
fail("registry_worker_invalid", `Registry worker ${JSON.stringify(id)} worker.yaml declares a different id`, {
|
|
172
|
+
worker: id,
|
|
173
|
+
declared_id: yaml.worker.id,
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
const workerDir = resolveWorkerDirectory(hqRoot, workerPath, id).abs;
|
|
177
|
+
const description = textValue(yaml.worker?.description ?? yaml.description ?? registryDescription, "description", id) || id;
|
|
178
|
+
const instructions = textValue(yaml.instructions, "instructions", id);
|
|
179
|
+
const skillFiles = [];
|
|
180
|
+
if (yaml.skills !== undefined) {
|
|
181
|
+
if (!Array.isArray(yaml.skills)) {
|
|
182
|
+
fail("registry_worker_invalid", `Registry worker ${JSON.stringify(id)} skills must be an array`, {
|
|
183
|
+
worker: id,
|
|
184
|
+
field: "skills",
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
for (const [index, skill] of yaml.skills.entries()) {
|
|
188
|
+
if (!isPlainObject(skill) || typeof skill.file !== "string" || skill.file.trim().length === 0) {
|
|
189
|
+
fail("registry_worker_invalid", `Registry worker ${JSON.stringify(id)} skills[${index}].file must be a non-empty string`, {
|
|
190
|
+
worker: id,
|
|
191
|
+
field: `skills[${index}].file`,
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
skillFiles.push(path.resolve(workerDir, skill.file.trim()));
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
let verification = {};
|
|
198
|
+
if (yaml.verification !== undefined) {
|
|
199
|
+
if (!isPlainObject(yaml.verification)) {
|
|
200
|
+
fail("registry_worker_invalid", `Registry worker ${JSON.stringify(id)} verification must be an object`, {
|
|
201
|
+
worker: id,
|
|
202
|
+
field: "verification",
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
verification = yaml.verification;
|
|
206
|
+
}
|
|
207
|
+
if (verification.approval_required !== undefined && typeof verification.approval_required !== "boolean") {
|
|
208
|
+
fail("registry_worker_invalid", `Registry worker ${JSON.stringify(id)} verification.approval_required must be a boolean`, {
|
|
209
|
+
worker: id,
|
|
210
|
+
field: "verification.approval_required",
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
const humanCheckpoints = pinnedApproval?.human_checkpoints ??
|
|
214
|
+
stringList(verification.human_checkpoints, "verification.human_checkpoints", id);
|
|
215
|
+
return {
|
|
216
|
+
id,
|
|
217
|
+
path: workerPath,
|
|
218
|
+
workerYamlSha256: sha256,
|
|
219
|
+
description,
|
|
220
|
+
instructions,
|
|
221
|
+
skillFiles,
|
|
222
|
+
postExecute: verificationCommands(verification.post_execute, id),
|
|
223
|
+
approvalRequired: pinnedApproval?.approval_required ?? verification.approval_required === true,
|
|
224
|
+
humanCheckpoints: [...humanCheckpoints],
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
function registryEntries(hqRoot) {
|
|
228
|
+
const registryPath = path.join(hqRoot, REGISTRY_RELATIVE_PATH);
|
|
229
|
+
let text;
|
|
230
|
+
try {
|
|
231
|
+
text = fs.readFileSync(registryPath, "utf8");
|
|
232
|
+
}
|
|
233
|
+
catch (err) {
|
|
234
|
+
fail("registry_worker_unreadable", `Cannot read worker registry at ${registryPath}: ${err instanceof Error ? err.message : String(err)}`, {
|
|
235
|
+
path: registryPath,
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
let parsed;
|
|
239
|
+
try {
|
|
240
|
+
parsed = parseUserYaml(text, registryPath);
|
|
241
|
+
}
|
|
242
|
+
catch (err) {
|
|
243
|
+
fail("registry_worker_unreadable", `Cannot parse worker registry at ${registryPath}: ${err instanceof Error ? err.message : String(err)}`, {
|
|
244
|
+
path: registryPath,
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
if (!isPlainObject(parsed) || !Array.isArray(parsed.workers)) {
|
|
248
|
+
fail("registry_worker_unreadable", `Worker registry at ${registryPath} must contain a workers array`, {
|
|
249
|
+
path: registryPath,
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
return parsed.workers.filter(isPlainObject);
|
|
253
|
+
}
|
|
254
|
+
/** Resolve an active registry worker for a lane without company fallback. */
|
|
255
|
+
export function resolveRegistryPersona(hqRoot, id, laneCompany) {
|
|
256
|
+
const workerId = id.trim();
|
|
257
|
+
if (!workerId) {
|
|
258
|
+
fail("registry_worker_invalid", "registry_worker must be a non-empty worker id", { worker: id });
|
|
259
|
+
}
|
|
260
|
+
const candidates = registryEntries(hqRoot).filter((entry) => entry.id === workerId);
|
|
261
|
+
if (candidates.length === 0) {
|
|
262
|
+
fail("registry_worker_not_found", `Registry worker ${JSON.stringify(workerId)} was not found`, {
|
|
263
|
+
worker: workerId,
|
|
264
|
+
path: path.join(hqRoot, REGISTRY_RELATIVE_PATH),
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
if (candidates.length > 1) {
|
|
268
|
+
fail("registry_worker_ambiguous", `Registry worker ${JSON.stringify(workerId)} appears more than once`, {
|
|
269
|
+
worker: workerId,
|
|
270
|
+
paths: candidates.map((entry) => entry.path),
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
const entry = candidates[0];
|
|
274
|
+
if (typeof entry.path !== "string" || entry.path.trim().length === 0) {
|
|
275
|
+
fail("registry_worker_invalid", `Registry worker ${JSON.stringify(workerId)} has no path`, {
|
|
276
|
+
worker: workerId,
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
if (entry.company !== undefined && typeof entry.company !== "string") {
|
|
280
|
+
fail("registry_worker_invalid", `Registry worker ${JSON.stringify(workerId)} company must be a string`, {
|
|
281
|
+
worker: workerId,
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
if (typeof entry.company === "string" && entry.company.trim().length === 0) {
|
|
285
|
+
fail("registry_worker_invalid", `Registry worker ${JSON.stringify(workerId)} company must be non-empty when supplied`, {
|
|
286
|
+
worker: workerId,
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
const workerCompany = typeof entry.company === "string" && entry.company.trim().length > 0
|
|
290
|
+
? entry.company.trim()
|
|
291
|
+
: undefined;
|
|
292
|
+
if (workerCompany !== undefined && workerCompany !== laneCompany) {
|
|
293
|
+
fail("registry_worker_company_mismatch", `Registry worker ${JSON.stringify(workerId)} belongs to company ${workerCompany} and is not available to ${laneCompany ?? "(none)"}`, {
|
|
294
|
+
worker: workerId,
|
|
295
|
+
worker_company: workerCompany,
|
|
296
|
+
lane_company: laneCompany ?? null,
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
const status = typeof entry.status === "string" && entry.status.trim() !== ""
|
|
300
|
+
? entry.status.trim()
|
|
301
|
+
: "active";
|
|
302
|
+
if (status !== "active") {
|
|
303
|
+
fail("registry_worker_inactive", `Registry worker ${JSON.stringify(workerId)} has status ${JSON.stringify(status)} (need active)`, {
|
|
304
|
+
worker: workerId,
|
|
305
|
+
status,
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
const worker = readWorkerYaml(hqRoot, entry.path.trim(), workerId);
|
|
309
|
+
const persona = personaFromYaml(hqRoot, workerId, entry.path.trim(), worker.yaml, worker.sha256, entry.description);
|
|
310
|
+
return persona;
|
|
311
|
+
}
|
|
312
|
+
/** Re-read the recorded path on resume; registry changes cannot redirect a lane. */
|
|
313
|
+
export function loadRegistryPersonaFromRecord(hqRoot, record) {
|
|
314
|
+
const worker = readWorkerYaml(hqRoot, record.path, record.id);
|
|
315
|
+
return personaFromYaml(hqRoot, record.id, record.path, worker.yaml, worker.sha256, undefined, {
|
|
316
|
+
approval_required: record.approval_required,
|
|
317
|
+
human_checkpoints: record.human_checkpoints,
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
export function registryWorkerRecord(persona) {
|
|
321
|
+
return {
|
|
322
|
+
id: persona.id,
|
|
323
|
+
path: persona.path,
|
|
324
|
+
worker_yaml_sha256: persona.workerYamlSha256,
|
|
325
|
+
approval_required: persona.approvalRequired,
|
|
326
|
+
human_checkpoints: [...persona.humanCheckpoints],
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
//# sourceMappingURL=registry-persona.js.map
|
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
*/
|
|
25
25
|
import * as fs from "node:fs";
|
|
26
26
|
import { type WorkerProfile } from "./profile.js";
|
|
27
|
+
import type { RegistryWorkerRecord } from "./types.js";
|
|
27
28
|
export declare const WORKER_REGISTRY_REL: string;
|
|
28
29
|
export interface RegistryWorkerEntry {
|
|
29
30
|
id: string;
|
|
@@ -115,5 +116,7 @@ export declare function tenancyFromRegistryPath(entry: RegistryWorkerEntry): Wor
|
|
|
115
116
|
*/
|
|
116
117
|
export declare function resolveContextBasePaths(hqRoot: string, workerDir: string, bases: string[] | undefined, deps?: RegistryDeps): string[];
|
|
117
118
|
export declare function resolveSkillFiles(workerDir: string, skills: WorkerYamlSkill[] | undefined, deps?: RegistryDeps): string[];
|
|
118
|
-
export declare function resolveLaneWorker(hqRoot: string, workerId: string, laneCompany: string | undefined, deps?: RegistryDeps
|
|
119
|
+
export declare function resolveLaneWorker(hqRoot: string, workerId: string, laneCompany: string | undefined, deps?: RegistryDeps, options?: {
|
|
120
|
+
registryWorkerRecord?: RegistryWorkerRecord;
|
|
121
|
+
}): ResolvedLaneWorker;
|
|
119
122
|
//# sourceMappingURL=registry.d.ts.map
|
|
@@ -28,7 +28,7 @@ import { parseUserYaml } from "../../utils/user-yaml-error.js";
|
|
|
28
28
|
import { readContributes } from "../scan-packages/index.js";
|
|
29
29
|
import { contributionLinks, linkStatus, } from "../../utils/pack-contributions.js";
|
|
30
30
|
import { WorkerResolutionError } from "./errors.js";
|
|
31
|
-
import {
|
|
31
|
+
import { loadWorkerProfileWithRegistry, workerProfileMarker, } from "./profile.js";
|
|
32
32
|
export const WORKER_REGISTRY_REL = path.join("core", "workers", "registry.yaml");
|
|
33
33
|
function defaultRegistryDeps() {
|
|
34
34
|
return {
|
|
@@ -371,10 +371,15 @@ function readRegistryOrEmpty(hqRoot, deps) {
|
|
|
371
371
|
return [];
|
|
372
372
|
return readWorkerRegistryFile(hqRoot, deps);
|
|
373
373
|
}
|
|
374
|
-
export function resolveLaneWorker(hqRoot, workerId, laneCompany, deps = defaultRegistryDeps()) {
|
|
374
|
+
export function resolveLaneWorker(hqRoot, workerId, laneCompany, deps = defaultRegistryDeps(), options = {}) {
|
|
375
375
|
const personalRel = path.posix.join("personal", "workers", workerId);
|
|
376
376
|
const profile = workerProfileMarker(hqRoot, workerId) === "present"
|
|
377
|
-
?
|
|
377
|
+
? loadWorkerProfileWithRegistry(hqRoot, workerId, {
|
|
378
|
+
laneCompany,
|
|
379
|
+
...(options.registryWorkerRecord !== undefined
|
|
380
|
+
? { registryWorkerRecord: options.registryWorkerRecord }
|
|
381
|
+
: {}),
|
|
382
|
+
})
|
|
378
383
|
: undefined;
|
|
379
384
|
const personalClaim = profile ? "profile" : personalDirClaim(hqRoot, workerId, deps);
|
|
380
385
|
if (personalClaim === "missing_yaml") {
|
|
@@ -239,7 +239,7 @@ export declare function buildProviderCommand(provider: LaneProvider, input: {
|
|
|
239
239
|
export declare function workerEnv(base: NodeJS.ProcessEnv, resolved: {
|
|
240
240
|
envVar: string;
|
|
241
241
|
home: string;
|
|
242
|
-
}, laneId: string, hqRoot: string, autoCompactTokens?: number, meshCompany?: LaneCompanyRef, workerPolicyDir?: string): NodeJS.ProcessEnv;
|
|
242
|
+
}, laneId: string, hqRoot: string, autoCompactTokens?: number, meshCompany?: LaneCompanyRef, workerPolicyDir?: string, registryWorker?: LaneRecord["registry_worker"]): NodeJS.ProcessEnv;
|
|
243
243
|
export declare const WORKER_POLICY_HOOK_CAPABILITY = "# hq-capability: worker-policy-dir v1";
|
|
244
244
|
/** Grok does not run HQ hooks; Claude and Codex use the installed capability marker. */
|
|
245
245
|
export declare function workerPolicyDeliveryMode(hqRoot: string, provider: LaneProvider): "hook" | "prompt";
|
package/dist/lib/lanes/spawn.js
CHANGED
|
@@ -23,7 +23,8 @@ import { appendEffortFlag, resolveLaneEffort, } from "./effort.js";
|
|
|
23
23
|
import { defaultProbe, validateLaneModel, validateProfileLaneModel, } from "./models.js";
|
|
24
24
|
import { claimableStatesFor, planLaneLaunch, providerSupportsSessionResume, } from "./delivery.js";
|
|
25
25
|
import { AccountResolutionError, LanesError } from "./errors.js";
|
|
26
|
-
import { ignoredWorkerProfileFields, loadWorkerProfile, materializeWorkerProfile, workerProfilePrompt, } from "./profile.js";
|
|
26
|
+
import { ignoredWorkerProfileFields, loadWorkerProfile, loadWorkerProfileWithRegistry, materializeWorkerProfile, workerProfilePrompt, } from "./profile.js";
|
|
27
|
+
import { registryWorkerRecord } from "./registry-persona.js";
|
|
27
28
|
import { resolveMachineIdentity, } from "./machine-identity.js";
|
|
28
29
|
import { appendCappedHistory, listLaneIds, readLane, retryUpdateLane } from "./store.js";
|
|
29
30
|
import { copySessionFiles, planSessionCopy } from "./move-account.js";
|
|
@@ -1236,7 +1237,7 @@ export function buildProviderCommand(provider, input) {
|
|
|
1236
1237
|
args: appendFastModeFlags(parsed, appendEffortFlag(parsed, appendModelFlag(parsed, appendCodexWorkerPolicyDirFlag(parsed, appendProviderProfileFlags(parsed, args, input.profile), input.workerPolicyDir), input.model), input.effort), input.fastMode),
|
|
1237
1238
|
};
|
|
1238
1239
|
}
|
|
1239
|
-
export function workerEnv(base, resolved, laneId, hqRoot, autoCompactTokens, meshCompany, workerPolicyDir) {
|
|
1240
|
+
export function workerEnv(base, resolved, laneId, hqRoot, autoCompactTokens, meshCompany, workerPolicyDir, registryWorker) {
|
|
1240
1241
|
const accepted = Object.values(PROVIDER_HOME_ENV);
|
|
1241
1242
|
if (!accepted.includes(resolved.envVar)) {
|
|
1242
1243
|
throw new LanesError("unknown_provider_home_env", `Unknown provider home variable ${JSON.stringify(resolved.envVar)}. Accepted: ${accepted.join(", ")}`, { accepted, value: resolved.envVar });
|
|
@@ -1245,6 +1246,8 @@ export function workerEnv(base, resolved, laneId, hqRoot, autoCompactTokens, mes
|
|
|
1245
1246
|
delete env.HQ_PROJECT_ID;
|
|
1246
1247
|
delete env.HQ_STORY_ID;
|
|
1247
1248
|
delete env.HQ_POLICY_WORKER_DIR;
|
|
1249
|
+
delete env.HQ_LANE_APPROVAL_REQUIRED;
|
|
1250
|
+
delete env.HQ_LANE_HUMAN_CHECKPOINTS;
|
|
1248
1251
|
env[resolved.envVar] = resolved.home;
|
|
1249
1252
|
delete env[CLAUDE_AUTO_COMPACT_ENV];
|
|
1250
1253
|
if (resolved.envVar === PROVIDER_HOME_ENV.claude && autoCompactTokens !== undefined) {
|
|
@@ -1252,6 +1255,12 @@ export function workerEnv(base, resolved, laneId, hqRoot, autoCompactTokens, mes
|
|
|
1252
1255
|
}
|
|
1253
1256
|
env.HQ_LANE_ID = laneId;
|
|
1254
1257
|
env.HQ_ROOT = hqRoot;
|
|
1258
|
+
if (registryWorker && (registryWorker.approval_required || registryWorker.human_checkpoints.length > 0)) {
|
|
1259
|
+
env.HQ_LANE_APPROVAL_REQUIRED = "1";
|
|
1260
|
+
env.HQ_LANE_HUMAN_CHECKPOINTS = registryWorker.human_checkpoints.length > 0
|
|
1261
|
+
? registryWorker.human_checkpoints.join(",")
|
|
1262
|
+
: "external_publish";
|
|
1263
|
+
}
|
|
1255
1264
|
if (workerPolicyDir !== undefined &&
|
|
1256
1265
|
(resolved.envVar === PROVIDER_HOME_ENV.claude || resolved.envVar === PROVIDER_HOME_ENV.codex)) {
|
|
1257
1266
|
env.HQ_POLICY_WORKER_DIR = workerPolicyDir;
|
|
@@ -1659,7 +1668,12 @@ export function spawnLaneWorker(hqRoot, lane, deps = {}, launch = {}) {
|
|
|
1659
1668
|
ensureDir(logsDir);
|
|
1660
1669
|
ensureDir(path.join(runDir, "envelopes"));
|
|
1661
1670
|
const profile = existing.worker_profile
|
|
1662
|
-
?
|
|
1671
|
+
? loadWorkerProfileWithRegistry(hqRoot, existing.worker_profile, {
|
|
1672
|
+
laneCompany: existing.company?.slug ?? existing.mesh_company?.slug,
|
|
1673
|
+
...(existing.registry_worker !== undefined
|
|
1674
|
+
? { registryWorkerRecord: existing.registry_worker }
|
|
1675
|
+
: {}),
|
|
1676
|
+
})
|
|
1663
1677
|
: undefined;
|
|
1664
1678
|
if (existing.host.kind === "outpost" &&
|
|
1665
1679
|
profile !== undefined &&
|
|
@@ -1827,7 +1841,7 @@ export function spawnLaneWorker(hqRoot, lane, deps = {}, launch = {}) {
|
|
|
1827
1841
|
const autoCompactTokens = lane.provider === "claude"
|
|
1828
1842
|
? providerAutoCompactTokens(config.providers.claude)
|
|
1829
1843
|
: undefined;
|
|
1830
|
-
const childEnv = workerEnv(env, { envVar: resolved.envVar, home: resolved.home }, lane.lane_id, hqRoot, autoCompactTokens, lane.mesh_company ?? lane.company ?? undefined, workerPolicyDir);
|
|
1844
|
+
const childEnv = workerEnv(env, { envVar: resolved.envVar, home: resolved.home }, lane.lane_id, hqRoot, autoCompactTokens, lane.mesh_company ?? lane.company ?? undefined, workerPolicyDir, lane.registry_worker);
|
|
1831
1845
|
const laneTurnId = randomUUID();
|
|
1832
1846
|
childEnv.HQ_LANE_TURN_ID = laneTurnId;
|
|
1833
1847
|
const model = resolveLaneModel({
|
|
@@ -2023,10 +2037,13 @@ export function spawnLaneWorker(hqRoot, lane, deps = {}, launch = {}) {
|
|
|
2023
2037
|
...(profile
|
|
2024
2038
|
? {
|
|
2025
2039
|
worker_profile_hash: profile.contentHash,
|
|
2040
|
+
registry_worker: profile.registryWorker !== undefined
|
|
2041
|
+
? registryWorkerRecord(profile.registryWorker)
|
|
2042
|
+
: undefined,
|
|
2026
2043
|
worker_profile_ignored: [...ignoredProfileFields],
|
|
2027
2044
|
worker_policy_delivery: policyDelivery,
|
|
2028
2045
|
}
|
|
2029
|
-
: { worker_policy_delivery: undefined }),
|
|
2046
|
+
: { registry_worker: undefined, worker_policy_delivery: undefined }),
|
|
2030
2047
|
};
|
|
2031
2048
|
if (resume !== undefined) {
|
|
2032
2049
|
next.provider_session_id = resume.session_id;
|
|
@@ -2240,7 +2257,11 @@ export function spawnEnvelopeRepair(hqRoot, lane, input, deps = {}) {
|
|
|
2240
2257
|
ensureDir(logsDir);
|
|
2241
2258
|
const config = loadLanesConfig(hqRoot);
|
|
2242
2259
|
const profile = lane.worker_profile
|
|
2243
|
-
?
|
|
2260
|
+
? loadWorkerProfileWithRegistry(hqRoot, lane.worker_profile, {
|
|
2261
|
+
laneCompany: lane.company?.slug ?? lane.mesh_company?.slug,
|
|
2262
|
+
...(lane.registry_worker !== undefined
|
|
2263
|
+
? { registryWorkerRecord: lane.registry_worker }
|
|
2264
|
+
: {}),
|
|
2244
2265
|
onUnknownMetaFields: (fields) => {
|
|
2245
2266
|
process.stderr.write(`hq lanes watcher: envelope repair ignored unknown worker profile meta field(s) for ${lane.worker_profile}: ${fields.join(", ")}\n`);
|
|
2246
2267
|
input.onProfileWarning?.(fields);
|
|
@@ -2289,7 +2310,7 @@ export function spawnEnvelopeRepair(hqRoot, lane, input, deps = {}) {
|
|
|
2289
2310
|
const autoCompactTokens = lane.provider === "claude"
|
|
2290
2311
|
? providerAutoCompactTokens(config.providers.claude)
|
|
2291
2312
|
: undefined;
|
|
2292
|
-
const childEnv = workerEnv(env, { envVar: resolved.envVar, home: resolved.home }, lane.lane_id, hqRoot, autoCompactTokens, lane.mesh_company ?? lane.company ?? undefined, workerPolicyDir);
|
|
2313
|
+
const childEnv = workerEnv(env, { envVar: resolved.envVar, home: resolved.home }, lane.lane_id, hqRoot, autoCompactTokens, lane.mesh_company ?? lane.company ?? undefined, workerPolicyDir, lane.registry_worker);
|
|
2293
2314
|
const accountCfg = config.providers[lane.provider].accounts[resolved.id];
|
|
2294
2315
|
const model = resolveLaneModel({
|
|
2295
2316
|
cliModel: pinnedModel,
|