@cabane/companion 0.6.126 → 0.6.127

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
@@ -13810,6 +13810,15 @@ var UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
13810
13810
  function containerName(attempt) {
13811
13811
  return `cabane-runner-${attempt.conversationId}-${attempt.agentId}`;
13812
13812
  }
13813
+ var networkName = containerName;
13814
+ function networkCreateError(stderr) {
13815
+ const said = stderr.trim();
13816
+ if (/address pool|fully subnetted/i.test(said))
13817
+ return new Error(
13818
+ `docker: couldn't create this conversation's runner network: the engine has no address pool left for another network. Each conversation and agent with a runner container holds one; remove kept runner containers, or widen \`default-address-pools\` in the daemon.json. Docker said: ${said}`
13819
+ );
13820
+ return new Error(`docker: creating the runner network failed: ${said || "no reason given"}`);
13821
+ }
13813
13822
  function sessionArchivePath(deviceDir, attemptId) {
13814
13823
  if (!UUID_RE.test(attemptId)) throw new Error(`docker: not an attempt id: ${attemptId}`);
13815
13824
  return join17(deviceDir, "attempts", attemptId, "session.tar");
@@ -13886,7 +13895,7 @@ function createDockerProvider(opts) {
13886
13895
  const killGraceMs = opts.killGraceMs ?? DEFAULT_KILL_GRACE_MS2;
13887
13896
  const sessionCap = opts.sessionCapBytes ?? DEFAULT_SESSION_CAP_BYTES;
13888
13897
  const collectCap = opts.collectCapBytes ?? COLLECT_CAP_BYTES;
13889
- const network = `cabane-runners-${opts.deviceKey}`;
13898
+ const sharedNetwork = `cabane-runners-${opts.deviceKey}`;
13890
13899
  const companionLabel = createHash4("sha256").update(opts.companion.key).digest("hex").slice(0, 16);
13891
13900
  const proxy = `cabane-egress-${opts.deviceKey}`;
13892
13901
  const held = /* @__PURE__ */ new Map();
@@ -13919,32 +13928,14 @@ function createDockerProvider(opts) {
13919
13928
  }
13920
13929
  throw new Error("docker: the egress proxy never started listening");
13921
13930
  };
13922
- let networkReady = null;
13931
+ let proxyReady = null;
13923
13932
  const proxyKey = async () => {
13924
13933
  const id = (await docker.run(["image", "inspect", "-f", "{{.Id}}", opts.image])).stdout.trim();
13925
13934
  return createHash4("sha256").update(JSON.stringify([id || opts.image, opts.companion.key, [...opts.egressAllow].sort()])).digest("hex").slice(0, 16);
13926
13935
  };
13927
- const ensureNetwork = () => {
13928
- networkReady ??= (async () => {
13936
+ const ensureProxy = () => {
13937
+ proxyReady ??= (async () => {
13929
13938
  const allowKey = await proxyKey();
13930
- if ((await docker.run(["network", "inspect", network])).code !== 0) {
13931
- await must(
13932
- [
13933
- "network",
13934
- "create",
13935
- // No route off the host, and no address of the host's on the
13936
- // bridge: the host's own ports are out of reach too. The proxy is
13937
- // the only other thing on the network.
13938
- "--internal",
13939
- "-o",
13940
- "com.docker.network.bridge.inhibit_ipv4=true",
13941
- "--label",
13942
- `cabane.device=${opts.deviceKey}`,
13943
- network
13944
- ],
13945
- "creating the runners network"
13946
- );
13947
- }
13948
13939
  const seen = await docker.run([
13949
13940
  "inspect",
13950
13941
  "-f",
@@ -13953,6 +13944,7 @@ function createDockerProvider(opts) {
13953
13944
  ]);
13954
13945
  const [key, running] = seen.stdout.trim().split(" ");
13955
13946
  if (seen.code === 0 && key === allowKey) {
13947
+ await retireSharedNetwork();
13956
13948
  if (running !== "true") {
13957
13949
  await must(["start", proxy], "starting the egress proxy");
13958
13950
  await proxyListening();
@@ -13987,17 +13979,61 @@ function createDockerProvider(opts) {
13987
13979
  ],
13988
13980
  "starting the egress proxy"
13989
13981
  );
13990
- await must(
13991
- ["network", "connect", "--alias", PROXY_ALIAS, network, proxy],
13992
- "connecting the proxy"
13993
- );
13982
+ await retireSharedNetwork();
13994
13983
  await proxyListening();
13995
- log.info({ network, proxy, allow: opts.egressAllow }, "runners: egress proxy ready");
13984
+ log.info({ proxy, allow: opts.egressAllow }, "runners: egress proxy ready");
13996
13985
  })().catch((err) => {
13997
- networkReady = null;
13986
+ proxyReady = null;
13998
13987
  throw err;
13999
13988
  });
14000
- return networkReady;
13989
+ return proxyReady;
13990
+ };
13991
+ const retireSharedNetwork = async () => {
13992
+ if ((await docker.run(["network", "inspect", sharedNetwork])).code !== 0) return;
13993
+ await docker.run(["network", "disconnect", "-f", sharedNetwork, proxy]);
13994
+ const on = await docker.run(["ps", "-aq", "--filter", `network=${sharedNetwork}`]);
13995
+ if (on.code === 0 && !on.stdout.trim()) await docker.run(["network", "rm", sharedNetwork]);
13996
+ };
13997
+ const networksOf = async (container) => (await docker.run([
13998
+ "inspect",
13999
+ "-f",
14000
+ "{{range $k, $_ := .NetworkSettings.Networks}}{{$k}} {{end}}",
14001
+ container
14002
+ ])).stdout.split(/\s+/).filter(Boolean);
14003
+ const ensurePairNetwork = async (net, attempt) => {
14004
+ if ((await docker.run(["network", "inspect", net])).code !== 0) {
14005
+ const made = await docker.run([
14006
+ "network",
14007
+ "create",
14008
+ // No route off the host, and no address of the host's on the bridge:
14009
+ // the host's own ports are out of reach too. The proxy is the only
14010
+ // other thing on the network.
14011
+ "--internal",
14012
+ "-o",
14013
+ "com.docker.network.bridge.inhibit_ipv4=true",
14014
+ "--label",
14015
+ `cabane.device=${opts.deviceKey}`,
14016
+ "--label",
14017
+ `cabane.conversation=${attempt.conversationId}`,
14018
+ "--label",
14019
+ `cabane.agent=${attempt.agentId}`,
14020
+ net
14021
+ ]);
14022
+ if (made.code !== 0) throw networkCreateError(made.stderr);
14023
+ }
14024
+ if (!(await networksOf(proxy)).includes(net))
14025
+ await must(
14026
+ ["network", "connect", "--alias", PROXY_ALIAS, net, proxy],
14027
+ "connecting the proxy"
14028
+ );
14029
+ };
14030
+ const settleNetworks = async (name, net) => {
14031
+ const on = await networksOf(name);
14032
+ if (!on.includes(net)) await must(["network", "connect", net, name], "moving the container");
14033
+ const others = on.filter((n) => n !== net);
14034
+ for (const other of others)
14035
+ await must(["network", "disconnect", "-f", other, name], "moving the container");
14036
+ if (others.includes(sharedNetwork)) await retireSharedNetwork();
14001
14037
  };
14002
14038
  const socketRoot = opts.socketRoot ?? join17(
14003
14039
  process.env.XDG_RUNTIME_DIR && existsSync13(process.env.XDG_RUNTIME_DIR) ? process.env.XDG_RUNTIME_DIR : tmpdir2(),
@@ -14029,7 +14065,7 @@ function createDockerProvider(opts) {
14029
14065
  "--label",
14030
14066
  `cabane.companion=${companionLabel}`,
14031
14067
  "--network",
14032
- network,
14068
+ networkName(attempt),
14033
14069
  "--mount",
14034
14070
  `type=volume,src=${name}-home,dst=${CONTAINER_HOME}`,
14035
14071
  "--mount",
@@ -14055,6 +14091,17 @@ function createDockerProvider(opts) {
14055
14091
  await docker.run(["rm", "-f", "-v", name]);
14056
14092
  await docker.run(["volume", "rm", "-f", `${name}-home`, `${name}-work`]);
14057
14093
  rmSync9(socketDir(name), { recursive: true, force: true });
14094
+ await dropNetwork(name);
14095
+ };
14096
+ const dropNetwork = async (name) => {
14097
+ if ((await docker.run(["network", "inspect", name])).code !== 0) return;
14098
+ await docker.run(["network", "disconnect", "-f", name, proxy]);
14099
+ const gone = await docker.run(["network", "rm", name]);
14100
+ if (gone.code !== 0)
14101
+ log.warn(
14102
+ { network: name, err: gone.stderr.trim() },
14103
+ "runners: couldn't remove a pair's network"
14104
+ );
14058
14105
  };
14059
14106
  const containerEnv = () => {
14060
14107
  const proxyUrl = `http://${PROXY_ALIAS}:${PROXY_PORT}`;
@@ -14221,7 +14268,7 @@ function createDockerProvider(opts) {
14221
14268
  });
14222
14269
  await before;
14223
14270
  try {
14224
- await ensureNetwork();
14271
+ await ensureProxy();
14225
14272
  mkdirSync10(socketRoot, { recursive: true, mode: 448 });
14226
14273
  mkdirSync10(socketDir(name), { recursive: true, mode: 457 });
14227
14274
  let seen = await docker.run([
@@ -14251,9 +14298,12 @@ function createDockerProvider(opts) {
14251
14298
  );
14252
14299
  seen = { code: 1, stdout: "", stderr: "" };
14253
14300
  }
14301
+ await ensurePairNetwork(networkName(attempt), attempt);
14254
14302
  if (seen.code !== 0) {
14255
14303
  await create(name, attempt);
14256
14304
  log.debug({ container: name }, "runners: container created");
14305
+ } else {
14306
+ await settleNetworks(name, networkName(attempt));
14257
14307
  }
14258
14308
  if (seen.stdout.trim().split(" ")[0] !== "true")
14259
14309
  await must(["start", name], "starting the container");
@@ -14267,7 +14317,8 @@ function createDockerProvider(opts) {
14267
14317
  return { provider: "docker", id: name };
14268
14318
  } catch (err) {
14269
14319
  const keep = opts.keepContainers || readPending(name).length > 0;
14270
- await (keep ? docker.run(["stop", "-t", "1", name]) : destroy(name)).catch(() => {
14320
+ const kept = keep && (await docker.run(["inspect", "-f", "{{.Id}}", name])).code === 0;
14321
+ await (kept ? docker.run(["stop", "-t", "1", name]) : keep ? dropNetwork(networkName(attempt)) : destroy(name)).catch(() => {
14271
14322
  });
14272
14323
  release();
14273
14324
  throw err;
package/dist/runtime.js CHANGED
@@ -11527,6 +11527,15 @@ var UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
11527
11527
  function containerName(attempt) {
11528
11528
  return `cabane-runner-${attempt.conversationId}-${attempt.agentId}`;
11529
11529
  }
11530
+ var networkName = containerName;
11531
+ function networkCreateError(stderr) {
11532
+ const said = stderr.trim();
11533
+ if (/address pool|fully subnetted/i.test(said))
11534
+ return new Error(
11535
+ `docker: couldn't create this conversation's runner network: the engine has no address pool left for another network. Each conversation and agent with a runner container holds one; remove kept runner containers, or widen \`default-address-pools\` in the daemon.json. Docker said: ${said}`
11536
+ );
11537
+ return new Error(`docker: creating the runner network failed: ${said || "no reason given"}`);
11538
+ }
11530
11539
  function sessionArchivePath(deviceDir, attemptId) {
11531
11540
  if (!UUID_RE.test(attemptId)) throw new Error(`docker: not an attempt id: ${attemptId}`);
11532
11541
  return join17(deviceDir, "attempts", attemptId, "session.tar");
@@ -11603,7 +11612,7 @@ function createDockerProvider(opts) {
11603
11612
  const killGraceMs = opts.killGraceMs ?? DEFAULT_KILL_GRACE_MS2;
11604
11613
  const sessionCap = opts.sessionCapBytes ?? DEFAULT_SESSION_CAP_BYTES;
11605
11614
  const collectCap = opts.collectCapBytes ?? COLLECT_CAP_BYTES;
11606
- const network = `cabane-runners-${opts.deviceKey}`;
11615
+ const sharedNetwork = `cabane-runners-${opts.deviceKey}`;
11607
11616
  const companionLabel = createHash4("sha256").update(opts.companion.key).digest("hex").slice(0, 16);
11608
11617
  const proxy = `cabane-egress-${opts.deviceKey}`;
11609
11618
  const held = /* @__PURE__ */ new Map();
@@ -11636,32 +11645,14 @@ function createDockerProvider(opts) {
11636
11645
  }
11637
11646
  throw new Error("docker: the egress proxy never started listening");
11638
11647
  };
11639
- let networkReady = null;
11648
+ let proxyReady = null;
11640
11649
  const proxyKey = async () => {
11641
11650
  const id = (await docker.run(["image", "inspect", "-f", "{{.Id}}", opts.image])).stdout.trim();
11642
11651
  return createHash4("sha256").update(JSON.stringify([id || opts.image, opts.companion.key, [...opts.egressAllow].sort()])).digest("hex").slice(0, 16);
11643
11652
  };
11644
- const ensureNetwork = () => {
11645
- networkReady ??= (async () => {
11653
+ const ensureProxy = () => {
11654
+ proxyReady ??= (async () => {
11646
11655
  const allowKey = await proxyKey();
11647
- if ((await docker.run(["network", "inspect", network])).code !== 0) {
11648
- await must(
11649
- [
11650
- "network",
11651
- "create",
11652
- // No route off the host, and no address of the host's on the
11653
- // bridge: the host's own ports are out of reach too. The proxy is
11654
- // the only other thing on the network.
11655
- "--internal",
11656
- "-o",
11657
- "com.docker.network.bridge.inhibit_ipv4=true",
11658
- "--label",
11659
- `cabane.device=${opts.deviceKey}`,
11660
- network
11661
- ],
11662
- "creating the runners network"
11663
- );
11664
- }
11665
11656
  const seen = await docker.run([
11666
11657
  "inspect",
11667
11658
  "-f",
@@ -11670,6 +11661,7 @@ function createDockerProvider(opts) {
11670
11661
  ]);
11671
11662
  const [key, running] = seen.stdout.trim().split(" ");
11672
11663
  if (seen.code === 0 && key === allowKey) {
11664
+ await retireSharedNetwork();
11673
11665
  if (running !== "true") {
11674
11666
  await must(["start", proxy], "starting the egress proxy");
11675
11667
  await proxyListening();
@@ -11704,17 +11696,61 @@ function createDockerProvider(opts) {
11704
11696
  ],
11705
11697
  "starting the egress proxy"
11706
11698
  );
11707
- await must(
11708
- ["network", "connect", "--alias", PROXY_ALIAS, network, proxy],
11709
- "connecting the proxy"
11710
- );
11699
+ await retireSharedNetwork();
11711
11700
  await proxyListening();
11712
- log.info({ network, proxy, allow: opts.egressAllow }, "runners: egress proxy ready");
11701
+ log.info({ proxy, allow: opts.egressAllow }, "runners: egress proxy ready");
11713
11702
  })().catch((err) => {
11714
- networkReady = null;
11703
+ proxyReady = null;
11715
11704
  throw err;
11716
11705
  });
11717
- return networkReady;
11706
+ return proxyReady;
11707
+ };
11708
+ const retireSharedNetwork = async () => {
11709
+ if ((await docker.run(["network", "inspect", sharedNetwork])).code !== 0) return;
11710
+ await docker.run(["network", "disconnect", "-f", sharedNetwork, proxy]);
11711
+ const on = await docker.run(["ps", "-aq", "--filter", `network=${sharedNetwork}`]);
11712
+ if (on.code === 0 && !on.stdout.trim()) await docker.run(["network", "rm", sharedNetwork]);
11713
+ };
11714
+ const networksOf = async (container) => (await docker.run([
11715
+ "inspect",
11716
+ "-f",
11717
+ "{{range $k, $_ := .NetworkSettings.Networks}}{{$k}} {{end}}",
11718
+ container
11719
+ ])).stdout.split(/\s+/).filter(Boolean);
11720
+ const ensurePairNetwork = async (net, attempt) => {
11721
+ if ((await docker.run(["network", "inspect", net])).code !== 0) {
11722
+ const made = await docker.run([
11723
+ "network",
11724
+ "create",
11725
+ // No route off the host, and no address of the host's on the bridge:
11726
+ // the host's own ports are out of reach too. The proxy is the only
11727
+ // other thing on the network.
11728
+ "--internal",
11729
+ "-o",
11730
+ "com.docker.network.bridge.inhibit_ipv4=true",
11731
+ "--label",
11732
+ `cabane.device=${opts.deviceKey}`,
11733
+ "--label",
11734
+ `cabane.conversation=${attempt.conversationId}`,
11735
+ "--label",
11736
+ `cabane.agent=${attempt.agentId}`,
11737
+ net
11738
+ ]);
11739
+ if (made.code !== 0) throw networkCreateError(made.stderr);
11740
+ }
11741
+ if (!(await networksOf(proxy)).includes(net))
11742
+ await must(
11743
+ ["network", "connect", "--alias", PROXY_ALIAS, net, proxy],
11744
+ "connecting the proxy"
11745
+ );
11746
+ };
11747
+ const settleNetworks = async (name, net) => {
11748
+ const on = await networksOf(name);
11749
+ if (!on.includes(net)) await must(["network", "connect", net, name], "moving the container");
11750
+ const others = on.filter((n) => n !== net);
11751
+ for (const other of others)
11752
+ await must(["network", "disconnect", "-f", other, name], "moving the container");
11753
+ if (others.includes(sharedNetwork)) await retireSharedNetwork();
11718
11754
  };
11719
11755
  const socketRoot = opts.socketRoot ?? join17(
11720
11756
  process.env.XDG_RUNTIME_DIR && existsSync13(process.env.XDG_RUNTIME_DIR) ? process.env.XDG_RUNTIME_DIR : tmpdir2(),
@@ -11746,7 +11782,7 @@ function createDockerProvider(opts) {
11746
11782
  "--label",
11747
11783
  `cabane.companion=${companionLabel}`,
11748
11784
  "--network",
11749
- network,
11785
+ networkName(attempt),
11750
11786
  "--mount",
11751
11787
  `type=volume,src=${name}-home,dst=${CONTAINER_HOME}`,
11752
11788
  "--mount",
@@ -11772,6 +11808,17 @@ function createDockerProvider(opts) {
11772
11808
  await docker.run(["rm", "-f", "-v", name]);
11773
11809
  await docker.run(["volume", "rm", "-f", `${name}-home`, `${name}-work`]);
11774
11810
  rmSync9(socketDir(name), { recursive: true, force: true });
11811
+ await dropNetwork(name);
11812
+ };
11813
+ const dropNetwork = async (name) => {
11814
+ if ((await docker.run(["network", "inspect", name])).code !== 0) return;
11815
+ await docker.run(["network", "disconnect", "-f", name, proxy]);
11816
+ const gone = await docker.run(["network", "rm", name]);
11817
+ if (gone.code !== 0)
11818
+ log.warn(
11819
+ { network: name, err: gone.stderr.trim() },
11820
+ "runners: couldn't remove a pair's network"
11821
+ );
11775
11822
  };
11776
11823
  const containerEnv = () => {
11777
11824
  const proxyUrl = `http://${PROXY_ALIAS}:${PROXY_PORT}`;
@@ -11938,7 +11985,7 @@ function createDockerProvider(opts) {
11938
11985
  });
11939
11986
  await before;
11940
11987
  try {
11941
- await ensureNetwork();
11988
+ await ensureProxy();
11942
11989
  mkdirSync10(socketRoot, { recursive: true, mode: 448 });
11943
11990
  mkdirSync10(socketDir(name), { recursive: true, mode: 457 });
11944
11991
  let seen = await docker.run([
@@ -11968,9 +12015,12 @@ function createDockerProvider(opts) {
11968
12015
  );
11969
12016
  seen = { code: 1, stdout: "", stderr: "" };
11970
12017
  }
12018
+ await ensurePairNetwork(networkName(attempt), attempt);
11971
12019
  if (seen.code !== 0) {
11972
12020
  await create(name, attempt);
11973
12021
  log.debug({ container: name }, "runners: container created");
12022
+ } else {
12023
+ await settleNetworks(name, networkName(attempt));
11974
12024
  }
11975
12025
  if (seen.stdout.trim().split(" ")[0] !== "true")
11976
12026
  await must(["start", name], "starting the container");
@@ -11984,7 +12034,8 @@ function createDockerProvider(opts) {
11984
12034
  return { provider: "docker", id: name };
11985
12035
  } catch (err) {
11986
12036
  const keep = opts.keepContainers || readPending(name).length > 0;
11987
- await (keep ? docker.run(["stop", "-t", "1", name]) : destroy(name)).catch(() => {
12037
+ const kept = keep && (await docker.run(["inspect", "-f", "{{.Id}}", name])).code === 0;
12038
+ await (kept ? docker.run(["stop", "-t", "1", name]) : keep ? dropNetwork(networkName(attempt)) : destroy(name)).catch(() => {
11988
12039
  });
11989
12040
  release();
11990
12041
  throw err;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cabane/companion",
3
- "version": "0.6.126",
3
+ "version": "0.6.127",
4
4
  "type": "module",
5
5
  "description": "The Cabane Companion (headless): connect a coding agent on your machine to your Cabane workspace as a responder — drive work against your own codebase, files, and MCP servers without putting any of it in Cabane.",
6
6
  "license": "UNLICENSED",