@archme/cli 0.15.0 → 0.15.1

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/cli.js CHANGED
@@ -10110,6 +10110,39 @@ function buildInitVolumeArgs(volume, uid, labels, image, slot) {
10110
10110
  "/work"
10111
10111
  ];
10112
10112
  }
10113
+ function buildAnchorArgs(slot, image) {
10114
+ assertImage(image);
10115
+ const network = slotNetwork(slot);
10116
+ return [
10117
+ "run",
10118
+ "-d",
10119
+ "--rm",
10120
+ "--init",
10121
+ "--read-only",
10122
+ "--cap-drop",
10123
+ "ALL",
10124
+ "--user",
10125
+ `${BOX_UID}:${BOX_UID}`,
10126
+ "--no-dns",
10127
+ "--kernel-arg",
10128
+ "ipv6.disable=1",
10129
+ "--progress",
10130
+ "none",
10131
+ "--cpus",
10132
+ "1",
10133
+ "--memory",
10134
+ `${INIT_MEMORY_MIB}M`,
10135
+ "--name",
10136
+ anchorBoxName(slot),
10137
+ ...labelArgs({ [JOB_LABEL]: "anchor", [SLOT_LABEL]: String(slot.index) }),
10138
+ "--network",
10139
+ network,
10140
+ "--entrypoint",
10141
+ "sleep",
10142
+ image,
10143
+ "infinity"
10144
+ ];
10145
+ }
10113
10146
  function buildCreateVolumeArgs(name, sizeGiB, labels) {
10114
10147
  assertName("volume", name);
10115
10148
  if (!Number.isInteger(sizeGiB) || sizeGiB < 1) throw new Error(`sandbox: invalid volume size ${sizeGiB}`);
@@ -10200,7 +10233,7 @@ function defaultSpawn(bin = "container") {
10200
10233
  };
10201
10234
  };
10202
10235
  }
10203
- var MIN_MEMORY_MIB, STDOUT_CAP_BYTES, STDERR_CAP_BYTES, INIT_MEMORY_MIB, NAME_RE, ENV_KEY_RE, LABEL_KEY_RE, CONTROL_RE, obj, str, TailBuffer, AppleContainerRuntime;
10236
+ var MIN_MEMORY_MIB, STDOUT_CAP_BYTES, STDERR_CAP_BYTES, INIT_MEMORY_MIB, NAME_RE, ENV_KEY_RE, LABEL_KEY_RE, CONTROL_RE, anchorBoxName, obj, str, TailBuffer, AppleContainerRuntime;
10204
10237
  var init_appleContainer = __esm({
10205
10238
  "src/daemon/sandbox/appleContainer.ts"() {
10206
10239
  "use strict";
@@ -10215,6 +10248,7 @@ var init_appleContainer = __esm({
10215
10248
  ENV_KEY_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
10216
10249
  LABEL_KEY_RE = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
10217
10250
  CONTROL_RE = /[\u0000\n\r]/;
10251
+ anchorBoxName = (slot) => `archme-anchor-${slot.index}`;
10218
10252
  obj = (v) => v && typeof v === "object" ? v : {};
10219
10253
  str = (v) => typeof v === "string" ? v : null;
10220
10254
  TailBuffer = class {
@@ -10324,6 +10358,11 @@ var init_appleContainer = __esm({
10324
10358
  }
10325
10359
  return slots;
10326
10360
  }
10361
+ /** Starts the slot's anchor box detached; resolves once it is running. */
10362
+ async startAnchor(slot, image) {
10363
+ await this.must(buildAnchorArgs(slot, image), `anchor ${slot.network}`);
10364
+ return anchorBoxName(slot);
10365
+ }
10327
10366
  async createVolume(name, sizeGiB, labels) {
10328
10367
  await this.must(buildCreateVolumeArgs(name, sizeGiB, labels), `volume create ${name}`);
10329
10368
  }
@@ -17167,6 +17206,99 @@ var init_semaphore = __esm({
17167
17206
  }
17168
17207
  });
17169
17208
 
17209
+ // src/daemon/sandbox/bridges.ts
17210
+ function holding(gateway, ifaces) {
17211
+ return Object.entries(ifaces).filter(([, addrs]) => (addrs ?? []).some((a2) => a2.family === "IPv4" && a2.address === gateway)).map(([name]) => name);
17212
+ }
17213
+ function gatewayPresent(slot, ifaces) {
17214
+ return holding(slot.gateway, ifaces).length > 0;
17215
+ }
17216
+ function bridgeMapFailures(slots, ifaces) {
17217
+ const failures = [];
17218
+ const owner = /* @__PURE__ */ new Map();
17219
+ for (const s of slots) {
17220
+ const on6 = holding(s.gateway, ifaces);
17221
+ if (on6.length === 0) {
17222
+ failures.push(`bridge: slot ${s.index} gateway ${s.gateway} is on no host interface`);
17223
+ continue;
17224
+ }
17225
+ for (const name of on6) {
17226
+ if (!/^bridge\d+$/.test(name)) failures.push(`bridge: slot ${s.index} gateway is on ${name}, not a bridge`);
17227
+ const other = owner.get(name);
17228
+ if (other !== void 0) failures.push(`bridge: slots ${other} and ${s.index} share ${name}`);
17229
+ else owner.set(name, s.index);
17230
+ }
17231
+ }
17232
+ return failures;
17233
+ }
17234
+ function helperNetworks() {
17235
+ return ["default", ...Array.from({ length: MAX_SLOTS }, (_, i2) => slotFor(i2).network)];
17236
+ }
17237
+ function parseHelperPids(ps, networks) {
17238
+ return ps.split("\n").flatMap((line) => {
17239
+ const m = HELPER_RE.exec(line);
17240
+ return m && networks.has(m[2]) ? [Number(m[1])] : [];
17241
+ });
17242
+ }
17243
+ async function resetNetworkHelpers(networks, deps) {
17244
+ const want = new Set(networks);
17245
+ const pids = parseHelperPids(await deps.ps(), want);
17246
+ const signal = (sig, list2) => {
17247
+ for (const pid of list2) {
17248
+ try {
17249
+ deps.kill(pid, sig);
17250
+ } catch {
17251
+ }
17252
+ }
17253
+ };
17254
+ const survivors = async () => {
17255
+ const now = new Set(parseHelperPids(await deps.ps(), want));
17256
+ return pids.filter((p) => now.has(p));
17257
+ };
17258
+ const waitGone = async () => {
17259
+ let left2 = await survivors();
17260
+ for (let i2 = 0; i2 < 20 && left2.length > 0; i2++) {
17261
+ await deps.sleep(250);
17262
+ left2 = await survivors();
17263
+ }
17264
+ return left2;
17265
+ };
17266
+ signal("SIGTERM", pids);
17267
+ let left = await waitGone();
17268
+ if (left.length > 0) {
17269
+ signal("SIGKILL", left);
17270
+ left = await waitGone();
17271
+ }
17272
+ if (left.length > 0) throw new Error(`network helpers still running: ${left.join(", ")}`);
17273
+ return pids.length;
17274
+ }
17275
+ function realResetHelpers(networks) {
17276
+ return resetNetworkHelpers(networks, {
17277
+ ps: async () => (await execa("ps", ["-axo", "pid=,command="])).stdout,
17278
+ kill: (pid, sig) => process.kill(pid, sig),
17279
+ sleep: (ms) => new Promise((r) => setTimeout(r, ms))
17280
+ });
17281
+ }
17282
+ async function pinBridges(slots, deps) {
17283
+ const killed = await deps.resetHelpers(helperNetworks());
17284
+ deps.log(` sandbox: restarted ${killed} network helper(s); anchoring ${slots.length} slot bridge(s)`);
17285
+ for (const s of slots) {
17286
+ if (!await deps.register(anchorBoxName(s))) throw new Error("shutdown began while anchoring");
17287
+ await deps.startAnchor(s);
17288
+ }
17289
+ }
17290
+ var ANCHOR_REGISTRY_ID, HELPER_RE;
17291
+ var init_bridges = __esm({
17292
+ "src/daemon/sandbox/bridges.ts"() {
17293
+ "use strict";
17294
+ init_execa();
17295
+ init_appleContainer();
17296
+ init_types();
17297
+ ANCHOR_REGISTRY_ID = "archme-anchors";
17298
+ HELPER_RE = /^\s*(\d+)\s+\S*\/container-network-vmnet start --id (\S+)(?:\s|$)/;
17299
+ }
17300
+ });
17301
+
17170
17302
  // src/scan/sessionAuth.ts
17171
17303
  var SessionAuth;
17172
17304
  var init_sessionAuth = __esm({
@@ -19942,6 +20074,13 @@ function sandboxBootFailurePolicy(mode) {
19942
20074
  function shutdownKillTargets(boxedJobs2, registry) {
19943
20075
  return [...boxedJobs2].filter((id) => registry.hasLiveBoxes(id));
19944
20076
  }
20077
+ function withBridgeCheck(probe, slots, bridgeFailures) {
20078
+ return async (slot) => {
20079
+ const report = await probe(slot);
20080
+ const failures = bridgeFailures(slots);
20081
+ return failures.length ? { ...report, ok: false, failures: [...report.failures, ...failures] } : report;
20082
+ };
20083
+ }
19945
20084
  async function bootSandbox(settings, parts, deps) {
19946
20085
  const sweep = await parts.runtime.sweep();
19947
20086
  const inputDirs = await (deps.sweepInputDirs ?? sweepInputDirs)();
@@ -19977,13 +20116,31 @@ async function bootSandbox(settings, parts, deps) {
19977
20116
  });
19978
20117
  throw new SandboxBootError(`sandbox preflight failed: ${failedChecks(preflight)}`);
19979
20118
  }
19980
- const probe = deps.probe ?? ((slot) => runConfinementProbe({ runtime: parts.runtime, image: settings.image, log: deps.log }, slot, primaryLanIpv4(networkInterfaces())));
20119
+ const pin = deps.pinBridges ?? ((s) => pinBridges(s, {
20120
+ resetHelpers: realResetHelpers,
20121
+ startAnchor: (slot) => parts.runtime.startAnchor(slot, settings.image),
20122
+ register: (box) => parts.registry.beginStep(ANCHOR_REGISTRY_ID, box),
20123
+ log: deps.log
20124
+ }));
20125
+ let pinError = null;
20126
+ try {
20127
+ await pin(slots);
20128
+ } catch (e) {
20129
+ pinError = e.message;
20130
+ deps.log(` sandbox: could not pin the slot bridges: ${pinError}`);
20131
+ }
20132
+ const probe = withBridgeCheck(
20133
+ deps.probe ?? ((slot) => runConfinementProbe({ runtime: parts.runtime, image: settings.image, log: deps.log }, slot, primaryLanIpv4(networkInterfaces()))),
20134
+ slots,
20135
+ deps.bridgeFailures ?? ((s) => bridgeMapFailures(s, networkInterfaces()))
20136
+ );
19981
20137
  let report;
19982
20138
  try {
19983
20139
  report = await probe(slots[0]);
19984
20140
  } catch (e) {
19985
20141
  report = { ok: false, slot: 0, failures: [`probe threw: ${e.message}`], outcomes: {}, ms: 0 };
19986
20142
  }
20143
+ if (pinError) report = { ...report, ok: false, failures: [...report.failures, `bridge pinning: ${pinError}`] };
19987
20144
  preflight = {
19988
20145
  ok: preflight.ok && report.ok,
19989
20146
  checks: [...preflight.checks, { name: "confinement_probe", ok: report.ok, detail: report.ok ? void 0 : report.failures.join("; ") }]
@@ -20013,6 +20170,7 @@ var init_host = __esm({
20013
20170
  "use strict";
20014
20171
  init_confinement();
20015
20172
  init_appleContainer();
20173
+ init_bridges();
20016
20174
  init_claudeToken();
20017
20175
  init_inputDir();
20018
20176
  init_types();
@@ -20121,9 +20279,13 @@ async function runSandboxedJob(deps, job, opts) {
20121
20279
  if (spec2.args[0] === "agent") agentStarted = true;
20122
20280
  const handle = await deps.runtime.runStep(spec2);
20123
20281
  const timer = setTimeout(() => kill("timeout"), timeoutMs);
20282
+ let gatewaySeen = false;
20124
20283
  const poll = setInterval(() => {
20125
20284
  const c3 = currentKillCause();
20126
- if (c3) kill(c3);
20285
+ if (c3) return kill(c3);
20286
+ if (!deps.gatewayPresent) return;
20287
+ if (deps.gatewayPresent(slot)) gatewaySeen = true;
20288
+ else if (gatewaySeen) kill("network_lost");
20127
20289
  }, pollMs);
20128
20290
  try {
20129
20291
  const exit = await handle.done;
@@ -22480,7 +22642,7 @@ import { randomUUID as randomUUID3 } from "node:crypto";
22480
22642
  import { mkdtemp as mkdtemp4, mkdir as mkdir16, writeFile as writeFile15, rm as rm8, stat as stat8, chmod as chmod6 } from "node:fs/promises";
22481
22643
  import { tmpdir as tmpdir4 } from "node:os";
22482
22644
  import { join as join25 } from "node:path";
22483
- import { homedir as homedir8 } from "node:os";
22645
+ import { homedir as homedir8, networkInterfaces as networkInterfaces2 } from "node:os";
22484
22646
  function attributeKillCause(result, recorded) {
22485
22647
  if (recorded) return recorded;
22486
22648
  if (result.signal === "SIGTERM") return "timeout";
@@ -23376,7 +23538,8 @@ function sandboxDepsFor(ctx, job, deviceId, host) {
23376
23538
  log: (msg) => console.log(`[${job.id}] ${msg}`),
23377
23539
  isCancelled: (id) => cancelled.has(id),
23378
23540
  killReason: (id) => killReasons.get(id),
23379
- isLeaseLost: (id) => leaseLost.has(id)
23541
+ isLeaseLost: (id) => leaseLost.has(id),
23542
+ gatewayPresent: (slot) => gatewayPresent(slot, networkInterfaces2())
23380
23543
  };
23381
23544
  }
23382
23545
  async function completeClaudeRun(ctx, job, deviceId, run) {
@@ -24552,6 +24715,7 @@ var init_run = __esm({
24552
24715
  init_supervision();
24553
24716
  init_paths();
24554
24717
  init_semaphore();
24718
+ init_bridges();
24555
24719
  init_bearer();
24556
24720
  init_credentials();
24557
24721
  init_sessionAuth();
@@ -25022,7 +25186,7 @@ __export(setupIo_exports, {
25022
25186
  createSetupIo: () => createSetupIo
25023
25187
  });
25024
25188
  import { mkdtemp as mkdtemp5, readFile as readFile16, writeFile as writeFile16 } from "node:fs/promises";
25025
- import { networkInterfaces as networkInterfaces2, tmpdir as tmpdir5 } from "node:os";
25189
+ import { networkInterfaces as networkInterfaces3, tmpdir as tmpdir5 } from "node:os";
25026
25190
  import { join as join26 } from "node:path";
25027
25191
  import { createInterface as createInterface2 } from "node:readline";
25028
25192
  import { Writable as Writable4 } from "node:stream";
@@ -25109,7 +25273,7 @@ function createSetupIo(env = process.env) {
25109
25273
  console.log(" (the daemon's gatekeepers are answering)");
25110
25274
  }
25111
25275
  try {
25112
- return await runConfinementProbe({ runtime, image }, slot, primaryLanIpv4(networkInterfaces2()));
25276
+ return await runConfinementProbe({ runtime, image }, slot, primaryLanIpv4(networkInterfaces3()));
25113
25277
  } finally {
25114
25278
  if (own) await gates.stop();
25115
25279
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@archme/cli",
3
- "version": "0.15.0",
3
+ "version": "0.15.1",
4
4
  "description": "Map your codebase into a living architecture diagram: run `archme init` in a repo to scan it, `archme daemon start` to keep it current on every commit.",
5
5
  "type": "module",
6
6
  "license": "SEE LICENSE IN LICENSE",
@@ -1,4 +1,4 @@
1
1
  {
2
2
  "image": "ghcr.io/architectme/archme-job",
3
- "digest": "sha256:3c596a29b1df99d223b81da90a2f0cfbcca40d5f74359cf11f7a90c7a21b8611"
3
+ "digest": "sha256:a3c757d0dc7608b3c4e45b24e1ba4459c8f7ece2d0551a4633d2f3833a6fdc3e"
4
4
  }