@outerlayer/cli 0.5.0 → 0.6.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.
Files changed (70) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/README.md +9 -0
  3. package/dist/{agent-setup-IFH3FB5X.js → agent-setup-USZ2SAXC.js} +2 -2
  4. package/dist/build-info.json +1 -1
  5. package/dist/{check-AXCXDYO2.js → check-CCVPBN7A.js} +26 -14
  6. package/dist/{chunk-5Y3QQRUH.js → chunk-3DD6AOWC.js} +37 -0
  7. package/dist/{chunk-WOBNRVQT.js → chunk-3DEVYTNQ.js} +1 -1
  8. package/dist/{chunk-L24G4PLP.js → chunk-43J3M6KB.js} +486 -21
  9. package/dist/{chunk-FC7TMRNA.js → chunk-4NLKY4WR.js} +2 -2
  10. package/dist/{lima-XN65D7GN.js → chunk-77MM5RYD.js} +0 -1
  11. package/dist/{chunk-D2KWEAN6.js → chunk-7DBI6SNT.js} +1 -1
  12. package/dist/{chunk-QDQEUVUF.js → chunk-AIW6GLET.js} +1 -1
  13. package/dist/{chunk-EX6M5JPR.js → chunk-ANK2B36Y.js} +131 -15
  14. package/dist/{chunk-RMAZ2NWA.js → chunk-BNMCXPG7.js} +14 -2
  15. package/dist/{chunk-UFFXNXLQ.js → chunk-FA2BOX4O.js} +31 -29
  16. package/dist/chunk-FBTE2A2X.js +38 -0
  17. package/dist/{chunk-DVDEBNQJ.js → chunk-GQD22KKN.js} +17 -6
  18. package/dist/{chunk-BCJNHMZT.js → chunk-HHJDN6AU.js} +18 -3
  19. package/dist/{chunk-JLDZTG3A.js → chunk-IASWYRVG.js} +1 -1
  20. package/dist/{install-cmd-7WXOIFDO.js → chunk-JWOQNNN2.js} +0 -1
  21. package/dist/{chunk-ZLB7GCTY.js → chunk-MIGEDH5P.js} +19 -11
  22. package/dist/{chunk-OHFDWLJP.js → chunk-MXB5VB4Y.js} +18 -6
  23. package/dist/chunk-NQXFAGHU.js +211 -0
  24. package/dist/chunk-OUUINEYB.js +27 -0
  25. package/dist/{chunk-OR6AEEDO.js → chunk-PMAMOSTP.js} +3 -3
  26. package/dist/{chunk-RJ467PDQ.js → chunk-RBOMWNOG.js} +1 -1
  27. package/dist/{chunk-4NMBDRBS.js → chunk-RZSF5B46.js} +289 -130
  28. package/dist/chunk-T6OIJQRT.js +31 -0
  29. package/dist/chunk-UGP76IZ5.js +34 -0
  30. package/dist/{chunk-EGABNHZG.js → chunk-UMFWVGJO.js} +148 -18
  31. package/dist/{chunk-RWOTHXYE.js → chunk-VMJ3MMBX.js} +206 -71
  32. package/dist/chunk-YYYRBYWG.js +32 -0
  33. package/dist/chunk-ZILGTTY6.js +75 -0
  34. package/dist/{cli-NNWPFP5S.js → cli-7WCNPBYC.js} +147 -69
  35. package/dist/command-ARFK3OYF.js +162 -0
  36. package/dist/{config-XVJYZ4IQ.js → config-HL2AIR3K.js} +1 -1
  37. package/dist/{connect-cmd-OJBFHZWP.js → connect-cmd-PCESZLTR.js} +4 -4
  38. package/dist/{context-adopt-UMA4O5NY.js → context-adopt-G5JAELE7.js} +2 -2
  39. package/dist/{context-materialize-G2FUFC72.js → context-materialize-3HLPZAF6.js} +2 -2
  40. package/dist/{dist-WDV7U7UJ.js → dist-RDLP4GGJ.js} +1 -1
  41. package/dist/{doctor-J3GIMR7K.js → doctor-VOMYOHQU.js} +47 -13
  42. package/dist/{emit-artifact-cmd-FDWYKURA.js → emit-artifact-cmd-TBF46VXZ.js} +6 -7
  43. package/dist/{emit-criteria-cmd-OCXHLK34.js → emit-criteria-cmd-3427XKFW.js} +3 -3
  44. package/dist/{emit-finding-cmd-POW7PKIA.js → emit-finding-cmd-GSI75K36.js} +3 -3
  45. package/dist/{emit-result-cmd-ID35SH53.js → emit-result-cmd-3PRQKB55.js} +6 -7
  46. package/dist/{hook-fast-EYENPK6F.js → hook-fast-NJBBZQ5Q.js} +1 -1
  47. package/dist/index.js +3 -2
  48. package/dist/{init-CZX7ZDWF.js → init-OHYFIKZD.js} +27 -56
  49. package/dist/{init-3ZQLFPNC.js → init-WVR4BZQ4.js} +8 -8
  50. package/dist/{init-cmd-W5NOLQPX.js → init-cmd-HMFNIFPI.js} +7 -7
  51. package/dist/inspect-XAUSB7L3.js +63 -0
  52. package/dist/install-DSUZE2TJ.js +13 -0
  53. package/dist/install-cmd-453C7IZT.js +8 -0
  54. package/dist/lima-7NPCNGWT.js +6 -0
  55. package/dist/{loop-VGM772I7.js → loop-UAJB34KA.js} +26 -11
  56. package/dist/{machine-5WEJA36F.js → machine-2JOWGRSZ.js} +7 -7
  57. package/dist/record-C43U4PKS.js +7 -0
  58. package/dist/session-sender-ED26V45B.js +28 -0
  59. package/dist/{status-KBDVUYQ3.js → status-6WNXX2O7.js} +42 -10
  60. package/dist/{sync-cmd-SWQ5KLPL.js → sync-cmd-YONVGUYS.js} +6 -7
  61. package/dist/{watch-3YW6Z3DQ.js → watch-HHKAF26L.js} +6 -7
  62. package/dist/{work-claim-cmd-BQPPURBW.js → work-claim-cmd-LXBZTMJA.js} +6 -6
  63. package/dist/{work-cmd-NUR4CAG6.js → work-cmd-OVNF44FY.js} +3 -3
  64. package/dist/{work-comment-cmd-6TTTYGHM.js → work-comment-cmd-6DGQNTAH.js} +3 -3
  65. package/dist/{work-launch-YI4CJDAP.js → work-launch-R5HTAP6X.js} +1 -1
  66. package/dist/{work-open-pr-cmd-KZ7QQFLD.js → work-open-pr-cmd-FCZHFHZS.js} +4 -4
  67. package/dist/{work-pr-cmd-QNUBE6IQ.js → work-pr-cmd-QJJVHZDC.js} +2 -2
  68. package/package.json +2 -1
  69. package/dist/chunk-ZOC7JSYC.js +0 -83
  70. package/dist/{chunk-XDDW4FRS.js → chunk-OIRNH6UO.js} +1 -1
@@ -7,23 +7,23 @@ import { createDockerHost, childEnvironment, spawnRun } from './chunk-774HEQL6.j
7
7
  import { RecipeError } from './chunk-QOZKPLVJ.js';
8
8
  import { execCapture } from './chunk-W4SQCOZU.js';
9
9
  import { GUEST_CID } from './chunk-B2G7JHHB.js';
10
- import { WorkCommandError, postJson } from './chunk-RMAZ2NWA.js';
10
+ import { WorkCommandError, postJson } from './chunk-BNMCXPG7.js';
11
11
  import { parseDuKilobytes, hostAllowed } from './chunk-F6GFZXZ3.js';
12
12
  import { RELAY_PORT, CHECKOUT_DIR, JOB_MOUNT, claudeCredential, buildEnvironment, startBuildServices, AGENT_CREDENTIAL_MISSING, GATEWAY_PORT, GATEWAY_SOCKET, CLAUDE_PORT, CLAUDE_SOCKET, DESTINATION_BASE_PORT } from './chunk-AUFG23AA.js';
13
- import { defaultGitRunner, materializeContext } from './chunk-XDDW4FRS.js';
13
+ import { defaultGitRunner, materializeContext } from './chunk-OIRNH6UO.js';
14
14
  import { readContextManifest, readSourceCache } from './chunk-WIOZAJ2W.js';
15
15
  import { signingFetchFor } from './chunk-BCLJSQCV.js';
16
16
  import { signerFor } from './chunk-M5POMKKW.js';
17
- import { rootfsPathFor, diskTemplatesDir, RUNNER_FILE_MODE, stagedContextDir, contextHomeDir, ghConfigDir, runnerDirOfBuild, spoolDir, mirrorDir, buildDir, RUNNER_DIR_MODE, vmAssetsDir } from './chunk-ZM2IMMYN.js';
18
- import { relative, join, dirname, basename } from 'path';
17
+ import { rootfsPathFor, diskTemplatesDir, RUNNER_FILE_MODE, stagedContextDir, contextHomeDir, ghConfigDir, runnerDirOfBuild, spoolDir, mirrorDir, buildDir, runnerDir, RUNNER_DIR_MODE, vmAssetsDir } from './chunk-ZM2IMMYN.js';
18
+ import { relative, join, dirname, resolve, basename } from 'path';
19
+ import { readFileSync, existsSync, mkdirSync, chmodSync, truncateSync, rmSync, writeFileSync, accessSync, constants, readdirSync, statSync, rmdirSync, realpathSync, renameSync, lstatSync, unlinkSync, openSync, closeSync, mkdtempSync, createWriteStream, createReadStream } from 'fs';
19
20
  import { spawnSync, spawn } from 'child_process';
20
- import { randomBytes, createHash } from 'crypto';
21
- import { existsSync, mkdirSync, chmodSync, truncateSync, rmSync, writeFileSync, renameSync, lstatSync, readFileSync, unlinkSync, readdirSync, openSync, closeSync, mkdtempSync, createWriteStream, statSync, createReadStream } from 'fs';
21
+ import { createHash, randomBytes } from 'crypto';
22
22
  import { createServer, request } from 'http';
23
23
  import { request as request$1 } from 'https';
24
24
  import { Transform } from 'stream';
25
25
  import { pipeline, finished } from 'stream/promises';
26
- import { createGunzip, gunzipSync, constants } from 'zlib';
26
+ import { createGunzip, gunzipSync, constants as constants$1 } from 'zlib';
27
27
  import { isIP, connect, createServer as createServer$1 } from 'net';
28
28
  import { lookup } from 'dns/promises';
29
29
  import { networkInterfaces } from 'os';
@@ -86,7 +86,7 @@ function isRetryable(err) {
86
86
  }
87
87
  async function retrySend(send, deps = {}) {
88
88
  const now = deps.now ?? (() => /* @__PURE__ */ new Date());
89
- const sleep = deps.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
89
+ const sleep = deps.sleep ?? ((ms) => new Promise((resolve2) => setTimeout(resolve2, ms)));
90
90
  const windowMs = deps.windowMs ?? RETRY_WINDOW_MS;
91
91
  const startedMs = now().getTime();
92
92
  let attempts = 0;
@@ -255,6 +255,111 @@ var BuildTokens = class {
255
255
  );
256
256
  }
257
257
  };
258
+ var BUILD_PREFIX = "build-";
259
+ var LEAF = "runner";
260
+ function serviceDirectory(selfCgroup, mount) {
261
+ const line = selfCgroup?.split("\n").find((l) => l.startsWith("0::"));
262
+ if (line === void 0) return void 0;
263
+ const path = line.slice(3).trim();
264
+ if (!path.endsWith(".service")) return void 0;
265
+ return join(mount, path);
266
+ }
267
+ function prepareRunnerCgroup(setup) {
268
+ if (setup.invocationId === void 0 || setup.invocationId === "") return void 0;
269
+ const root = serviceDirectory(setup.selfCgroup, setup.mount);
270
+ if (root === void 0) return void 0;
271
+ const { fs } = setup;
272
+ if (!fs.exists(root) || !fs.writable(join(root, "cgroup.procs"))) return void 0;
273
+ if (!(fs.read(join(root, "cgroup.controllers")) ?? "").split(/\s+/).includes("memory")) return void 0;
274
+ for (const name of fs.list(root)) {
275
+ if (!name.startsWith(BUILD_PREFIX)) continue;
276
+ try {
277
+ fs.rmdir(join(root, name));
278
+ } catch {
279
+ }
280
+ }
281
+ if (!fs.exists(join(root, LEAF))) fs.mkdir(join(root, LEAF));
282
+ fs.write(join(root, LEAF, "cgroup.procs"), String(setup.pid));
283
+ fs.write(join(root, "cgroup.subtree_control"), "+memory");
284
+ return { root };
285
+ }
286
+ function createBuildCgroup(fs, root, buildId, buildMemoryBytes) {
287
+ const path = join(root, `${BUILD_PREFIX}${buildId}`);
288
+ const firecrackerOverheadBytes = 256 * 1024 ** 2;
289
+ fs.mkdir(path);
290
+ try {
291
+ fs.write(join(path, "memory.max"), String(buildMemoryBytes + firecrackerOverheadBytes));
292
+ fs.write(join(path, "memory.swap.max"), "0");
293
+ } catch (err) {
294
+ removeBuildCgroup(fs, path);
295
+ throw err;
296
+ }
297
+ return path;
298
+ }
299
+ function oomKills(fs, path) {
300
+ const match = /^oom_kill (\d+)$/m.exec(fs.read(join(path, "memory.events")) ?? "");
301
+ return match === null ? 0 : Number(match[1]);
302
+ }
303
+ function removeBuildCgroup(fs, path) {
304
+ try {
305
+ fs.rmdir(path);
306
+ } catch (err) {
307
+ if (err.code !== "ENOENT") throw err;
308
+ }
309
+ }
310
+ function realCgroupFs() {
311
+ return {
312
+ exists: (path) => existsSync(path),
313
+ read: (path) => {
314
+ try {
315
+ return readFileSync(path, "utf8");
316
+ } catch {
317
+ return void 0;
318
+ }
319
+ },
320
+ write: (path, data) => writeFileSync(path, data),
321
+ mkdir: (path) => mkdirSync(path),
322
+ rmdir: (path) => rmdirSync(path),
323
+ list: (path) => readdirSync(path).filter((name) => statSync(join(path, name)).isDirectory()),
324
+ writable: (path) => {
325
+ try {
326
+ accessSync(path, constants.W_OK);
327
+ return true;
328
+ } catch {
329
+ return false;
330
+ }
331
+ }
332
+ };
333
+ }
334
+ var delegatedRoot;
335
+ function setUpRunnerCgroup(over = {}) {
336
+ let selfCgroup;
337
+ try {
338
+ selfCgroup = readFileSync("/proc/self/cgroup", "utf8");
339
+ } catch {
340
+ selfCgroup = void 0;
341
+ }
342
+ let refusal;
343
+ try {
344
+ const prepared = prepareRunnerCgroup({ fs: realCgroupFs(), selfCgroup, pid: process.pid, invocationId: process.env.INVOCATION_ID, mount: "/sys/fs/cgroup", ...over });
345
+ delegatedRoot = prepared?.root;
346
+ } catch (err) {
347
+ delegatedRoot = void 0;
348
+ refusal = err instanceof Error ? err.message : String(err);
349
+ }
350
+ return { delegated: delegatedRoot !== void 0, ...refusal === void 0 ? {} : { refusal } };
351
+ }
352
+ function realCgroupPort() {
353
+ const fs = realCgroupFs();
354
+ return {
355
+ create: (buildId, bytes) => {
356
+ if (delegatedRoot === void 0) return void 0;
357
+ return createBuildCgroup(fs, delegatedRoot, buildId, bytes);
358
+ },
359
+ oomKills: (path) => oomKills(fs, path),
360
+ remove: (path) => removeBuildCgroup(fs, path)
361
+ };
362
+ }
258
363
  var BranchPrepareError = class extends Error {
259
364
  };
260
365
  var OWN_PREFIX = "outerlayer/";
@@ -356,6 +461,119 @@ function lifecycleSteps(lifecycle) {
356
461
  function lifecycleFailureReason(step, exitCode) {
357
462
  return `lifecycle command ${step.name} (${step.source}) exited ${exitCode}`;
358
463
  }
464
+ var BUILD_LABEL = "ai.outerlayer.build";
465
+ var RUNNER_LABEL = "ai.outerlayer.runner";
466
+ function resolvedRunnerDir(configPath) {
467
+ try {
468
+ return realpathSync(runnerDir(configPath));
469
+ } catch {
470
+ try {
471
+ return join(realpathSync(dirname(resolve(configPath))), "runner");
472
+ } catch {
473
+ return resolve(runnerDir(configPath));
474
+ }
475
+ }
476
+ }
477
+ function runnerLabelValue(configPath) {
478
+ return createHash("sha256").update(resolvedRunnerDir(configPath)).digest("hex").slice(0, 12);
479
+ }
480
+ function hardened(input) {
481
+ const { memory, cpus, pids } = input.limits;
482
+ return [
483
+ "--label",
484
+ `${BUILD_LABEL}=${input.buildId}`,
485
+ "--label",
486
+ `${RUNNER_LABEL}=${input.runnerId}`,
487
+ "--network",
488
+ "none",
489
+ "--cap-drop",
490
+ "ALL",
491
+ "--security-opt",
492
+ "no-new-privileges",
493
+ "--read-only",
494
+ "--user",
495
+ input.user,
496
+ // The same figure for swap as for memory leaves the container none.
497
+ "--memory",
498
+ memory,
499
+ "--memory-swap",
500
+ memory,
501
+ "--cpus",
502
+ String(cpus),
503
+ "--pids-limit",
504
+ String(pids)
505
+ ];
506
+ }
507
+ function mountPath(path) {
508
+ if (/[",]/.test(path)) throw new Error(`${path} holds a comma or a quote, which a Docker mount cannot carry; move the runner's config file`);
509
+ return path;
510
+ }
511
+ function containerRunArgs(input) {
512
+ return [
513
+ "--detach",
514
+ "--name",
515
+ input.container,
516
+ ...hardened(input),
517
+ "--mount",
518
+ `type=volume,src=${input.volume},dst=/build`,
519
+ // `/tmp` is a directory of the same volume, so it is measured and limited with the rest of the build.
520
+ "--mount",
521
+ `type=volume,src=${input.volume},dst=/tmp,volume-subpath=tmp`,
522
+ "--mount",
523
+ `type=bind,src=${mountPath(input.buildDir)},dst=${JOB_MOUNT},readonly`,
524
+ "--entrypoint",
525
+ "/opt/outerlayer/bin/tini",
526
+ input.image,
527
+ "--",
528
+ "/opt/outerlayer/bin/node",
529
+ "/opt/outerlayer/cli/dist/index.js",
530
+ "broker-relay",
531
+ "--port",
532
+ String(RELAY_PORT),
533
+ "--socket",
534
+ `${JOB_MOUNT}/tunnel.sock`,
535
+ ...Array.from({ length: input.destinations ?? 0 }, (_unused, index) => [
536
+ "--forward",
537
+ `${DESTINATION_BASE_PORT + index}=${JOB_MOUNT}/dest-${index}.sock`
538
+ ]).flat(),
539
+ ...[
540
+ "--forward",
541
+ `${GATEWAY_PORT}=${JOB_MOUNT}/${GATEWAY_SOCKET}`,
542
+ "--forward",
543
+ `${CLAUDE_PORT}=${JOB_MOUNT}/${CLAUDE_SOCKET}`
544
+ ]
545
+ ];
546
+ }
547
+ function prepareRunArgs(input) {
548
+ return [
549
+ "--rm",
550
+ ...hardened(input),
551
+ "--mount",
552
+ `type=volume,src=${input.volume},dst=/build`,
553
+ "--entrypoint",
554
+ "mkdir",
555
+ input.image,
556
+ "-p",
557
+ "-m",
558
+ "1777",
559
+ "/build/tmp",
560
+ "/build/home",
561
+ "/build/work"
562
+ ];
563
+ }
564
+ function containerExecArgs(input) {
565
+ const literals = Object.keys(input.env.literals).sort().flatMap((name) => ["-e", `${name}=${input.env.literals[name]}`]);
566
+ const passed = Object.keys(input.env.passed).filter((name) => input.env.passed[name] !== void 0).sort().flatMap((name) => ["-e", name]);
567
+ return [
568
+ "-u",
569
+ input.user,
570
+ ...input.workdir === void 0 ? [] : ["-w", input.workdir],
571
+ ...literals,
572
+ ...passed,
573
+ input.container,
574
+ ...input.argv
575
+ ];
576
+ }
359
577
  var AssetError = class extends Error {
360
578
  constructor(file, message) {
361
579
  super(message);
@@ -496,7 +714,7 @@ function converterArgs(dest, extra, script) {
496
714
  }
497
715
  var firstLine2 = (text) => text.trim().split("\n")[0] ?? "";
498
716
  function exportInto(container, converter) {
499
- return new Promise((resolve, reject) => {
717
+ return new Promise((resolve2, reject) => {
500
718
  const exporter = spawn("docker", ["export", container], { stdio: ["ignore", "pipe", "pipe"], env: childEnvironment(process.env) });
501
719
  const writer = spawn("docker", converter, { stdio: ["pipe", "ignore", "pipe"], env: childEnvironment(process.env) });
502
720
  const errors = [];
@@ -513,7 +731,7 @@ function exportInto(container, converter) {
513
731
  if (code !== 0 && failure === void 0) failure = `${name} exited ${code}`;
514
732
  exits += 1;
515
733
  if (exits < 2) return;
516
- if (failure === void 0) resolve();
734
+ if (failure === void 0) resolve2();
517
735
  else reject(new Error(`${failure}: ${firstLine2(errors.join("\n"))}`));
518
736
  };
519
737
  exporter.once("exit", (code) => done("docker export", code));
@@ -634,7 +852,9 @@ function machineConfig(limits2, paths) {
634
852
  };
635
853
  }
636
854
  function firecrackerCommand(input) {
637
- return { command: input.binary, args: ["--no-api", "--config-file", input.configPath] };
855
+ const args = ["--no-api", "--config-file", input.configPath];
856
+ if (input.cgroup === void 0) return { command: input.binary, args };
857
+ return { command: "sh", args: ["-c", 'echo $$ > "$1/cgroup.procs" && shift && exec "$@"', "sh", input.cgroup, input.binary, ...args] };
638
858
  }
639
859
  var STOP_WAIT_MS = 1e4;
640
860
  var POLL_MS = 20;
@@ -660,7 +880,7 @@ function createFirecrackerHost(options = {}) {
660
880
  const failed = new Promise((_resolve, reject) => child.once("error", reject));
661
881
  const pid = child.pid;
662
882
  if (pid === void 0) return await failed;
663
- exits.set(pid, new Promise((resolve) => child.once("exit", () => resolve())));
883
+ exits.set(pid, new Promise((resolve2) => child.once("exit", () => resolve2())));
664
884
  child.unref();
665
885
  const start = startTimeOf(pid);
666
886
  if (start === null) throw new Error(`${command} exited as soon as it started; its console is at ${input.consolePath}`);
@@ -677,8 +897,8 @@ function createFirecrackerHost(options = {}) {
677
897
  }
678
898
  const exited = exits.get(vm.pid);
679
899
  const deadline = Date.now() + STOP_WAIT_MS;
680
- if (exited !== void 0) await Promise.race([exited, new Promise((resolve) => setTimeout(resolve, STOP_WAIT_MS))]);
681
- while (alive(vm) && Date.now() < deadline) await new Promise((resolve) => setTimeout(resolve, POLL_MS));
900
+ if (exited !== void 0) await Promise.race([exited, new Promise((resolve2) => setTimeout(resolve2, STOP_WAIT_MS))]);
901
+ while (alive(vm) && Date.now() < deadline) await new Promise((resolve2) => setTimeout(resolve2, POLL_MS));
682
902
  exits.delete(vm.pid);
683
903
  },
684
904
  alive
@@ -688,9 +908,10 @@ function createFirecrackerHost(options = {}) {
688
908
  // src/runner/microvm/host.ts
689
909
  function createMicroVmPorts(over = {}) {
690
910
  return {
691
- assets: over.assets ?? ((runnerDir) => ensureAssets({ runnerDir })),
911
+ assets: over.assets ?? ((runnerDir2) => ensureAssets({ runnerDir: runnerDir2 })),
692
912
  rootfs: over.rootfs ?? createRootfsHost(),
693
913
  firecracker: over.firecracker ?? createFirecrackerHost(),
914
+ cgroup: over.cgroup ?? realCgroupPort(),
694
915
  exec: over.exec ?? execCapture
695
916
  };
696
917
  }
@@ -845,104 +1066,6 @@ function removeStagedContext(buildDir2) {
845
1066
  rmSync(stagedContextDir(buildDir2), { recursive: true, force: true });
846
1067
  }
847
1068
 
848
- // src/runner/container/run-args.ts
849
- var BUILD_LABEL = "ai.outerlayer.build";
850
- function hardened(input) {
851
- const { memory, cpus, pids } = input.limits;
852
- return [
853
- "--label",
854
- `${BUILD_LABEL}=${input.buildId}`,
855
- "--network",
856
- "none",
857
- "--cap-drop",
858
- "ALL",
859
- "--security-opt",
860
- "no-new-privileges",
861
- "--read-only",
862
- "--user",
863
- input.user,
864
- // The same figure for swap as for memory leaves the container none.
865
- "--memory",
866
- memory,
867
- "--memory-swap",
868
- memory,
869
- "--cpus",
870
- String(cpus),
871
- "--pids-limit",
872
- String(pids)
873
- ];
874
- }
875
- function mountPath(path) {
876
- if (/[",]/.test(path)) throw new Error(`${path} holds a comma or a quote, which a Docker mount cannot carry; move the runner's config file`);
877
- return path;
878
- }
879
- function containerRunArgs(input) {
880
- return [
881
- "--detach",
882
- "--name",
883
- input.container,
884
- ...hardened(input),
885
- "--mount",
886
- `type=volume,src=${input.volume},dst=/build`,
887
- // `/tmp` is a directory of the same volume, so it is measured and limited with the rest of the build.
888
- "--mount",
889
- `type=volume,src=${input.volume},dst=/tmp,volume-subpath=tmp`,
890
- "--mount",
891
- `type=bind,src=${mountPath(input.buildDir)},dst=${JOB_MOUNT},readonly`,
892
- "--entrypoint",
893
- "/opt/outerlayer/bin/tini",
894
- input.image,
895
- "--",
896
- "/opt/outerlayer/bin/node",
897
- "/opt/outerlayer/cli/dist/index.js",
898
- "broker-relay",
899
- "--port",
900
- String(RELAY_PORT),
901
- "--socket",
902
- `${JOB_MOUNT}/tunnel.sock`,
903
- ...Array.from({ length: input.destinations ?? 0 }, (_unused, index) => [
904
- "--forward",
905
- `${DESTINATION_BASE_PORT + index}=${JOB_MOUNT}/dest-${index}.sock`
906
- ]).flat(),
907
- ...[
908
- "--forward",
909
- `${GATEWAY_PORT}=${JOB_MOUNT}/${GATEWAY_SOCKET}`,
910
- "--forward",
911
- `${CLAUDE_PORT}=${JOB_MOUNT}/${CLAUDE_SOCKET}`
912
- ]
913
- ];
914
- }
915
- function prepareRunArgs(input) {
916
- return [
917
- "--rm",
918
- ...hardened(input),
919
- "--mount",
920
- `type=volume,src=${input.volume},dst=/build`,
921
- "--entrypoint",
922
- "mkdir",
923
- input.image,
924
- "-p",
925
- "-m",
926
- "1777",
927
- "/build/tmp",
928
- "/build/home",
929
- "/build/work"
930
- ];
931
- }
932
- function containerExecArgs(input) {
933
- const literals = Object.keys(input.env.literals).sort().flatMap((name) => ["-e", `${name}=${input.env.literals[name]}`]);
934
- const passed = Object.keys(input.env.passed).filter((name) => input.env.passed[name] !== void 0).sort().flatMap((name) => ["-e", name]);
935
- return [
936
- "-u",
937
- input.user,
938
- ...input.workdir === void 0 ? [] : ["-w", input.workdir],
939
- ...literals,
940
- ...passed,
941
- input.container,
942
- ...input.argv
943
- ];
944
- }
945
-
946
1069
  // src/runner/container/docker.ts
947
1070
  function createContainerHost(deps = {}) {
948
1071
  const run = deps.run ?? spawnRun;
@@ -1027,9 +1150,9 @@ async function git(args, cwd, env, input) {
1027
1150
  if (size < maxOutputBytes) out.push(chunk);
1028
1151
  size += chunk.length;
1029
1152
  });
1030
- const closed = new Promise((resolve, reject) => {
1153
+ const closed = new Promise((resolve2, reject) => {
1031
1154
  child.once("error", reject);
1032
- child.once("close", (status) => resolve(status));
1155
+ child.once("close", (status) => resolve2(status));
1033
1156
  });
1034
1157
  if (input !== void 0) {
1035
1158
  await pipeline(input, child.stdin).catch(() => void 0);
@@ -1078,7 +1201,7 @@ function decode(prefix, gzip) {
1078
1201
  if (!gzip) return prefix;
1079
1202
  try {
1080
1203
  const { compressedWindow, inflatedBytes } = limits();
1081
- return gunzipSync(prefix.subarray(0, compressedWindow), { finishFlush: constants.Z_SYNC_FLUSH, maxOutputLength: inflatedBytes });
1204
+ return gunzipSync(prefix.subarray(0, compressedWindow), { finishFlush: constants$1.Z_SYNC_FLUSH, maxOutputLength: inflatedBytes });
1082
1205
  } catch (err) {
1083
1206
  const code = err.code;
1084
1207
  if (code === "ERR_BUFFER_TOO_LARGE") return refused(TOO_LONG);
@@ -1226,14 +1349,14 @@ async function drain(req) {
1226
1349
  }
1227
1350
  }
1228
1351
  function readPrefix(req, gzip) {
1229
- return new Promise((resolve, reject) => {
1352
+ return new Promise((resolve2, reject) => {
1230
1353
  const held = [];
1231
1354
  const finish = (section) => {
1232
1355
  req.off("data", onData);
1233
1356
  req.off("end", onEnd);
1234
1357
  req.off("error", reject);
1235
1358
  req.pause();
1236
- resolve({ held, section });
1359
+ resolve2({ held, section });
1237
1360
  };
1238
1361
  const onData = (chunk) => {
1239
1362
  held.push(chunk);
@@ -1247,19 +1370,19 @@ function readPrefix(req, gzip) {
1247
1370
  });
1248
1371
  }
1249
1372
  function readBody(req, limit) {
1250
- return new Promise((resolve, reject) => {
1373
+ return new Promise((resolve2, reject) => {
1251
1374
  const chunks = [];
1252
1375
  let size = 0;
1253
1376
  req.on("data", (chunk) => {
1254
1377
  size += chunk.length;
1255
1378
  if (size > limit) {
1256
1379
  req.pause();
1257
- resolve(void 0);
1380
+ resolve2(void 0);
1258
1381
  return;
1259
1382
  }
1260
1383
  chunks.push(chunk);
1261
1384
  });
1262
- req.once("end", () => resolve(Buffer.concat(chunks)));
1385
+ req.once("end", () => resolve2(Buffer.concat(chunks)));
1263
1386
  req.once("error", reject);
1264
1387
  });
1265
1388
  }
@@ -1272,7 +1395,7 @@ function createGitRemote(options) {
1272
1395
  const { route, res } = input;
1273
1396
  const url = new URL(`${gitUrl}/${route.owner}/${route.name}.git/${route.kind === "info-refs" ? "info/refs" : route.kind === "lfs-batch" ? "info/lfs/objects/batch" : `git-${route.kind}`}${route.search === "" ? "" : `?${route.search}`}`);
1274
1397
  const send = url.protocol === "https:" ? request$1 : request;
1275
- return new Promise((resolve) => {
1398
+ return new Promise((resolve2) => {
1276
1399
  const upstream = send(url, { method: input.method, headers: { ...input.headers, connection: "close" } }, (answer) => {
1277
1400
  const headers = {};
1278
1401
  for (const [name, value] of Object.entries(answer.headers)) {
@@ -1294,18 +1417,18 @@ function createGitRemote(options) {
1294
1417
  });
1295
1418
  answer.once("end", () => {
1296
1419
  res.end();
1297
- resolve();
1420
+ resolve2();
1298
1421
  });
1299
1422
  answer.once("error", () => {
1300
1423
  res.destroy();
1301
- resolve();
1424
+ resolve2();
1302
1425
  });
1303
1426
  });
1304
1427
  upstream.on("error", (err) => {
1305
1428
  log(`could not reach GitHub for ${route.kind}: ${err.message}`);
1306
1429
  if (res.headersSent) res.destroy();
1307
1430
  else plain(res, 502, "the runner could not reach GitHub\n");
1308
- resolve();
1431
+ resolve2();
1309
1432
  });
1310
1433
  res.once("close", () => upstream.destroy());
1311
1434
  input.feed(upstream);
@@ -1594,7 +1717,7 @@ async function startTunnel(socketPath, opts = {}) {
1594
1717
  if (Buffer.byteLength(socketPath) > MAX_SOCKET_PATH_BYTES) {
1595
1718
  throw new Error(`${socketPath} is too long for a Unix socket; keep the runner's config file nearer the root of the filesystem`);
1596
1719
  }
1597
- const resolve = opts.resolve ?? systemResolve;
1720
+ const resolve2 = opts.resolve ?? systemResolve;
1598
1721
  const openConnection = opts.connect ?? ((t) => connect({ host: t.address, port: t.port }));
1599
1722
  const ownAddresses = opts.hostAddresses ?? systemHostAddresses;
1600
1723
  const connectTimeoutMs = opts.connectTimeoutMs ?? CONNECT_TIMEOUT_MS;
@@ -1678,7 +1801,7 @@ async function startTunnel(socketPath, opts = {}) {
1678
1801
  }
1679
1802
  let addresses;
1680
1803
  try {
1681
- addresses = await resolve(host);
1804
+ addresses = await resolve2(host);
1682
1805
  } catch {
1683
1806
  return { ok: false, status: 502, body: `could not resolve ${host}
1684
1807
  ` };
@@ -1863,7 +1986,7 @@ function createContainerRuntime(opts = {}) {
1863
1986
  )),
1864
1987
  startTunnel: opts.startTunnel ?? startTunnel,
1865
1988
  tunnelOptions: opts.tunnelOptions,
1866
- sleep: opts.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms))),
1989
+ sleep: opts.sleep ?? ((ms) => new Promise((resolve2) => setTimeout(resolve2, ms))),
1867
1990
  hostEnv: opts.hostEnv ?? process.env,
1868
1991
  credentials: opts.credentials ?? /* @__PURE__ */ new Map(),
1869
1992
  github: { gitUrl: opts.github?.gitUrl ?? "https://github.com", apiUrl: opts.github?.apiUrl ?? "https://api.github.com" },
@@ -2035,11 +2158,13 @@ async function provisionContainer(input) {
2035
2158
  return bail(`the tunnel could not start: ${describeCallFailure(err)}`);
2036
2159
  }
2037
2160
  const limits2 = { memory: config.build?.memory ?? "8g", cpus: config.build?.cpus ?? 4, pids: config.build?.pids ?? 4096 };
2038
- const volume = await host.createVolume(name, { [BUILD_LABEL]: buildId });
2161
+ const runnerId = runnerLabelValue(config.configPath);
2162
+ const volume = await host.createVolume(name, { [BUILD_LABEL]: buildId, [RUNNER_LABEL]: runnerId });
2039
2163
  if (volume.status !== 0) return dockerFailed("creating the build's volume", volume);
2040
2164
  if (input.aborted()) return bail("", true);
2041
2165
  const runInput = {
2042
2166
  buildId,
2167
+ runnerId,
2043
2168
  container: name,
2044
2169
  volume: name,
2045
2170
  buildDir: buildDir2,
@@ -2132,6 +2257,40 @@ function containerCommandLine(input) {
2132
2257
  env: { ...childEnvironment(input.runtime.hostEnv), ...env.passed }
2133
2258
  };
2134
2259
  }
2260
+ function containerSenderCommand(input) {
2261
+ const state = input.job.container;
2262
+ const env = buildEnvironment({
2263
+ config: input.config,
2264
+ item: input.job.item,
2265
+ itemKey: input.job.itemKey,
2266
+ branch: input.job.branch,
2267
+ repository: state.repository,
2268
+ hostEnv: input.runtime.hostEnv,
2269
+ containerEnv: state.containerEnv ?? {},
2270
+ builtIns: state.builtIns === true,
2271
+ identity: input.job.commitIdentity
2272
+ });
2273
+ const args = containerExecArgs({
2274
+ container: state.container,
2275
+ user: state.agentUser ?? ADDED_USER,
2276
+ workdir: CHECKOUT_DIR,
2277
+ env,
2278
+ argv: ["outerlayer", "session-sender"]
2279
+ });
2280
+ return { command: "docker", args: ["exec", ...args], env: { ...childEnvironment(input.runtime.hostEnv), ...env.passed } };
2281
+ }
2282
+ function senderSignalScript(pidfile, signal) {
2283
+ const file = shellQuote(pidfile);
2284
+ return `p="$(cat ${file})" && [ -n "$p" ] && grep -qa session-sender "/proc/$p/cmdline" && kill -${signal} "$p"`;
2285
+ }
2286
+ async function signalInContainer(runtime, state, pidfile, signal) {
2287
+ await runtime.host.exec({
2288
+ container: state.container,
2289
+ user: state.agentUser ?? ADDED_USER,
2290
+ env: NO_ENV,
2291
+ argv: ["/bin/sh", "-c", senderSignalScript(pidfile, signal)]
2292
+ });
2293
+ }
2135
2294
  async function stopContainer(runtime, state) {
2136
2295
  await runtime.host.stop(state.container);
2137
2296
  state.stopped = true;
@@ -2230,4 +2389,4 @@ async function cleanupContainer(runtime, state, jobDir) {
2230
2389
  if (failures.length > 0) throw new Error(failures.join("; "));
2231
2390
  }
2232
2391
 
2233
- export { ADDED_USER, AgentUserError, AssetError, BuildTokens, GH_CONFIG_YML, MIN_ENGINE_MAJOR, classifyRefusal, cleanupContainer, containerCommandLine, createBuildDisk, createContainerRuntime, createRunnerLog, deleteBuildDisk, describeCallFailure, ensureRootfs, ensureTunnel, formatClock, formatDuration, ghHostsYml, gitRemoteFor, isLiteralAddress, keepGhConfigCurrent, lifecycleFailureReason, lifecycleSteps, machineConfig, measureDisk, obtainImage, prepareBranch, provisionContainer, refusalCode, relativeToRunner, removeStagedContext, resolveAgentUser, retrySend, runBuildSteps, shellQuote, shortClaim, stageGovernedContext, startServicesFor, stopContainer, syncInContainer };
2392
+ export { ADDED_USER, AgentUserError, AssetError, BUILD_LABEL, BuildTokens, GH_CONFIG_YML, MIN_ENGINE_MAJOR, RUNNER_LABEL, classifyRefusal, cleanupContainer, containerCommandLine, containerSenderCommand, createBuildDisk, createContainerRuntime, createRunnerLog, deleteBuildDisk, describeCallFailure, ensureRootfs, ensureTunnel, formatClock, formatDuration, ghHostsYml, gitRemoteFor, isLiteralAddress, keepGhConfigCurrent, lifecycleFailureReason, lifecycleSteps, machineConfig, measureDisk, obtainImage, prepareBranch, provisionContainer, refusalCode, relativeToRunner, removeStagedContext, resolveAgentUser, retrySend, runBuildSteps, runnerLabelValue, senderSignalScript, setUpRunnerCgroup, shellQuote, shortClaim, signalInContainer, stageGovernedContext, startServicesFor, stopContainer, syncInContainer };
@@ -0,0 +1,31 @@
1
+ #!/usr/bin/env node
2
+ import { createRequire } from 'node:module';
3
+ import { RUNNER_DIR_MODE, RUNNER_FILE_MODE } from './chunk-ZM2IMMYN.js';
4
+ import { existsSync, readFileSync, mkdirSync, writeFileSync } from 'fs';
5
+ import { dirname } from 'path';
6
+
7
+ createRequire(import.meta.url);
8
+ function readJobFile(path) {
9
+ if (!existsSync(path)) return null;
10
+ try {
11
+ return JSON.parse(readFileSync(path, "utf8"));
12
+ } catch {
13
+ return null;
14
+ }
15
+ }
16
+ function readJobFileChecked(path) {
17
+ if (!existsSync(path)) return { kind: "missing" };
18
+ try {
19
+ const parsed = JSON.parse(readFileSync(path, "utf8"));
20
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return { kind: "unreadable" };
21
+ return { kind: "ok", job: parsed };
22
+ } catch {
23
+ return { kind: "unreadable" };
24
+ }
25
+ }
26
+ function writeJobFile(path, job) {
27
+ mkdirSync(dirname(path), { recursive: true, mode: RUNNER_DIR_MODE });
28
+ writeFileSync(path, JSON.stringify(job, null, 2), { mode: RUNNER_FILE_MODE });
29
+ }
30
+
31
+ export { readJobFile, readJobFileChecked, writeJobFile };
@@ -0,0 +1,34 @@
1
+ #!/usr/bin/env node
2
+ import { createRequire } from 'node:module';
3
+ import { RUNNER_DIR_MODE, RUNNER_FILE_MODE } from './chunk-ZM2IMMYN.js';
4
+ import { readFileSync, mkdirSync, writeFileSync, renameSync } from 'fs';
5
+ import { join } from 'path';
6
+
7
+ createRequire(import.meta.url);
8
+ var FILE = "housekeeping.json";
9
+ function readHousekeepingRecord(runnerDirPath) {
10
+ const path = join(runnerDirPath, FILE);
11
+ try {
12
+ const raw = JSON.parse(readFileSync(path, "utf8"));
13
+ const record = {};
14
+ if (typeof raw.lastRunAt === "string") record.lastRunAt = raw.lastRunAt;
15
+ if (typeof raw.freedBytes === "number") record.freedBytes = raw.freedBytes;
16
+ const holding = raw.holding;
17
+ if (typeof holding?.free === "number" && typeof holding.needed === "number" && typeof holding.since === "string") {
18
+ record.holding = { free: holding.free, needed: holding.needed, since: holding.since };
19
+ }
20
+ return record;
21
+ } catch {
22
+ return {};
23
+ }
24
+ }
25
+ function writeHousekeepingRecord(runnerDirPath, patch) {
26
+ const merged = { ...readHousekeepingRecord(runnerDirPath), ...patch };
27
+ mkdirSync(runnerDirPath, { recursive: true, mode: RUNNER_DIR_MODE });
28
+ const path = join(runnerDirPath, FILE);
29
+ const temp = `${path}.partial-${process.pid}`;
30
+ writeFileSync(temp, JSON.stringify(merged, null, 2), { mode: RUNNER_FILE_MODE });
31
+ renameSync(temp, path);
32
+ }
33
+
34
+ export { readHousekeepingRecord, writeHousekeepingRecord };