@rallycry/conveyor-agent 10.13.14 → 10.13.16
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/boot-NMXFM4GK.js +1268 -0
- package/dist/boot-NMXFM4GK.js.map +1 -0
- package/dist/{server-TVIXWCAI.js → chunk-5UYMDSAE.js} +8 -3
- package/dist/chunk-5UYMDSAE.js.map +1 -0
- package/dist/chunk-6GS5ADIY.js +11 -0
- package/dist/chunk-6GS5ADIY.js.map +1 -0
- package/dist/{chunk-RSIW2UCR.js → chunk-JG54UJ6V.js} +2 -2
- package/dist/{chunk-VKZ5W2VO.js → chunk-JIGG755T.js} +1 -1
- package/dist/chunk-JIGG755T.js.map +1 -0
- package/dist/{chunk-J4UH5TOO.js → chunk-OW2TIDWZ.js} +71 -114
- package/dist/chunk-OW2TIDWZ.js.map +1 -0
- package/dist/{chunk-LZM5OKAC.js → chunk-UDSRAHKP.js} +19 -2
- package/dist/chunk-UDSRAHKP.js.map +1 -0
- package/dist/chunk-UZTJJD7Y.js +254 -0
- package/dist/chunk-UZTJJD7Y.js.map +1 -0
- package/dist/cli.js +30 -47
- package/dist/cli.js.map +1 -1
- package/dist/{client-VQIBGPE6.js → client-BU4XA7CV.js} +3 -3
- package/dist/index.d.ts +0 -1
- package/dist/index.js +5 -4
- package/dist/index.js.map +1 -1
- package/dist/{protocol-IHRTO5C4.js → protocol-QLVS5W6O.js} +2 -2
- package/dist/server-CO5O7G4U.js +9 -0
- package/dist/server-CO5O7G4U.js.map +1 -0
- package/package.json +1 -1
- package/runtime/entrypoint.sh +8 -975
- package/dist/chunk-J4UH5TOO.js.map +0 -1
- package/dist/chunk-LZM5OKAC.js.map +0 -1
- package/dist/chunk-VKZ5W2VO.js.map +0 -1
- package/dist/server-TVIXWCAI.js.map +0 -1
- /package/dist/{chunk-RSIW2UCR.js.map → chunk-JG54UJ6V.js.map} +0 -0
- /package/dist/{client-VQIBGPE6.js.map → client-BU4XA7CV.js.map} +0 -0
- /package/dist/{protocol-IHRTO5C4.js.map → protocol-QLVS5W6O.js.map} +0 -0
|
@@ -0,0 +1,1268 @@
|
|
|
1
|
+
import {
|
|
2
|
+
TOMBSTONE_MESSAGE,
|
|
3
|
+
isLegacyEntrypointLaunch
|
|
4
|
+
} from "./chunk-6GS5ADIY.js";
|
|
5
|
+
import {
|
|
6
|
+
GitPrepJob,
|
|
7
|
+
defaultGit,
|
|
8
|
+
ensureDir,
|
|
9
|
+
readAgentVersion,
|
|
10
|
+
redactToken
|
|
11
|
+
} from "./chunk-UZTJJD7Y.js";
|
|
12
|
+
import {
|
|
13
|
+
workbenchPort
|
|
14
|
+
} from "./chunk-4VUQ2NPF.js";
|
|
15
|
+
import {
|
|
16
|
+
startWorkbenchServer
|
|
17
|
+
} from "./chunk-5UYMDSAE.js";
|
|
18
|
+
import "./chunk-JG54UJ6V.js";
|
|
19
|
+
import {
|
|
20
|
+
DEFAULT_WORKBENCH_PORT
|
|
21
|
+
} from "./chunk-JIGG755T.js";
|
|
22
|
+
|
|
23
|
+
// src/boot/index.ts
|
|
24
|
+
import { spawn } from "child_process";
|
|
25
|
+
import { statSync as statSync3 } from "fs";
|
|
26
|
+
import { hostname as osHostname } from "os";
|
|
27
|
+
|
|
28
|
+
// src/boot/bundle.ts
|
|
29
|
+
function requireString(source, field) {
|
|
30
|
+
const value = source[field];
|
|
31
|
+
if (typeof value !== "string") {
|
|
32
|
+
throw new Error(`bundle missing field: ${field}`);
|
|
33
|
+
}
|
|
34
|
+
return value;
|
|
35
|
+
}
|
|
36
|
+
function parseGitPlan(raw) {
|
|
37
|
+
if (typeof raw !== "object" || raw === null) {
|
|
38
|
+
throw new Error("bundle missing field: gitPlan");
|
|
39
|
+
}
|
|
40
|
+
const obj = raw;
|
|
41
|
+
const plan = {
|
|
42
|
+
repoOwner: requireString(obj, "repoOwner"),
|
|
43
|
+
repoName: requireString(obj, "repoName"),
|
|
44
|
+
branch: requireString(obj, "branch"),
|
|
45
|
+
baseBranch: requireString(obj, "baseBranch")
|
|
46
|
+
};
|
|
47
|
+
if (typeof obj.checkoutRef === "string") {
|
|
48
|
+
plan.checkoutRef = obj.checkoutRef;
|
|
49
|
+
}
|
|
50
|
+
return plan;
|
|
51
|
+
}
|
|
52
|
+
function parseBundle(text) {
|
|
53
|
+
const raw = JSON.parse(text);
|
|
54
|
+
const envVarsRaw = raw.envVars;
|
|
55
|
+
const envVars = envVarsRaw !== null && envVarsRaw !== void 0 && typeof envVarsRaw === "object" ? envVarsRaw : {};
|
|
56
|
+
const typed = {
|
|
57
|
+
sessionJwt: requireString(raw, "sessionJwt"),
|
|
58
|
+
githubToken: requireString(raw, "githubToken"),
|
|
59
|
+
anthropicKey: requireString(raw, "anthropicKey"),
|
|
60
|
+
gcpToken: requireString(raw, "gcpToken"),
|
|
61
|
+
envVars,
|
|
62
|
+
gitPlan: parseGitPlan(raw.gitPlan)
|
|
63
|
+
};
|
|
64
|
+
return { ...raw, ...typed };
|
|
65
|
+
}
|
|
66
|
+
function decodeJwtClaims(jwt) {
|
|
67
|
+
try {
|
|
68
|
+
const segment = jwt.split(".")[1] ?? "";
|
|
69
|
+
const normalized = segment.replace(/-/g, "+").replace(/_/g, "/");
|
|
70
|
+
const padded = normalized + "=".repeat((4 - normalized.length % 4) % 4);
|
|
71
|
+
const json = Buffer.from(padded, "base64").toString("utf8");
|
|
72
|
+
const parsed = JSON.parse(json);
|
|
73
|
+
if (typeof parsed !== "object" || parsed === null) {
|
|
74
|
+
return {};
|
|
75
|
+
}
|
|
76
|
+
return parsed;
|
|
77
|
+
} catch {
|
|
78
|
+
return {};
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
function resolveModeEnv(claims) {
|
|
82
|
+
if (claims.mode === "review") return { CONVEYOR_MODE: "code-review" };
|
|
83
|
+
if (claims.mode === "pack") return { CONVEYOR_MODE: "pack" };
|
|
84
|
+
if (claims.mode === "adhoc") return { CONVEYOR_MODE: "adhoc" };
|
|
85
|
+
if (!claims.taskId && claims.projectId) return { CONVEYOR_MODE: "pm" };
|
|
86
|
+
return {};
|
|
87
|
+
}
|
|
88
|
+
var ENV_KEY_DENYLIST = /* @__PURE__ */ new Set([
|
|
89
|
+
"PATH",
|
|
90
|
+
"HOME",
|
|
91
|
+
"LD_PRELOAD",
|
|
92
|
+
"SHELL"
|
|
93
|
+
]);
|
|
94
|
+
var IDENTIFIER_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
95
|
+
function buildChildEnv(opts) {
|
|
96
|
+
const { base, bundle, claims, hostname, log } = opts;
|
|
97
|
+
const child = {};
|
|
98
|
+
for (const [k, v] of Object.entries(base)) {
|
|
99
|
+
if (typeof v === "string") child[k] = v;
|
|
100
|
+
}
|
|
101
|
+
for (const [key, value] of Object.entries(bundle.envVars ?? {})) {
|
|
102
|
+
if (!IDENTIFIER_RE.test(key)) {
|
|
103
|
+
log.warn(`[boot] WARN skipping invalid env key from bundle: '${key}'`);
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
if (ENV_KEY_DENYLIST.has(key)) {
|
|
107
|
+
log.warn(`[boot] WARN skipping denylisted env key from bundle: '${key}'`);
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
child[key] = value;
|
|
111
|
+
}
|
|
112
|
+
const setIf = (k, v) => {
|
|
113
|
+
if (v) child[k] = v;
|
|
114
|
+
};
|
|
115
|
+
setIf("CONVEYOR_TASK_TOKEN", bundle.sessionJwt);
|
|
116
|
+
setIf("CONVEYOR_GITHUB_TOKEN", bundle.githubToken);
|
|
117
|
+
setIf("GH_TOKEN", bundle.githubToken);
|
|
118
|
+
setIf("ANTHROPIC_API_KEY", bundle.anthropicKey);
|
|
119
|
+
setIf("CLOUDSDK_AUTH_ACCESS_TOKEN", bundle.gcpToken);
|
|
120
|
+
setIf("REPO_OWNER", bundle.gitPlan.repoOwner);
|
|
121
|
+
setIf("REPO_NAME", bundle.gitPlan.repoName);
|
|
122
|
+
setIf("BRANCH", bundle.gitPlan.branch);
|
|
123
|
+
setIf("CONVEYOR_TASK_ID", claims.taskId);
|
|
124
|
+
setIf("CONVEYOR_PROJECT_ID", claims.projectId);
|
|
125
|
+
setIf("CONVEYOR_SESSION_ID", claims.sessionId);
|
|
126
|
+
setIf("CONVEYOR_WORKSPACE_ID", claims.workspaceId);
|
|
127
|
+
Object.assign(child, resolveModeEnv(claims));
|
|
128
|
+
child.CLAUDESPACE_NAME = hostname;
|
|
129
|
+
child.CONVEYOR_WORKSPACE = opts.workspaceDir ?? "/workspaces/repo";
|
|
130
|
+
child.CONVEYOR_BOOT_VNEXT = "1";
|
|
131
|
+
return child;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// src/boot/graphify.ts
|
|
135
|
+
var USER_HOME_MOUNT = "/mnt/conveyor-users";
|
|
136
|
+
function computeGraphifyEnv(env, repoName, dirExists) {
|
|
137
|
+
if (env.CONVEYOR_GRAPHIFY_DISABLE === "1") {
|
|
138
|
+
return {};
|
|
139
|
+
}
|
|
140
|
+
const slug = env.CONVEYOR_GRAPHIFY_SLUG || repoName;
|
|
141
|
+
if (!slug) {
|
|
142
|
+
return {};
|
|
143
|
+
}
|
|
144
|
+
const primaryRoot = `${USER_HOME_MOUNT}/users/_shared/graphify`;
|
|
145
|
+
const legacyRoot = `${USER_HOME_MOUNT}/_shared/graphify`;
|
|
146
|
+
let sharedRoot = env.CONVEYOR_GRAPHIFY_SHARED_ROOT;
|
|
147
|
+
if (!sharedRoot) {
|
|
148
|
+
sharedRoot = dirExists(primaryRoot) || !dirExists(legacyRoot) ? primaryRoot : legacyRoot;
|
|
149
|
+
}
|
|
150
|
+
const dir = env.CONVEYOR_GRAPHIFY_DIR || `${sharedRoot}/${slug}/latest`;
|
|
151
|
+
const graph = env.CONVEYOR_GRAPHIFY_GRAPH || `${dir}/graph.json`;
|
|
152
|
+
return {
|
|
153
|
+
CONVEYOR_GRAPHIFY_SLUG: slug,
|
|
154
|
+
CONVEYOR_GRAPHIFY_SHARED_ROOT: sharedRoot,
|
|
155
|
+
CONVEYOR_GRAPHIFY_DIR: dir,
|
|
156
|
+
CONVEYOR_GRAPHIFY_GRAPH: graph
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// src/boot/logger.ts
|
|
161
|
+
import { appendFileSync } from "fs";
|
|
162
|
+
var BOOTSTRAP_LOG_PATH = "/tmp/claudespace-bootstrap.log";
|
|
163
|
+
function createBootLogger(filePath = BOOTSTRAP_LOG_PATH) {
|
|
164
|
+
const emit = (line) => {
|
|
165
|
+
process.stdout.write(`${line}
|
|
166
|
+
`);
|
|
167
|
+
if (filePath) {
|
|
168
|
+
try {
|
|
169
|
+
appendFileSync(filePath, `${line}
|
|
170
|
+
`);
|
|
171
|
+
} catch {
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
};
|
|
175
|
+
return { info: emit, warn: emit, error: emit };
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// src/boot/types.ts
|
|
179
|
+
var BootFatalError = class extends Error {
|
|
180
|
+
constructor(step, message, options) {
|
|
181
|
+
super(`[boot] FATAL step=${step}: ${message}`, options);
|
|
182
|
+
this.step = step;
|
|
183
|
+
this.name = "BootFatalError";
|
|
184
|
+
}
|
|
185
|
+
step;
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
// src/boot/pipeline.ts
|
|
189
|
+
async function runPipeline(steps, ctx) {
|
|
190
|
+
const t0 = Date.now();
|
|
191
|
+
const timings = [];
|
|
192
|
+
for (const step of steps) {
|
|
193
|
+
const s0 = Date.now();
|
|
194
|
+
try {
|
|
195
|
+
await step.run(ctx);
|
|
196
|
+
} catch (err) {
|
|
197
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
198
|
+
if (step.critical) {
|
|
199
|
+
ctx.log.error(`[boot] FATAL step=${step.name}: ${msg}`);
|
|
200
|
+
throw err instanceof BootFatalError ? err : new BootFatalError(step.name, msg, { cause: err });
|
|
201
|
+
}
|
|
202
|
+
ctx.log.warn(`[boot] WARN step=${step.name} degraded: ${msg}`);
|
|
203
|
+
}
|
|
204
|
+
const ms = Date.now() - s0;
|
|
205
|
+
timings.push(`${step.name}:${ms}`);
|
|
206
|
+
ctx.log.info(`[boot] step=${step.name} ms=${ms}`);
|
|
207
|
+
}
|
|
208
|
+
ctx.log.info(
|
|
209
|
+
`[boot] timeline role=${ctx.role} total=${Date.now() - t0} steps=${timings.join(",")}`
|
|
210
|
+
);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// src/boot/poll.ts
|
|
214
|
+
var BootstrapUnauthorizedError = class extends Error {
|
|
215
|
+
constructor(message) {
|
|
216
|
+
super(message);
|
|
217
|
+
this.name = "BootstrapUnauthorizedError";
|
|
218
|
+
}
|
|
219
|
+
};
|
|
220
|
+
var PROGRESS_LOG_EVERY = 30;
|
|
221
|
+
var DEFAULT_POLL_INTERVAL_MS = 2e3;
|
|
222
|
+
function sleepUnlessAborted(sleepFn, ms, signal) {
|
|
223
|
+
if (!signal) return sleepFn(ms);
|
|
224
|
+
if (signal.aborted) return Promise.resolve();
|
|
225
|
+
return new Promise((resolve) => {
|
|
226
|
+
const onAbort = () => {
|
|
227
|
+
resolve();
|
|
228
|
+
};
|
|
229
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
230
|
+
void sleepFn(ms).then(() => {
|
|
231
|
+
signal.removeEventListener("abort", onAbort);
|
|
232
|
+
resolve();
|
|
233
|
+
});
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
async function pollBootstrapBundle(apiUrl, token, deps) {
|
|
237
|
+
const interval = deps.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;
|
|
238
|
+
let pollCount = 0;
|
|
239
|
+
while (true) {
|
|
240
|
+
if (deps.signal?.aborted) return null;
|
|
241
|
+
let res;
|
|
242
|
+
try {
|
|
243
|
+
res = await deps.fetchFn(`${apiUrl}/api/v3/pods/bootstrap`, {
|
|
244
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
245
|
+
signal: deps.signal
|
|
246
|
+
});
|
|
247
|
+
} catch {
|
|
248
|
+
if (deps.signal?.aborted) return null;
|
|
249
|
+
res = void 0;
|
|
250
|
+
}
|
|
251
|
+
if (res?.status === 200) {
|
|
252
|
+
return parseBundle(await res.text());
|
|
253
|
+
}
|
|
254
|
+
if (res?.status === 401) {
|
|
255
|
+
throw new BootstrapUnauthorizedError("bootstrap token rejected (401) \u2014 pod identity invalid");
|
|
256
|
+
}
|
|
257
|
+
pollCount++;
|
|
258
|
+
if (pollCount % PROGRESS_LOG_EVERY === 0) {
|
|
259
|
+
deps.log.info(`[boot] Still waiting for bind (poll #${pollCount})...`);
|
|
260
|
+
}
|
|
261
|
+
await sleepUnlessAborted(deps.sleepFn, interval, deps.signal);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// src/boot/self-update.ts
|
|
266
|
+
import { execFile } from "child_process";
|
|
267
|
+
import { promisify } from "util";
|
|
268
|
+
var AGENT_PACKAGE = "@rallycry/conveyor-agent";
|
|
269
|
+
var NPM_VIEW_TIMEOUT_MS = 3e3;
|
|
270
|
+
var NPM_INSTALL_TIMEOUT_MS = 12e4;
|
|
271
|
+
var NPM_CACHE_DIR = "/tmp/npm-cache";
|
|
272
|
+
var NPM_REGISTRY_ARG = "--@rallycry:registry=https://registry.npmjs.org/";
|
|
273
|
+
var SEMVER_RE = /^\d+\.\d+\.\d+([-+][A-Za-z0-9.-]+)?$/;
|
|
274
|
+
function isValidSemver(value) {
|
|
275
|
+
return SEMVER_RE.test(value);
|
|
276
|
+
}
|
|
277
|
+
function isStrictlyNewer(candidate, installed) {
|
|
278
|
+
const num = (v) => (v.split(/[-+]/)[0] ?? "").split(".").map((p) => Number(p));
|
|
279
|
+
const [a, b] = [num(candidate), num(installed)];
|
|
280
|
+
for (let i = 0; i < 3; i++) {
|
|
281
|
+
const av = a[i] ?? 0;
|
|
282
|
+
const bv = b[i] ?? 0;
|
|
283
|
+
if (av !== bv) return av > bv;
|
|
284
|
+
}
|
|
285
|
+
return false;
|
|
286
|
+
}
|
|
287
|
+
var execFileAsync = promisify(execFile);
|
|
288
|
+
function defaultExec(cmd, args, opts) {
|
|
289
|
+
return execFileAsync(cmd, args, { timeout: opts.timeoutMs });
|
|
290
|
+
}
|
|
291
|
+
async function runPreflightUpdate(deps) {
|
|
292
|
+
let latest = "";
|
|
293
|
+
try {
|
|
294
|
+
const { stdout } = await deps.exec(
|
|
295
|
+
"npm",
|
|
296
|
+
["view", "--cache", NPM_CACHE_DIR, NPM_REGISTRY_ARG, AGENT_PACKAGE, "version"],
|
|
297
|
+
{ timeoutMs: NPM_VIEW_TIMEOUT_MS }
|
|
298
|
+
);
|
|
299
|
+
latest = stdout.replace(/\s+/g, "");
|
|
300
|
+
} catch (err) {
|
|
301
|
+
deps.log.warn(
|
|
302
|
+
`[boot] WARN preflight npm view failed (${err instanceof Error ? err.message : err}); keeping baked version`
|
|
303
|
+
);
|
|
304
|
+
return "skipped";
|
|
305
|
+
}
|
|
306
|
+
if (!isValidSemver(latest)) {
|
|
307
|
+
deps.log.warn(
|
|
308
|
+
`[boot] WARN preflight got non-semver '${latest}' from npm; keeping baked version`
|
|
309
|
+
);
|
|
310
|
+
return "skipped";
|
|
311
|
+
}
|
|
312
|
+
let installed = "";
|
|
313
|
+
try {
|
|
314
|
+
installed = (await deps.installedVersion()).trim();
|
|
315
|
+
} catch (err) {
|
|
316
|
+
deps.log.warn(
|
|
317
|
+
`[boot] WARN preflight installed-version lookup failed (${err instanceof Error ? err.message : err}); keeping baked version`
|
|
318
|
+
);
|
|
319
|
+
return "skipped";
|
|
320
|
+
}
|
|
321
|
+
if (!isValidSemver(installed) || isStrictlyNewer(latest, installed)) {
|
|
322
|
+
deps.log.info(
|
|
323
|
+
`[boot] preflight updating ${AGENT_PACKAGE} ${installed || "unknown"} -> ${latest}`
|
|
324
|
+
);
|
|
325
|
+
try {
|
|
326
|
+
await deps.exec(
|
|
327
|
+
"sudo",
|
|
328
|
+
[
|
|
329
|
+
"npm",
|
|
330
|
+
"install",
|
|
331
|
+
"-g",
|
|
332
|
+
"--cache",
|
|
333
|
+
NPM_CACHE_DIR,
|
|
334
|
+
"--silent",
|
|
335
|
+
`${AGENT_PACKAGE}@${latest}`
|
|
336
|
+
],
|
|
337
|
+
{ timeoutMs: NPM_INSTALL_TIMEOUT_MS }
|
|
338
|
+
);
|
|
339
|
+
return "updated";
|
|
340
|
+
} catch (err) {
|
|
341
|
+
deps.log.warn(
|
|
342
|
+
`[boot] WARN preflight install failed (${err instanceof Error ? err.message : err}); keeping baked version`
|
|
343
|
+
);
|
|
344
|
+
return "skipped";
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
deps.log.info(`[boot] preflight agent ${installed} is current (registry ${latest})`);
|
|
348
|
+
return "current";
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// src/boot/supervisor.ts
|
|
352
|
+
import { spawn as spawnChildProcess } from "child_process";
|
|
353
|
+
import { appendFileSync as appendFileSync2, writeFileSync } from "fs";
|
|
354
|
+
import { join } from "path";
|
|
355
|
+
|
|
356
|
+
// src/boot/crash-report.ts
|
|
357
|
+
var CRASH_REPORT_TIMEOUT_MS = 1e4;
|
|
358
|
+
async function reportAgentCrash(apiUrl, bootstrapToken, report, fetchFn = fetch) {
|
|
359
|
+
const controller = new AbortController();
|
|
360
|
+
const timer = setTimeout(() => controller.abort(), CRASH_REPORT_TIMEOUT_MS);
|
|
361
|
+
try {
|
|
362
|
+
await fetchFn(`${apiUrl}/api/v3/pods/agent-crash`, {
|
|
363
|
+
method: "POST",
|
|
364
|
+
headers: {
|
|
365
|
+
"content-type": "application/json",
|
|
366
|
+
authorization: `Bearer ${bootstrapToken}`
|
|
367
|
+
},
|
|
368
|
+
body: JSON.stringify(report),
|
|
369
|
+
signal: controller.signal
|
|
370
|
+
});
|
|
371
|
+
} catch {
|
|
372
|
+
} finally {
|
|
373
|
+
clearTimeout(timer);
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// src/boot/supervisor.ts
|
|
378
|
+
var AGENT_CRASH_MAX = 3;
|
|
379
|
+
var AGENT_LOG_PATH = "/tmp/claudespace-agent.log";
|
|
380
|
+
var LAUNCH_MARKER_NAME = "conveyor-agent-launching";
|
|
381
|
+
var RESTARTING_MARKER_NAME = "conveyor-agent-restarting";
|
|
382
|
+
var LAUNCH_MARKER = `/tmp/${LAUNCH_MARKER_NAME}`;
|
|
383
|
+
var RESTARTING_MARKER = `/tmp/${RESTARTING_MARKER_NAME}`;
|
|
384
|
+
var DEFAULT_CRASH_BACKOFF_MS = 1e4;
|
|
385
|
+
var DEFAULT_DRAIN_TIMEOUT_MS = 45e3;
|
|
386
|
+
var DEFAULT_MARKER_DIR = "/tmp";
|
|
387
|
+
function touchMarker(path, log) {
|
|
388
|
+
try {
|
|
389
|
+
writeFileSync(path, "");
|
|
390
|
+
} catch (err) {
|
|
391
|
+
log.warn(
|
|
392
|
+
`[boot] WARN failed to write liveness marker ${path}: ${err instanceof Error ? err.message : String(err)}`
|
|
393
|
+
);
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
function teeChildOutput(child) {
|
|
397
|
+
const onData = (chunk) => {
|
|
398
|
+
process.stdout.write(chunk);
|
|
399
|
+
try {
|
|
400
|
+
appendFileSync2(AGENT_LOG_PATH, chunk);
|
|
401
|
+
} catch {
|
|
402
|
+
}
|
|
403
|
+
};
|
|
404
|
+
child.stdout?.on("data", onData);
|
|
405
|
+
child.stderr?.on("data", onData);
|
|
406
|
+
}
|
|
407
|
+
function spawnAgent(spawnFn, opts) {
|
|
408
|
+
return spawnFn("conveyor-agent", [], {
|
|
409
|
+
cwd: opts.cwd,
|
|
410
|
+
env: opts.childEnv,
|
|
411
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
412
|
+
});
|
|
413
|
+
}
|
|
414
|
+
function waitForExit(child) {
|
|
415
|
+
return new Promise((resolve) => {
|
|
416
|
+
child.once("exit", (code, signal) => {
|
|
417
|
+
resolve(code ?? (signal ? 1 : 0));
|
|
418
|
+
});
|
|
419
|
+
child.once("error", () => resolve(1));
|
|
420
|
+
});
|
|
421
|
+
}
|
|
422
|
+
function whenAborted(signal) {
|
|
423
|
+
return new Promise((resolve) => {
|
|
424
|
+
if (!signal) return;
|
|
425
|
+
if (signal.aborted) {
|
|
426
|
+
resolve();
|
|
427
|
+
return;
|
|
428
|
+
}
|
|
429
|
+
signal.addEventListener("abort", () => resolve(), { once: true });
|
|
430
|
+
});
|
|
431
|
+
}
|
|
432
|
+
function sleepUnlessAborted2(ms, signal) {
|
|
433
|
+
if (!signal) {
|
|
434
|
+
return new Promise((resolve) => {
|
|
435
|
+
setTimeout(resolve, ms);
|
|
436
|
+
});
|
|
437
|
+
}
|
|
438
|
+
if (signal.aborted) return Promise.resolve();
|
|
439
|
+
return new Promise((resolve) => {
|
|
440
|
+
const onAbort = () => {
|
|
441
|
+
clearTimeout(timer);
|
|
442
|
+
resolve();
|
|
443
|
+
};
|
|
444
|
+
const timer = setTimeout(() => {
|
|
445
|
+
signal.removeEventListener("abort", onAbort);
|
|
446
|
+
resolve();
|
|
447
|
+
}, ms);
|
|
448
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
449
|
+
});
|
|
450
|
+
}
|
|
451
|
+
async function drainChild(child, exitPromise, drainTimeoutMs, log) {
|
|
452
|
+
log.info("[boot] SIGTERM received, draining agent...");
|
|
453
|
+
child.kill("SIGTERM");
|
|
454
|
+
const TIMED_OUT = /* @__PURE__ */ Symbol("drain-timeout");
|
|
455
|
+
let timer;
|
|
456
|
+
const timeout = new Promise((resolve) => {
|
|
457
|
+
timer = setTimeout(() => resolve(TIMED_OUT), drainTimeoutMs);
|
|
458
|
+
});
|
|
459
|
+
const winner = await Promise.race([exitPromise, timeout]);
|
|
460
|
+
if (timer) clearTimeout(timer);
|
|
461
|
+
if (winner === TIMED_OUT) {
|
|
462
|
+
log.warn(`[boot] agent did not exit within drainTimeoutMs=${drainTimeoutMs}ms \u2014 SIGKILL`);
|
|
463
|
+
child.kill("SIGKILL");
|
|
464
|
+
await exitPromise;
|
|
465
|
+
}
|
|
466
|
+
return 0;
|
|
467
|
+
}
|
|
468
|
+
async function superviseRunner(opts) {
|
|
469
|
+
const spawnFn = opts.spawnFn ?? spawnChildProcess;
|
|
470
|
+
const reportFn = opts.reportFn ?? reportAgentCrash;
|
|
471
|
+
const crashBackoffMs = opts.crashBackoffMs ?? DEFAULT_CRASH_BACKOFF_MS;
|
|
472
|
+
const drainTimeoutMs = opts.drainTimeoutMs ?? DEFAULT_DRAIN_TIMEOUT_MS;
|
|
473
|
+
const markerDir = opts.markerDir ?? DEFAULT_MARKER_DIR;
|
|
474
|
+
const launchMarkerPath = join(markerDir, LAUNCH_MARKER_NAME);
|
|
475
|
+
const restartingMarkerPath = join(markerDir, RESTARTING_MARKER_NAME);
|
|
476
|
+
touchMarker(launchMarkerPath, opts.log);
|
|
477
|
+
let attempt = 0;
|
|
478
|
+
while (true) {
|
|
479
|
+
attempt++;
|
|
480
|
+
opts.log.info("[boot] Launching agent...");
|
|
481
|
+
const child = spawnAgent(spawnFn, opts);
|
|
482
|
+
teeChildOutput(child);
|
|
483
|
+
const exitPromise = waitForExit(child);
|
|
484
|
+
const winner = await Promise.race([
|
|
485
|
+
exitPromise.then((code) => ({ type: "exit", code })),
|
|
486
|
+
whenAborted(opts.signal).then(() => ({ type: "abort" }))
|
|
487
|
+
]);
|
|
488
|
+
if (winner.type === "abort") {
|
|
489
|
+
return await drainChild(child, exitPromise, drainTimeoutMs, opts.log);
|
|
490
|
+
}
|
|
491
|
+
const exitCode = winner.code;
|
|
492
|
+
if (exitCode === 0) {
|
|
493
|
+
opts.log.info("[boot] agent exited cleanly (code 0), shutting down pod.");
|
|
494
|
+
return 0;
|
|
495
|
+
}
|
|
496
|
+
if (attempt >= AGENT_CRASH_MAX) {
|
|
497
|
+
opts.log.error(
|
|
498
|
+
`[boot] agent crashed (code ${exitCode}) \u2014 attempt cap ${AGENT_CRASH_MAX} reached, giving up.`
|
|
499
|
+
);
|
|
500
|
+
await reportFn(opts.apiUrl, opts.bootstrapToken, { exitCode, attempt, final: true });
|
|
501
|
+
return 1;
|
|
502
|
+
}
|
|
503
|
+
touchMarker(restartingMarkerPath, opts.log);
|
|
504
|
+
await reportFn(opts.apiUrl, opts.bootstrapToken, { exitCode, attempt, final: false });
|
|
505
|
+
opts.log.warn(
|
|
506
|
+
`[boot] agent crashed (code ${exitCode}), retrying in ${crashBackoffMs}ms (attempt ${attempt}/${AGENT_CRASH_MAX})...`
|
|
507
|
+
);
|
|
508
|
+
await sleepUnlessAborted2(crashBackoffMs, opts.signal);
|
|
509
|
+
if (opts.signal?.aborted) {
|
|
510
|
+
return 0;
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
// src/boot/user-home.ts
|
|
516
|
+
import {
|
|
517
|
+
chmodSync,
|
|
518
|
+
copyFileSync,
|
|
519
|
+
lstatSync,
|
|
520
|
+
mkdirSync,
|
|
521
|
+
readFileSync,
|
|
522
|
+
rmSync,
|
|
523
|
+
statSync,
|
|
524
|
+
symlinkSync,
|
|
525
|
+
writeFileSync as writeFileSync2
|
|
526
|
+
} from "fs";
|
|
527
|
+
import { dirname, join as join2 } from "path";
|
|
528
|
+
var SEED_GATES = {
|
|
529
|
+
hasCompletedOnboarding: true,
|
|
530
|
+
bypassPermissionsModeAccepted: true,
|
|
531
|
+
projects: {
|
|
532
|
+
"/workspaces/repo": { hasTrustDialogAccepted: true }
|
|
533
|
+
}
|
|
534
|
+
};
|
|
535
|
+
var SEED_FABLE_OPTION = {
|
|
536
|
+
value: "claude-fable-5[1m]",
|
|
537
|
+
label: "Fable",
|
|
538
|
+
description: "Fable 5 - most capable for your hardest and longest-running tasks"
|
|
539
|
+
};
|
|
540
|
+
function isPlainObject(value) {
|
|
541
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
542
|
+
}
|
|
543
|
+
function deepMerge(target, patch) {
|
|
544
|
+
const out = { ...target };
|
|
545
|
+
for (const [k, v] of Object.entries(patch)) {
|
|
546
|
+
const cur = out[k];
|
|
547
|
+
out[k] = isPlainObject(cur) && isPlainObject(v) ? deepMerge(cur, v) : v;
|
|
548
|
+
}
|
|
549
|
+
return out;
|
|
550
|
+
}
|
|
551
|
+
function hasFableEntry(cache) {
|
|
552
|
+
return cache.some((entry) => {
|
|
553
|
+
if (!isPlainObject(entry)) return false;
|
|
554
|
+
const value = entry.value;
|
|
555
|
+
return typeof value === "string" && value.toLowerCase().includes("fable");
|
|
556
|
+
});
|
|
557
|
+
}
|
|
558
|
+
function seedClaudeJson(targetPath) {
|
|
559
|
+
try {
|
|
560
|
+
let config = null;
|
|
561
|
+
try {
|
|
562
|
+
const parsed = JSON.parse(readFileSync(targetPath, "utf8"));
|
|
563
|
+
if (isPlainObject(parsed)) config = parsed;
|
|
564
|
+
} catch {
|
|
565
|
+
}
|
|
566
|
+
let merged = config ? deepMerge(config, SEED_GATES) : { ...SEED_GATES, theme: "dark" };
|
|
567
|
+
const cache = Array.isArray(merged.additionalModelOptionsCache) ? merged.additionalModelOptionsCache : [];
|
|
568
|
+
if (!hasFableEntry(cache)) {
|
|
569
|
+
merged = { ...merged, additionalModelOptionsCache: [...cache, SEED_FABLE_OPTION] };
|
|
570
|
+
}
|
|
571
|
+
writeFileSync2(targetPath, JSON.stringify(merged));
|
|
572
|
+
} catch {
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
function isDir(path) {
|
|
576
|
+
try {
|
|
577
|
+
return statSync(path).isDirectory();
|
|
578
|
+
} catch {
|
|
579
|
+
return false;
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
function isRegularFile(path) {
|
|
583
|
+
try {
|
|
584
|
+
return lstatSync(path).isFile();
|
|
585
|
+
} catch {
|
|
586
|
+
return false;
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
function safely(log, label, fn) {
|
|
590
|
+
try {
|
|
591
|
+
fn();
|
|
592
|
+
} catch (err) {
|
|
593
|
+
log.warn(`[boot] WARN ${label} failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
function relink(target, link) {
|
|
597
|
+
mkdirSync(dirname(link), { recursive: true });
|
|
598
|
+
rmSync(link, { recursive: true, force: true });
|
|
599
|
+
symlinkSync(target, link);
|
|
600
|
+
}
|
|
601
|
+
function wireCredentials(opts) {
|
|
602
|
+
const { home, root, sharedDir, log } = opts;
|
|
603
|
+
const shared = join2(root, ".claude", ".credentials.json");
|
|
604
|
+
const podLocal = sharedDir ? join2(sharedDir, "claude-credentials.pod.json") : join2(home, ".claude-credentials.pod.json");
|
|
605
|
+
safely(log, "carry legacy shared credentials", () => {
|
|
606
|
+
if (isRegularFile(shared)) copyFileSync(shared, podLocal);
|
|
607
|
+
});
|
|
608
|
+
safely(log, "repoint credentials symlink", () => relink(podLocal, shared));
|
|
609
|
+
safely(log, "chmod pod-local credentials", () => chmodSync(podLocal, 384));
|
|
610
|
+
}
|
|
611
|
+
function wireMountedUserHome(opts) {
|
|
612
|
+
const { home, userHomeMount, userId, projectId, sharedDir, log } = opts;
|
|
613
|
+
const root = join2(userHomeMount, "users", userId, projectId);
|
|
614
|
+
safely(log, "mkdir user-home root", () => {
|
|
615
|
+
mkdirSync(join2(root, ".claude"), { recursive: true });
|
|
616
|
+
mkdirSync(join2(root, ".config", "claude"), { recursive: true });
|
|
617
|
+
});
|
|
618
|
+
safely(log, "wire ~/.claude symlink", () => relink(join2(root, ".claude"), join2(home, ".claude")));
|
|
619
|
+
safely(
|
|
620
|
+
log,
|
|
621
|
+
"wire ~/.config/claude symlink",
|
|
622
|
+
() => relink(join2(root, ".config", "claude"), join2(home, ".config", "claude"))
|
|
623
|
+
);
|
|
624
|
+
safely(log, "seed pod-local ~/.claude.json", () => {
|
|
625
|
+
rmSync(join2(home, ".claude.json"), { recursive: true, force: true });
|
|
626
|
+
seedClaudeJson(join2(home, ".claude.json"));
|
|
627
|
+
});
|
|
628
|
+
wireCredentials({ home, root, sharedDir, log });
|
|
629
|
+
safely(log, "mkdir opencode root", () => {
|
|
630
|
+
mkdirSync(join2(root, ".local", "share", "opencode"), { recursive: true });
|
|
631
|
+
mkdirSync(join2(root, ".config", "opencode"), { recursive: true });
|
|
632
|
+
});
|
|
633
|
+
safely(
|
|
634
|
+
log,
|
|
635
|
+
"wire opencode data symlink",
|
|
636
|
+
() => relink(join2(root, ".local", "share", "opencode"), join2(home, ".local", "share", "opencode"))
|
|
637
|
+
);
|
|
638
|
+
safely(
|
|
639
|
+
log,
|
|
640
|
+
"wire opencode config symlink",
|
|
641
|
+
() => relink(join2(root, ".config", "opencode"), join2(home, ".config", "opencode"))
|
|
642
|
+
);
|
|
643
|
+
}
|
|
644
|
+
function wireNoMountUserHome(opts) {
|
|
645
|
+
const { home, sharedDir, log } = opts;
|
|
646
|
+
safely(
|
|
647
|
+
log,
|
|
648
|
+
"seed pod-local ~/.claude.json (no mount)",
|
|
649
|
+
() => seedClaudeJson(join2(home, ".claude.json"))
|
|
650
|
+
);
|
|
651
|
+
if (sharedDir) {
|
|
652
|
+
safely(log, "wire ~/.claude to shared emptyDir", () => {
|
|
653
|
+
const target = join2(sharedDir, "claude-home", ".claude");
|
|
654
|
+
mkdirSync(target, { recursive: true });
|
|
655
|
+
relink(target, join2(home, ".claude"));
|
|
656
|
+
});
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
function wireUserHome(opts) {
|
|
660
|
+
const { home, userHomeMount, userId, projectId, sharedDir, log } = opts;
|
|
661
|
+
if (userId && projectId && isDir(userHomeMount)) {
|
|
662
|
+
wireMountedUserHome({ home, userHomeMount, userId, projectId, sharedDir, log });
|
|
663
|
+
} else {
|
|
664
|
+
wireNoMountUserHome({ home, sharedDir, log });
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
var RECLAIM_FIND_TIMEOUT_MS = 1e4;
|
|
668
|
+
async function reclaimHomeOwnership(home, exec, log) {
|
|
669
|
+
for (const dir of [join2(home, ".config"), join2(home, ".local")]) {
|
|
670
|
+
if (!isDir(dir)) continue;
|
|
671
|
+
try {
|
|
672
|
+
await exec(
|
|
673
|
+
"sudo",
|
|
674
|
+
[
|
|
675
|
+
"-n",
|
|
676
|
+
"find",
|
|
677
|
+
dir,
|
|
678
|
+
"!",
|
|
679
|
+
"-user",
|
|
680
|
+
"conveyor",
|
|
681
|
+
"-exec",
|
|
682
|
+
"chown",
|
|
683
|
+
"conveyor:conveyor",
|
|
684
|
+
"{}",
|
|
685
|
+
"+"
|
|
686
|
+
],
|
|
687
|
+
{ timeoutMs: RECLAIM_FIND_TIMEOUT_MS }
|
|
688
|
+
);
|
|
689
|
+
} catch (err) {
|
|
690
|
+
log.warn(
|
|
691
|
+
`[boot] WARN reclaim_home_ownership failed for ${dir}: ${err instanceof Error ? err.message : String(err)}`
|
|
692
|
+
);
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
// src/boot/sshd.ts
|
|
698
|
+
import { chmodSync as chmodSync2, closeSync, existsSync, mkdirSync as mkdirSync2, openSync } from "fs";
|
|
699
|
+
import { join as join3 } from "path";
|
|
700
|
+
async function startWorkspaceSshd(exec, log, port = process.env.CONVEYOR_WORKSPACE_SSH_PORT || "2222", home = "/home/conveyor") {
|
|
701
|
+
try {
|
|
702
|
+
const sshDir = join3(home, ".ssh");
|
|
703
|
+
mkdirSync2(sshDir, { recursive: true });
|
|
704
|
+
chmodSync2(sshDir, 448);
|
|
705
|
+
const authorizedKeys = join3(sshDir, "authorized_keys");
|
|
706
|
+
if (!existsSync(authorizedKeys)) closeSync(openSync(authorizedKeys, "a"));
|
|
707
|
+
chmodSync2(authorizedKeys, 384);
|
|
708
|
+
} catch (err) {
|
|
709
|
+
log.warn(
|
|
710
|
+
`[boot] WARN: workspace SSHD key setup failed: ${err instanceof Error ? err.message : String(err)}`
|
|
711
|
+
);
|
|
712
|
+
return;
|
|
713
|
+
}
|
|
714
|
+
if (!existsSync("/usr/sbin/sshd")) {
|
|
715
|
+
log.warn("[boot] WARN: workspace SSHD unavailable");
|
|
716
|
+
return;
|
|
717
|
+
}
|
|
718
|
+
try {
|
|
719
|
+
await exec("sudo", ["mkdir", "-p", "/run/sshd"], { timeoutMs: 1e4 });
|
|
720
|
+
await exec("sudo", ["/usr/sbin/sshd", "-o", "ListenAddress=0.0.0.0", "-o", `Port=${port}`], {
|
|
721
|
+
timeoutMs: 1e4
|
|
722
|
+
});
|
|
723
|
+
log.info(`[boot] Workspace SSHD listening on 0.0.0.0:${port}`);
|
|
724
|
+
} catch {
|
|
725
|
+
log.warn("[boot] WARN: workspace SSHD failed to start");
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
// src/boot/workspace-extras.ts
|
|
730
|
+
import {
|
|
731
|
+
existsSync as existsSync2,
|
|
732
|
+
lstatSync as lstatSync2,
|
|
733
|
+
mkdirSync as mkdirSync3,
|
|
734
|
+
readdirSync,
|
|
735
|
+
rmSync as rmSync2,
|
|
736
|
+
statSync as statSync2,
|
|
737
|
+
symlinkSync as symlinkSync2
|
|
738
|
+
} from "fs";
|
|
739
|
+
import { join as join4 } from "path";
|
|
740
|
+
function errText(err) {
|
|
741
|
+
return redactToken(err instanceof Error ? err.message : String(err));
|
|
742
|
+
}
|
|
743
|
+
function isDir2(path) {
|
|
744
|
+
try {
|
|
745
|
+
return statSync2(path).isDirectory();
|
|
746
|
+
} catch {
|
|
747
|
+
return false;
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
function isSymlink(path) {
|
|
751
|
+
try {
|
|
752
|
+
return lstatSync2(path).isSymbolicLink();
|
|
753
|
+
} catch {
|
|
754
|
+
return false;
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
var GRAPHIFY_BUNDLE_FILES = [
|
|
758
|
+
"graph.json",
|
|
759
|
+
"GRAPH_REPORT.md",
|
|
760
|
+
"manifest.json",
|
|
761
|
+
".graphify_analysis.json",
|
|
762
|
+
".graphify_labels.json",
|
|
763
|
+
"publish-manifest.json",
|
|
764
|
+
"cost.json"
|
|
765
|
+
];
|
|
766
|
+
function bindGraphifyBundle(workspace, graphifyEnv, log) {
|
|
767
|
+
const slug = graphifyEnv.CONVEYOR_GRAPHIFY_SLUG;
|
|
768
|
+
const sourceDir = graphifyEnv.CONVEYOR_GRAPHIFY_DIR;
|
|
769
|
+
const graphFile = graphifyEnv.CONVEYOR_GRAPHIFY_GRAPH;
|
|
770
|
+
if (!slug || !sourceDir || !graphFile) return;
|
|
771
|
+
if (!existsSync2(graphFile)) {
|
|
772
|
+
log.info(`[boot] Graphify bundle not found for '${slug}' at ${sourceDir}`);
|
|
773
|
+
return;
|
|
774
|
+
}
|
|
775
|
+
const targetDir = join4(workspace, "graphify-out");
|
|
776
|
+
try {
|
|
777
|
+
mkdirSync3(targetDir, { recursive: true });
|
|
778
|
+
} catch {
|
|
779
|
+
log.warn(`[boot] WARN: unable to create graphify-out at ${targetDir}`);
|
|
780
|
+
return;
|
|
781
|
+
}
|
|
782
|
+
for (const rel of GRAPHIFY_BUNDLE_FILES) {
|
|
783
|
+
const source = join4(sourceDir, rel);
|
|
784
|
+
const target = join4(targetDir, rel);
|
|
785
|
+
if (!existsSync2(source)) continue;
|
|
786
|
+
if (existsSync2(target) || isSymlink(target)) continue;
|
|
787
|
+
try {
|
|
788
|
+
symlinkSync2(source, target);
|
|
789
|
+
} catch {
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
log.info(`[boot] Bound graphify bundle '${slug}' into ${targetDir}`);
|
|
793
|
+
}
|
|
794
|
+
async function ensureGrimoireSubmodule(workspace, githubToken, git, log) {
|
|
795
|
+
try {
|
|
796
|
+
if (!existsSync2(join4(workspace, ".gitmodules"))) return;
|
|
797
|
+
let paths = "";
|
|
798
|
+
try {
|
|
799
|
+
({ stdout: paths } = await git(
|
|
800
|
+
["config", "--file", ".gitmodules", "--get-regexp", String.raw`submodule\..*\.path`],
|
|
801
|
+
{ cwd: workspace, timeoutMs: 1e4 }
|
|
802
|
+
));
|
|
803
|
+
} catch {
|
|
804
|
+
return;
|
|
805
|
+
}
|
|
806
|
+
if (!paths.split("\n").some((line) => line.trim().endsWith(".claude/grimoire"))) return;
|
|
807
|
+
if (isDir2(join4(workspace, ".claude", "grimoire", "skills"))) return;
|
|
808
|
+
if (!githubToken) {
|
|
809
|
+
log.warn("[boot] WARN: grimoire submodule absent and no token to fetch it");
|
|
810
|
+
return;
|
|
811
|
+
}
|
|
812
|
+
try {
|
|
813
|
+
await git(
|
|
814
|
+
[
|
|
815
|
+
"-c",
|
|
816
|
+
`url.https://x-access-token:${githubToken}@github.com/.insteadOf=https://github.com/`,
|
|
817
|
+
"submodule",
|
|
818
|
+
"update",
|
|
819
|
+
"--init",
|
|
820
|
+
".claude/grimoire"
|
|
821
|
+
],
|
|
822
|
+
{ cwd: workspace, timeoutMs: 3e5 }
|
|
823
|
+
);
|
|
824
|
+
log.info("[boot] Initialized grimoire submodule");
|
|
825
|
+
} catch (err) {
|
|
826
|
+
log.warn(
|
|
827
|
+
`[boot] WARN: grimoire submodule init failed (baked pods may lack rc-* skills): ${errText(err)}`
|
|
828
|
+
);
|
|
829
|
+
}
|
|
830
|
+
} catch (err) {
|
|
831
|
+
log.warn(`[boot] WARN: grimoire submodule check failed: ${errText(err)}`);
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
function linkGrimoireSkills(workspace, log) {
|
|
835
|
+
const sourceDir = join4(workspace, ".claude", "grimoire", "skills");
|
|
836
|
+
const targetDir = join4(workspace, ".claude", "skills");
|
|
837
|
+
if (!isDir2(sourceDir)) return;
|
|
838
|
+
try {
|
|
839
|
+
mkdirSync3(targetDir, { recursive: true });
|
|
840
|
+
} catch {
|
|
841
|
+
log.warn(`[boot] WARN: unable to create ${targetDir}; skipping grimoire skill links`);
|
|
842
|
+
return;
|
|
843
|
+
}
|
|
844
|
+
let linked = 0;
|
|
845
|
+
let names = [];
|
|
846
|
+
try {
|
|
847
|
+
names = readdirSync(sourceDir);
|
|
848
|
+
} catch {
|
|
849
|
+
return;
|
|
850
|
+
}
|
|
851
|
+
for (const name of names) {
|
|
852
|
+
if (!isDir2(join4(sourceDir, name))) continue;
|
|
853
|
+
const target = join4(targetDir, name);
|
|
854
|
+
if (existsSync2(target) && !isSymlink(target)) {
|
|
855
|
+
log.warn(`[boot] WARN: ${target} exists and is not a symlink; skipping`);
|
|
856
|
+
continue;
|
|
857
|
+
}
|
|
858
|
+
try {
|
|
859
|
+
rmSync2(target, { force: true });
|
|
860
|
+
symlinkSync2(join4("..", "grimoire", "skills", name), target);
|
|
861
|
+
linked++;
|
|
862
|
+
} catch {
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
log.info(`[boot] Linked ${linked} grimoire skills into ${targetDir}`);
|
|
866
|
+
}
|
|
867
|
+
function parseReferenceRepos(refsJson) {
|
|
868
|
+
let parsed;
|
|
869
|
+
try {
|
|
870
|
+
parsed = JSON.parse(refsJson);
|
|
871
|
+
} catch {
|
|
872
|
+
return [];
|
|
873
|
+
}
|
|
874
|
+
if (!Array.isArray(parsed)) return [];
|
|
875
|
+
const refs = [];
|
|
876
|
+
for (const entry of parsed) {
|
|
877
|
+
if (typeof entry !== "object" || entry === null) continue;
|
|
878
|
+
const raw = entry;
|
|
879
|
+
const str = (key) => typeof raw[key] === "string" ? raw[key] : "";
|
|
880
|
+
const ref = {
|
|
881
|
+
slug: str("slug"),
|
|
882
|
+
owner: str("owner"),
|
|
883
|
+
name: str("name"),
|
|
884
|
+
branch: str("branch") || "main",
|
|
885
|
+
token: str("token")
|
|
886
|
+
};
|
|
887
|
+
if (ref.token && ref.slug && ref.owner && ref.name) refs.push(ref);
|
|
888
|
+
}
|
|
889
|
+
return refs;
|
|
890
|
+
}
|
|
891
|
+
async function runPreReadyBinds(workspace, bundle, git, log) {
|
|
892
|
+
const graphifyEnv = computeGraphifyEnv(process.env, bundle.gitPlan.repoName, isDir2);
|
|
893
|
+
bindGraphifyBundle(workspace, graphifyEnv, log);
|
|
894
|
+
await ensureGrimoireSubmodule(workspace, bundle.githubToken, git, log);
|
|
895
|
+
linkGrimoireSkills(workspace, log);
|
|
896
|
+
}
|
|
897
|
+
async function cloneReferenceRepos(refsJson, git, log, referencesDir = "/workspaces/references") {
|
|
898
|
+
if (!refsJson) return;
|
|
899
|
+
const refs = parseReferenceRepos(refsJson);
|
|
900
|
+
if (refs.length === 0) return;
|
|
901
|
+
try {
|
|
902
|
+
mkdirSync3(referencesDir, { recursive: true });
|
|
903
|
+
} catch {
|
|
904
|
+
return;
|
|
905
|
+
}
|
|
906
|
+
for (const ref of refs) {
|
|
907
|
+
const dest = join4(referencesDir, ref.slug);
|
|
908
|
+
if (existsSync2(join4(dest, ".git"))) continue;
|
|
909
|
+
try {
|
|
910
|
+
await git(
|
|
911
|
+
[
|
|
912
|
+
"clone",
|
|
913
|
+
"--depth",
|
|
914
|
+
"1",
|
|
915
|
+
"--single-branch",
|
|
916
|
+
"--branch",
|
|
917
|
+
ref.branch,
|
|
918
|
+
`https://x-access-token:${ref.token}@github.com/${ref.owner}/${ref.name}.git`,
|
|
919
|
+
dest
|
|
920
|
+
],
|
|
921
|
+
{ timeoutMs: 6e5 }
|
|
922
|
+
);
|
|
923
|
+
try {
|
|
924
|
+
await git(
|
|
925
|
+
["remote", "set-url", "origin", `https://github.com/${ref.owner}/${ref.name}.git`],
|
|
926
|
+
{ cwd: dest, timeoutMs: 1e4 }
|
|
927
|
+
);
|
|
928
|
+
} catch {
|
|
929
|
+
}
|
|
930
|
+
log.info(`[boot] cloned reference ${ref.slug} (${ref.owner}/${ref.name}@${ref.branch})`);
|
|
931
|
+
} catch {
|
|
932
|
+
log.warn(`[boot] WARN: reference clone failed: ${ref.slug}`);
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
// src/boot/workbench-boot-deps.ts
|
|
938
|
+
var WORKSPACE_DIR = "/workspaces/repo";
|
|
939
|
+
var HOME_DIR = "/home/conveyor";
|
|
940
|
+
var USER_HOME_MOUNT2 = "/mnt/conveyor-users";
|
|
941
|
+
function sleep(ms) {
|
|
942
|
+
return new Promise((resolve) => {
|
|
943
|
+
setTimeout(resolve, ms);
|
|
944
|
+
});
|
|
945
|
+
}
|
|
946
|
+
function defaultWorkbenchBootDeps() {
|
|
947
|
+
return {
|
|
948
|
+
pollBundle: (ctx) => pollBootstrapBundle(ctx.apiUrl, ctx.bootstrapToken, {
|
|
949
|
+
fetchFn: fetch,
|
|
950
|
+
sleepFn: sleep,
|
|
951
|
+
log: ctx.log
|
|
952
|
+
}),
|
|
953
|
+
unpack: (ctx, bundle) => {
|
|
954
|
+
const claims = decodeJwtClaims(bundle.sessionJwt);
|
|
955
|
+
ctx.claims = claims;
|
|
956
|
+
return buildChildEnv({
|
|
957
|
+
base: process.env,
|
|
958
|
+
bundle,
|
|
959
|
+
claims,
|
|
960
|
+
hostname: ctx.hostname,
|
|
961
|
+
log: ctx.log
|
|
962
|
+
});
|
|
963
|
+
},
|
|
964
|
+
startDaemon: (opts) => startWorkbenchServer(opts),
|
|
965
|
+
gitPrepDeps: (ctx, bundle) => ({
|
|
966
|
+
git: defaultGit,
|
|
967
|
+
bundle,
|
|
968
|
+
podImage: process.env.CONVEYOR_POD_IMAGE === "1",
|
|
969
|
+
log: ctx.log
|
|
970
|
+
}),
|
|
971
|
+
bindsBeforeReady: (ctx, bundle) => runPreReadyBinds(WORKSPACE_DIR, bundle, defaultGit, ctx.log),
|
|
972
|
+
cloneReferences: (ctx, bundle) => cloneReferenceRepos(bundle.envVars?.REFERENCE_REPOS_JSON, defaultGit, ctx.log),
|
|
973
|
+
ensureWorkspaceDir: (ctx) => {
|
|
974
|
+
ensureDir(WORKSPACE_DIR);
|
|
975
|
+
ctx.log.info(`[boot] ensured workspace dir ${WORKSPACE_DIR}`);
|
|
976
|
+
},
|
|
977
|
+
wireHome: (ctx) => {
|
|
978
|
+
wireUserHome({
|
|
979
|
+
home: HOME_DIR,
|
|
980
|
+
userHomeMount: USER_HOME_MOUNT2,
|
|
981
|
+
userId: ctx.childEnv?.CONVEYOR_USER_ID,
|
|
982
|
+
projectId: ctx.childEnv?.CONVEYOR_PROJECT_ID ?? ctx.childEnv?.PROJECT_ID,
|
|
983
|
+
sharedDir: process.env.CONVEYOR_SHARED_DIR,
|
|
984
|
+
log: ctx.log
|
|
985
|
+
});
|
|
986
|
+
},
|
|
987
|
+
startSshd: (ctx) => startWorkspaceSshd(defaultExec, ctx.log),
|
|
988
|
+
reclaimOwnership: (ctx) => reclaimHomeOwnership(HOME_DIR, defaultExec, ctx.log),
|
|
989
|
+
park: () => new Promise(() => {
|
|
990
|
+
})
|
|
991
|
+
};
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
// src/boot/workbench-boot.ts
|
|
995
|
+
function toGitStatusFrame(state) {
|
|
996
|
+
return state.state === "failed" ? { t: "gitStatus", state: "failed", reason: state.reason } : { t: "gitStatus", state: state.state };
|
|
997
|
+
}
|
|
998
|
+
function warnErr(ctx, label) {
|
|
999
|
+
return (err) => {
|
|
1000
|
+
ctx.log.warn(
|
|
1001
|
+
`[boot] WARN ${label} failed: ${err instanceof Error ? err.message : String(err)}`
|
|
1002
|
+
);
|
|
1003
|
+
};
|
|
1004
|
+
}
|
|
1005
|
+
async function runWorkbenchBoot(ctx, overrides = {}) {
|
|
1006
|
+
const deps = { ...defaultWorkbenchBootDeps(), ...overrides };
|
|
1007
|
+
const bundle = await deps.pollBundle(ctx);
|
|
1008
|
+
if (!bundle) {
|
|
1009
|
+
ctx.log.info("[boot] standby poll aborted \u2014 parking for shutdown");
|
|
1010
|
+
return deps.park(ctx);
|
|
1011
|
+
}
|
|
1012
|
+
ctx.bundle = bundle;
|
|
1013
|
+
ctx.childEnv = deps.unpack(ctx, bundle);
|
|
1014
|
+
const job = new GitPrepJob(deps.gitPrepDeps(ctx, bundle), {
|
|
1015
|
+
// Order pinned by the old entrypoint-grimoire test: binds BEFORE the
|
|
1016
|
+
// ready flip (Claude must not spawn before skills exist), references after.
|
|
1017
|
+
onReady: () => deps.bindsBeforeReady(ctx, bundle),
|
|
1018
|
+
afterReady: () => deps.cloneReferences(ctx, bundle)
|
|
1019
|
+
});
|
|
1020
|
+
await deps.startDaemon({
|
|
1021
|
+
port: workbenchPort() ?? DEFAULT_WORKBENCH_PORT,
|
|
1022
|
+
token: ctx.bootstrapToken,
|
|
1023
|
+
version: readAgentVersion() ?? "unknown",
|
|
1024
|
+
getGitStatus: () => toGitStatusFrame(job.status)
|
|
1025
|
+
});
|
|
1026
|
+
ctx.log.info("[boot] step=startDaemon listen socket up (startupProbe gate open)");
|
|
1027
|
+
try {
|
|
1028
|
+
deps.ensureWorkspaceDir(ctx);
|
|
1029
|
+
} catch (err) {
|
|
1030
|
+
warnErr(ctx, "ensureWorkspaceDir")(err);
|
|
1031
|
+
}
|
|
1032
|
+
job.start();
|
|
1033
|
+
try {
|
|
1034
|
+
deps.wireHome(ctx);
|
|
1035
|
+
} catch (err) {
|
|
1036
|
+
warnErr(ctx, "wireUserHome")(err);
|
|
1037
|
+
}
|
|
1038
|
+
void deps.startSshd(ctx).catch(warnErr(ctx, "startWorkspaceSshd"));
|
|
1039
|
+
void deps.reclaimOwnership(ctx).catch(warnErr(ctx, "reclaimHomeOwnership"));
|
|
1040
|
+
return deps.park(ctx);
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
// src/boot/index.ts
|
|
1044
|
+
var HOME_DIR2 = "/home/conveyor";
|
|
1045
|
+
var USER_HOME_MOUNT3 = "/mnt/conveyor-users";
|
|
1046
|
+
function defaultBootDeps() {
|
|
1047
|
+
return {
|
|
1048
|
+
fetchFn: fetch,
|
|
1049
|
+
sleepFn: (ms) => new Promise((resolve) => {
|
|
1050
|
+
setTimeout(resolve, ms);
|
|
1051
|
+
}),
|
|
1052
|
+
spawnFn: spawn,
|
|
1053
|
+
exec: defaultExec,
|
|
1054
|
+
installedVersion: () => {
|
|
1055
|
+
const version = readAgentVersion();
|
|
1056
|
+
return version ? Promise.resolve(version) : Promise.reject(new Error("could not read installed agent version"));
|
|
1057
|
+
},
|
|
1058
|
+
dirExists: (path) => {
|
|
1059
|
+
try {
|
|
1060
|
+
return statSync3(path).isDirectory();
|
|
1061
|
+
} catch {
|
|
1062
|
+
return false;
|
|
1063
|
+
}
|
|
1064
|
+
},
|
|
1065
|
+
hostname: osHostname(),
|
|
1066
|
+
log: createBootLogger()
|
|
1067
|
+
};
|
|
1068
|
+
}
|
|
1069
|
+
function errMsg(err) {
|
|
1070
|
+
return err instanceof Error ? err.message : String(err);
|
|
1071
|
+
}
|
|
1072
|
+
function whenAborted2(signal) {
|
|
1073
|
+
return new Promise((resolve) => {
|
|
1074
|
+
if (signal.aborted) {
|
|
1075
|
+
resolve();
|
|
1076
|
+
return;
|
|
1077
|
+
}
|
|
1078
|
+
signal.addEventListener("abort", () => resolve(), { once: true });
|
|
1079
|
+
});
|
|
1080
|
+
}
|
|
1081
|
+
function agentBootSteps(deps = defaultBootDeps(), signal = new AbortController().signal) {
|
|
1082
|
+
return [
|
|
1083
|
+
{
|
|
1084
|
+
name: "pollBootstrap",
|
|
1085
|
+
critical: true,
|
|
1086
|
+
run: async (ctx) => {
|
|
1087
|
+
const bundle = await pollBootstrapBundle(ctx.apiUrl, ctx.bootstrapToken, {
|
|
1088
|
+
fetchFn: deps.fetchFn,
|
|
1089
|
+
sleepFn: deps.sleepFn,
|
|
1090
|
+
log: ctx.log,
|
|
1091
|
+
signal
|
|
1092
|
+
});
|
|
1093
|
+
if (bundle) ctx.bundle = bundle;
|
|
1094
|
+
}
|
|
1095
|
+
},
|
|
1096
|
+
{
|
|
1097
|
+
name: "unpackBundle",
|
|
1098
|
+
critical: true,
|
|
1099
|
+
run: (ctx) => {
|
|
1100
|
+
if (ctx.bundle) {
|
|
1101
|
+
const claims = decodeJwtClaims(ctx.bundle.sessionJwt);
|
|
1102
|
+
ctx.claims = claims;
|
|
1103
|
+
const childEnv = buildChildEnv({
|
|
1104
|
+
base: process.env,
|
|
1105
|
+
bundle: ctx.bundle,
|
|
1106
|
+
claims,
|
|
1107
|
+
hostname: ctx.hostname,
|
|
1108
|
+
log: ctx.log
|
|
1109
|
+
});
|
|
1110
|
+
const graphifyEnv = computeGraphifyEnv(
|
|
1111
|
+
process.env,
|
|
1112
|
+
ctx.bundle.gitPlan.repoName,
|
|
1113
|
+
deps.dirExists
|
|
1114
|
+
);
|
|
1115
|
+
ctx.childEnv = { ...childEnv, ...graphifyEnv };
|
|
1116
|
+
}
|
|
1117
|
+
return Promise.resolve();
|
|
1118
|
+
}
|
|
1119
|
+
},
|
|
1120
|
+
{
|
|
1121
|
+
name: "reclaimHome",
|
|
1122
|
+
critical: false,
|
|
1123
|
+
run: async (ctx) => {
|
|
1124
|
+
if (!ctx.bundle) return;
|
|
1125
|
+
await reclaimHomeOwnership(HOME_DIR2, deps.exec, ctx.log);
|
|
1126
|
+
}
|
|
1127
|
+
},
|
|
1128
|
+
{
|
|
1129
|
+
name: "wireUserHome",
|
|
1130
|
+
critical: true,
|
|
1131
|
+
run: (ctx) => {
|
|
1132
|
+
if (ctx.bundle) {
|
|
1133
|
+
wireUserHome({
|
|
1134
|
+
home: HOME_DIR2,
|
|
1135
|
+
userHomeMount: USER_HOME_MOUNT3,
|
|
1136
|
+
userId: ctx.childEnv?.CONVEYOR_USER_ID,
|
|
1137
|
+
projectId: ctx.childEnv?.CONVEYOR_PROJECT_ID ?? ctx.childEnv?.PROJECT_ID,
|
|
1138
|
+
sharedDir: process.env.CONVEYOR_SHARED_DIR,
|
|
1139
|
+
log: ctx.log
|
|
1140
|
+
});
|
|
1141
|
+
}
|
|
1142
|
+
return Promise.resolve();
|
|
1143
|
+
}
|
|
1144
|
+
}
|
|
1145
|
+
];
|
|
1146
|
+
}
|
|
1147
|
+
function workbenchBootSteps(deps = defaultWorkbenchBootDeps()) {
|
|
1148
|
+
return [
|
|
1149
|
+
{
|
|
1150
|
+
name: "pollBootstrap",
|
|
1151
|
+
critical: true,
|
|
1152
|
+
run: async (ctx) => {
|
|
1153
|
+
const bundle = await deps.pollBundle(ctx);
|
|
1154
|
+
if (bundle) ctx.bundle = bundle;
|
|
1155
|
+
}
|
|
1156
|
+
},
|
|
1157
|
+
{
|
|
1158
|
+
name: "unpackBundle",
|
|
1159
|
+
critical: true,
|
|
1160
|
+
run: (ctx) => {
|
|
1161
|
+
if (ctx.bundle) ctx.childEnv = deps.unpack(ctx, ctx.bundle);
|
|
1162
|
+
return Promise.resolve();
|
|
1163
|
+
}
|
|
1164
|
+
},
|
|
1165
|
+
{
|
|
1166
|
+
name: "startDaemon",
|
|
1167
|
+
critical: true,
|
|
1168
|
+
run: async (ctx) => {
|
|
1169
|
+
await deps.startDaemon({
|
|
1170
|
+
port: workbenchPort() ?? DEFAULT_WORKBENCH_PORT,
|
|
1171
|
+
token: ctx.bootstrapToken,
|
|
1172
|
+
version: readAgentVersion() ?? "unknown",
|
|
1173
|
+
// The manifest has no GitPrepJob, so it must never claim readiness —
|
|
1174
|
+
// "pending" is the safe answer if anyone ever executes these steps
|
|
1175
|
+
// directly instead of via runWorkbenchBoot (which serves live state).
|
|
1176
|
+
getGitStatus: () => ({ t: "gitStatus", state: "pending" })
|
|
1177
|
+
});
|
|
1178
|
+
}
|
|
1179
|
+
}
|
|
1180
|
+
];
|
|
1181
|
+
}
|
|
1182
|
+
async function runAgentRole(ctx, deps, signal) {
|
|
1183
|
+
try {
|
|
1184
|
+
await runPipeline(agentBootSteps(deps, signal), ctx);
|
|
1185
|
+
} catch (err) {
|
|
1186
|
+
ctx.log.error(`[boot] agent boot failed: ${errMsg(err)}`);
|
|
1187
|
+
return 1;
|
|
1188
|
+
}
|
|
1189
|
+
if (signal.aborted || !ctx.bundle) {
|
|
1190
|
+
ctx.log.info("[boot] SIGTERM during standby \u2014 exiting.");
|
|
1191
|
+
return 0;
|
|
1192
|
+
}
|
|
1193
|
+
process.chdir(HOME_DIR2);
|
|
1194
|
+
return superviseRunner({
|
|
1195
|
+
childEnv: ctx.childEnv ?? {},
|
|
1196
|
+
cwd: HOME_DIR2,
|
|
1197
|
+
apiUrl: ctx.apiUrl,
|
|
1198
|
+
bootstrapToken: ctx.bootstrapToken,
|
|
1199
|
+
log: ctx.log,
|
|
1200
|
+
spawnFn: deps.spawnFn,
|
|
1201
|
+
signal
|
|
1202
|
+
});
|
|
1203
|
+
}
|
|
1204
|
+
async function runWorkbenchRole(ctx, deps, signal) {
|
|
1205
|
+
try {
|
|
1206
|
+
await Promise.race([
|
|
1207
|
+
runWorkbenchBoot(ctx, {
|
|
1208
|
+
pollBundle: (workbenchCtx) => pollBootstrapBundle(workbenchCtx.apiUrl, workbenchCtx.bootstrapToken, {
|
|
1209
|
+
fetchFn: deps.fetchFn,
|
|
1210
|
+
sleepFn: deps.sleepFn,
|
|
1211
|
+
log: workbenchCtx.log,
|
|
1212
|
+
signal
|
|
1213
|
+
})
|
|
1214
|
+
}),
|
|
1215
|
+
whenAborted2(signal)
|
|
1216
|
+
]);
|
|
1217
|
+
} catch (err) {
|
|
1218
|
+
ctx.log.error(`[boot] workbench boot failed: ${errMsg(err)}`);
|
|
1219
|
+
return 1;
|
|
1220
|
+
}
|
|
1221
|
+
ctx.log.info(
|
|
1222
|
+
ctx.bundle ? "[boot] SIGTERM \u2014 workbench shutting down." : "[boot] SIGTERM during standby \u2014 exiting."
|
|
1223
|
+
);
|
|
1224
|
+
return 0;
|
|
1225
|
+
}
|
|
1226
|
+
async function runBoot(argv, deps = defaultBootDeps()) {
|
|
1227
|
+
const { log } = deps;
|
|
1228
|
+
if (argv.includes("--preflight-update")) {
|
|
1229
|
+
await runPreflightUpdate({
|
|
1230
|
+
exec: deps.exec,
|
|
1231
|
+
installedVersion: deps.installedVersion,
|
|
1232
|
+
log
|
|
1233
|
+
});
|
|
1234
|
+
return 0;
|
|
1235
|
+
}
|
|
1236
|
+
const role = process.env.CONVEYOR_CONTAINER_ROLE;
|
|
1237
|
+
if (role !== "agent" && role !== "workbench") {
|
|
1238
|
+
log.error("[boot] FATAL missing/invalid CONVEYOR_CONTAINER_ROLE \u2014 vnext pods are split-only");
|
|
1239
|
+
return 1;
|
|
1240
|
+
}
|
|
1241
|
+
const apiUrl = process.env.CONVEYOR_API_URL;
|
|
1242
|
+
const bootstrapToken = process.env.POD_BOOTSTRAP_TOKEN;
|
|
1243
|
+
if (!apiUrl || !bootstrapToken) {
|
|
1244
|
+
log.error("[boot] FATAL missing required env: CONVEYOR_API_URL + POD_BOOTSTRAP_TOKEN");
|
|
1245
|
+
return 1;
|
|
1246
|
+
}
|
|
1247
|
+
const controller = new AbortController();
|
|
1248
|
+
const abort = () => controller.abort();
|
|
1249
|
+
process.on("SIGTERM", abort);
|
|
1250
|
+
process.on("SIGINT", abort);
|
|
1251
|
+
const ctx = {
|
|
1252
|
+
role,
|
|
1253
|
+
apiUrl,
|
|
1254
|
+
bootstrapToken,
|
|
1255
|
+
hostname: deps.hostname,
|
|
1256
|
+
log
|
|
1257
|
+
};
|
|
1258
|
+
return role === "workbench" ? runWorkbenchRole(ctx, deps, controller.signal) : runAgentRole(ctx, deps, controller.signal);
|
|
1259
|
+
}
|
|
1260
|
+
export {
|
|
1261
|
+
TOMBSTONE_MESSAGE,
|
|
1262
|
+
agentBootSteps,
|
|
1263
|
+
defaultBootDeps,
|
|
1264
|
+
isLegacyEntrypointLaunch,
|
|
1265
|
+
runBoot,
|
|
1266
|
+
workbenchBootSteps
|
|
1267
|
+
};
|
|
1268
|
+
//# sourceMappingURL=boot-NMXFM4GK.js.map
|