@archme/cli 0.15.0 → 0.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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({
@@ -19475,6 +19607,45 @@ var init_claimStance = __esm({
19475
19607
  }
19476
19608
  });
19477
19609
 
19610
+ // src/daemon/platformDevices.ts
19611
+ async function readPlatformDevices(client, userId) {
19612
+ const { data, error } = await client.from("agent_devices").select("id, name").eq("user_id", userId).is("org_id", null).eq("is_platform", true).eq("status", "enabled");
19613
+ if (error) throw new Error(error.message);
19614
+ return data ?? [];
19615
+ }
19616
+ function withPlatformDevices(devices, platform3) {
19617
+ const out = [...devices];
19618
+ for (const p of platform3) {
19619
+ if (!out.some((d) => d.deviceId === p.id)) out.push({ orgId: null, deviceId: p.id });
19620
+ }
19621
+ return out;
19622
+ }
19623
+ function deviceForOrgLookup(devices) {
19624
+ const members = devices.filter((d) => d.orgId !== null);
19625
+ const byOrg = new Map(members.map((d) => [d.orgId, d.deviceId]));
19626
+ return (orgId) => byOrg.get(orgId) ?? members[0]?.deviceId ?? null;
19627
+ }
19628
+ function poolBootLine(platform3, mode) {
19629
+ if (platform3.length === 0) return "not in managed pool";
19630
+ const names = platform3.map((p) => `${p.name} (${p.id})`).join(", ");
19631
+ return `in managed pool: platform device ${names}, ${mode === "required" ? "active" : "inactive unless sandbox=required"}`;
19632
+ }
19633
+ async function discoverClaimDevices(client, userId, devices, mode, out = console) {
19634
+ let platform3 = [];
19635
+ try {
19636
+ platform3 = await readPlatformDevices(client, userId);
19637
+ } catch (e) {
19638
+ out.warn(`platform device read failed, starting member-only until restart: ${e.message}`);
19639
+ }
19640
+ out.log(` ${poolBootLine(platform3, mode)}`);
19641
+ return { devices: withPlatformDevices(devices, platform3), platform: platform3 };
19642
+ }
19643
+ var init_platformDevices = __esm({
19644
+ "src/daemon/platformDevices.ts"() {
19645
+ "use strict";
19646
+ }
19647
+ });
19648
+
19478
19649
  // src/daemon/sandboxJob/result.ts
19479
19650
  import { readFile as readFile13 } from "node:fs/promises";
19480
19651
  function isValidNonce(nonce) {
@@ -19942,6 +20113,13 @@ function sandboxBootFailurePolicy(mode) {
19942
20113
  function shutdownKillTargets(boxedJobs2, registry) {
19943
20114
  return [...boxedJobs2].filter((id) => registry.hasLiveBoxes(id));
19944
20115
  }
20116
+ function withBridgeCheck(probe, slots, bridgeFailures) {
20117
+ return async (slot) => {
20118
+ const report = await probe(slot);
20119
+ const failures = bridgeFailures(slots);
20120
+ return failures.length ? { ...report, ok: false, failures: [...report.failures, ...failures] } : report;
20121
+ };
20122
+ }
19945
20123
  async function bootSandbox(settings, parts, deps) {
19946
20124
  const sweep = await parts.runtime.sweep();
19947
20125
  const inputDirs = await (deps.sweepInputDirs ?? sweepInputDirs)();
@@ -19977,13 +20155,31 @@ async function bootSandbox(settings, parts, deps) {
19977
20155
  });
19978
20156
  throw new SandboxBootError(`sandbox preflight failed: ${failedChecks(preflight)}`);
19979
20157
  }
19980
- const probe = deps.probe ?? ((slot) => runConfinementProbe({ runtime: parts.runtime, image: settings.image, log: deps.log }, slot, primaryLanIpv4(networkInterfaces())));
20158
+ const pin = deps.pinBridges ?? ((s) => pinBridges(s, {
20159
+ resetHelpers: realResetHelpers,
20160
+ startAnchor: (slot) => parts.runtime.startAnchor(slot, settings.image),
20161
+ register: (box) => parts.registry.beginStep(ANCHOR_REGISTRY_ID, box),
20162
+ log: deps.log
20163
+ }));
20164
+ let pinError = null;
20165
+ try {
20166
+ await pin(slots);
20167
+ } catch (e) {
20168
+ pinError = e.message;
20169
+ deps.log(` sandbox: could not pin the slot bridges: ${pinError}`);
20170
+ }
20171
+ const probe = withBridgeCheck(
20172
+ deps.probe ?? ((slot) => runConfinementProbe({ runtime: parts.runtime, image: settings.image, log: deps.log }, slot, primaryLanIpv4(networkInterfaces()))),
20173
+ slots,
20174
+ deps.bridgeFailures ?? ((s) => bridgeMapFailures(s, networkInterfaces()))
20175
+ );
19981
20176
  let report;
19982
20177
  try {
19983
20178
  report = await probe(slots[0]);
19984
20179
  } catch (e) {
19985
20180
  report = { ok: false, slot: 0, failures: [`probe threw: ${e.message}`], outcomes: {}, ms: 0 };
19986
20181
  }
20182
+ if (pinError) report = { ...report, ok: false, failures: [...report.failures, `bridge pinning: ${pinError}`] };
19987
20183
  preflight = {
19988
20184
  ok: preflight.ok && report.ok,
19989
20185
  checks: [...preflight.checks, { name: "confinement_probe", ok: report.ok, detail: report.ok ? void 0 : report.failures.join("; ") }]
@@ -20013,6 +20209,7 @@ var init_host = __esm({
20013
20209
  "use strict";
20014
20210
  init_confinement();
20015
20211
  init_appleContainer();
20212
+ init_bridges();
20016
20213
  init_claudeToken();
20017
20214
  init_inputDir();
20018
20215
  init_types();
@@ -20121,9 +20318,13 @@ async function runSandboxedJob(deps, job, opts) {
20121
20318
  if (spec2.args[0] === "agent") agentStarted = true;
20122
20319
  const handle = await deps.runtime.runStep(spec2);
20123
20320
  const timer = setTimeout(() => kill("timeout"), timeoutMs);
20321
+ let gatewaySeen = false;
20124
20322
  const poll = setInterval(() => {
20125
20323
  const c3 = currentKillCause();
20126
- if (c3) kill(c3);
20324
+ if (c3) return kill(c3);
20325
+ if (!deps.gatewayPresent) return;
20326
+ if (deps.gatewayPresent(slot)) gatewaySeen = true;
20327
+ else if (gatewaySeen) kill("network_lost");
20127
20328
  }, pollMs);
20128
20329
  try {
20129
20330
  const exit = await handle.done;
@@ -22480,7 +22681,7 @@ import { randomUUID as randomUUID3 } from "node:crypto";
22480
22681
  import { mkdtemp as mkdtemp4, mkdir as mkdir16, writeFile as writeFile15, rm as rm8, stat as stat8, chmod as chmod6 } from "node:fs/promises";
22481
22682
  import { tmpdir as tmpdir4 } from "node:os";
22482
22683
  import { join as join25 } from "node:path";
22483
- import { homedir as homedir8 } from "node:os";
22684
+ import { homedir as homedir8, networkInterfaces as networkInterfaces2 } from "node:os";
22484
22685
  function attributeKillCause(result, recorded) {
22485
22686
  if (recorded) return recorded;
22486
22687
  if (result.signal === "SIGTERM") return "timeout";
@@ -23074,14 +23275,16 @@ async function rpcSetDaemonPause(ctx, pauseUntil, reason, detail) {
23074
23275
  console.error(`set_daemon_pause failed: ${error.message}`);
23075
23276
  }
23076
23277
  }
23077
- async function rpcReportEvent(ctx, orgId, eventType, payload) {
23278
+ async function rpcReportEvent(ctx, job, deviceId, eventType, payload) {
23078
23279
  try {
23079
23280
  const { error } = await withAuthRetry(
23080
23281
  "report_daemon_event",
23081
23282
  () => ctx.supabase.rpc("report_daemon_event", {
23082
- p_org_id: orgId,
23283
+ p_org_id: job.org_id,
23083
23284
  p_event_type: eventType,
23084
- p_payload: payload
23285
+ p_payload: payload,
23286
+ p_job_id: job.id,
23287
+ p_device_id: deviceId
23085
23288
  }),
23086
23289
  ctx.reLogin
23087
23290
  );
@@ -23376,7 +23579,8 @@ function sandboxDepsFor(ctx, job, deviceId, host) {
23376
23579
  log: (msg) => console.log(`[${job.id}] ${msg}`),
23377
23580
  isCancelled: (id) => cancelled.has(id),
23378
23581
  killReason: (id) => killReasons.get(id),
23379
- isLeaseLost: (id) => leaseLost.has(id)
23582
+ isLeaseLost: (id) => leaseLost.has(id),
23583
+ gatewayPresent: (slot) => gatewayPresent(slot, networkInterfaces2())
23380
23584
  };
23381
23585
  }
23382
23586
  async function completeClaudeRun(ctx, job, deviceId, run) {
@@ -23589,7 +23793,7 @@ async function runJob(ctx, job, claimDeviceId) {
23589
23793
  Object.assign(timings, outcome.metrics.durations_ms);
23590
23794
  Object.assign(metrics, outcome.metrics, { durations_ms: timings });
23591
23795
  const sandboxMetrics = outcome.metrics.sandbox;
23592
- void rpcReportEvent(ctx, job.org_id, "sandbox_job_metrics", {
23796
+ await rpcReportEvent(ctx, job, deviceId, "sandbox_job_metrics", {
23593
23797
  job_id: job.id,
23594
23798
  project_id: job.project_id,
23595
23799
  outcome: outcome.kind,
@@ -23970,7 +24174,7 @@ ${logs.stderr}`, sentPrompt);
23970
24174
  if (plan) {
23971
24175
  const { parsed, pauseUntil, info } = plan;
23972
24176
  if (!parsed) {
23973
- void rpcReportEvent(ctx, job.org_id, "rate_limit_unmatched_exit", {
24177
+ await rpcReportEvent(ctx, job, deviceId, "rate_limit_unmatched_exit", {
23974
24178
  job_id: job.id,
23975
24179
  excerpt: [runErr?.claudeError?.text, haystack].filter(Boolean).join("\n").slice(0, 500)
23976
24180
  });
@@ -24313,7 +24517,9 @@ async function runDaemon() {
24313
24517
  const via = usedFallback ? ` (via device ${active.cred.device_id})` : "";
24314
24518
  const accountUserId = auth.getUserId() ?? "OAuth account";
24315
24519
  console.log(` account ${accountUserId}: ${active.result.user.email ?? "OAuth user"} (${devices.length} org(s))${via}`);
24316
- const byOrg = new Map(devices.map((d) => [d.orgId, d.deviceId]));
24520
+ const discovered = await discoverClaimDevices(client, auth.getUserId() ?? "", devices, sandboxSettings.mode);
24521
+ platformDevices = new Set(discovered.platform.map((p) => p.id));
24522
+ const claimDevices = discovered.devices;
24317
24523
  const sessions = [{
24318
24524
  supabase: client,
24319
24525
  auth,
@@ -24323,16 +24529,9 @@ async function runDaemon() {
24323
24529
  supabaseUrl: SUPABASE_URL,
24324
24530
  cloneBaseDir: CLONE_BASE_DIR,
24325
24531
  reLogin: () => auth.reauthenticate(),
24326
- devices,
24327
- deviceForOrg: (orgId) => byOrg.get(orgId) ?? devices[0]?.deviceId ?? null
24532
+ devices: claimDevices,
24533
+ deviceForOrg: deviceForOrgLookup(claimDevices)
24328
24534
  }];
24329
- try {
24330
- const { data, error } = await client.from("agent_devices").select("id, is_platform").in("id", devices.map((d) => d.deviceId));
24331
- if (error) throw new Error(error.message);
24332
- platformDevices = new Set((data ?? []).filter((d) => d.is_platform).map((d) => d.id));
24333
- } catch (e) {
24334
- console.warn(`agent_devices.is_platform read failed (treating devices as not platform): ${e.message}`);
24335
- }
24336
24535
  if (sandboxSettings.mode !== "off") {
24337
24536
  try {
24338
24537
  const { createSandboxParts: createSandboxParts2 } = await Promise.resolve().then(() => (init_parts(), parts_exports));
@@ -24552,6 +24751,7 @@ var init_run = __esm({
24552
24751
  init_supervision();
24553
24752
  init_paths();
24554
24753
  init_semaphore();
24754
+ init_bridges();
24555
24755
  init_bearer();
24556
24756
  init_credentials();
24557
24757
  init_sessionAuth();
@@ -24569,6 +24769,7 @@ var init_run = __esm({
24569
24769
  init_architectureCounts();
24570
24770
  init_imageManifest();
24571
24771
  init_claimStance();
24772
+ init_platformDevices();
24572
24773
  init_claudeVersion();
24573
24774
  init_host();
24574
24775
  init_runSandboxedJob();
@@ -25022,7 +25223,7 @@ __export(setupIo_exports, {
25022
25223
  createSetupIo: () => createSetupIo
25023
25224
  });
25024
25225
  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";
25226
+ import { networkInterfaces as networkInterfaces3, tmpdir as tmpdir5 } from "node:os";
25026
25227
  import { join as join26 } from "node:path";
25027
25228
  import { createInterface as createInterface2 } from "node:readline";
25028
25229
  import { Writable as Writable4 } from "node:stream";
@@ -25109,7 +25310,7 @@ function createSetupIo(env = process.env) {
25109
25310
  console.log(" (the daemon's gatekeepers are answering)");
25110
25311
  }
25111
25312
  try {
25112
- return await runConfinementProbe({ runtime, image }, slot, primaryLanIpv4(networkInterfaces2()));
25313
+ return await runConfinementProbe({ runtime, image }, slot, primaryLanIpv4(networkInterfaces3()));
25113
25314
  } finally {
25114
25315
  if (own) await gates.stop();
25115
25316
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@archme/cli",
3
- "version": "0.15.0",
3
+ "version": "0.16.0",
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:3f8b289540503cc18a2794fa9de858e75a4ceb2d3cf709d814cf816569c36a96"
4
4
  }