@cabane/companion 0.6.126 → 0.6.128
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 +119 -40
- package/dist/runtime.js +119 -40
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -1037,6 +1037,11 @@ function leaseRefusal(err) {
|
|
|
1037
1037
|
function isWriteFenceRefusal(err) {
|
|
1038
1038
|
return err instanceof ApiError && err.status === 409 && apiErrorCode(err) === "not_running";
|
|
1039
1039
|
}
|
|
1040
|
+
function isTurnEndedRefusal(err) {
|
|
1041
|
+
if (!(err instanceof ApiError) || err.status !== 401) return false;
|
|
1042
|
+
const body = err.body;
|
|
1043
|
+
return typeof body === "object" && body !== null && body.reason === "turn_ended";
|
|
1044
|
+
}
|
|
1040
1045
|
function describeErrorBody(body) {
|
|
1041
1046
|
if (body === void 0 || body === null) return void 0;
|
|
1042
1047
|
let text;
|
|
@@ -1286,6 +1291,15 @@ var init_api = __esm({
|
|
|
1286
1291
|
progressed = true;
|
|
1287
1292
|
} catch (err) {
|
|
1288
1293
|
if (err instanceof ApiError && err.status >= 400 && err.status < 500) {
|
|
1294
|
+
if (entry.kind === "active-run" && isTurnEndedRefusal(err)) {
|
|
1295
|
+
this.opts.log?.debug(
|
|
1296
|
+
{ kind: entry.kind, turnId: entry.turnId, ...apiErrorLogFields(err) },
|
|
1297
|
+
"companion: queued settle refused; the turn had already ended"
|
|
1298
|
+
);
|
|
1299
|
+
outbox.remove(entry.turnId, entry.seq);
|
|
1300
|
+
progressed = true;
|
|
1301
|
+
continue;
|
|
1302
|
+
}
|
|
1289
1303
|
this.opts.log?.warn(
|
|
1290
1304
|
{ kind: entry.kind, turnId: entry.turnId, seq: entry.seq, ...apiErrorLogFields(err) },
|
|
1291
1305
|
"Cabane rejected a saved update; it could not be delivered. This companion may need an update: npm i -g @cabane/companion@latest"
|
|
@@ -8998,10 +9012,17 @@ var init_turn_execution = __esm({
|
|
|
8998
9012
|
body
|
|
8999
9013
|
);
|
|
9000
9014
|
} catch (err) {
|
|
9001
|
-
|
|
9002
|
-
|
|
9003
|
-
|
|
9004
|
-
|
|
9015
|
+
if (isTurnEndedRefusal(err)) {
|
|
9016
|
+
turnLog.debug(
|
|
9017
|
+
{ ...apiErrorLogFields(err), turnId },
|
|
9018
|
+
"companion: settle refused; the turn had already ended"
|
|
9019
|
+
);
|
|
9020
|
+
} else {
|
|
9021
|
+
turnLog.warn(
|
|
9022
|
+
{ ...apiErrorLogFields(err), turnId },
|
|
9023
|
+
"Couldn't save the turn's result to Cabane. This companion may need an update: npm i -g @cabane/companion@latest"
|
|
9024
|
+
);
|
|
9025
|
+
}
|
|
9005
9026
|
}
|
|
9006
9027
|
const durationMs = Date.now() - startedAt;
|
|
9007
9028
|
this.supervisor.notifyEnd({ id: payload.messageId, ok: false, durationMs, reason });
|
|
@@ -9809,10 +9830,17 @@ var init_turn_execution = __esm({
|
|
|
9809
9830
|
body
|
|
9810
9831
|
);
|
|
9811
9832
|
} catch (err) {
|
|
9812
|
-
|
|
9813
|
-
|
|
9814
|
-
|
|
9815
|
-
|
|
9833
|
+
if (isTurnEndedRefusal(err)) {
|
|
9834
|
+
turnLog.debug(
|
|
9835
|
+
{ ...apiErrorLogFields(err) },
|
|
9836
|
+
"companion: settle refused; the turn had already ended"
|
|
9837
|
+
);
|
|
9838
|
+
} else {
|
|
9839
|
+
turnLog.warn(
|
|
9840
|
+
{ ...apiErrorLogFields(err) },
|
|
9841
|
+
"Couldn't save the turn's result to Cabane. This companion may need an update: npm i -g @cabane/companion@latest"
|
|
9842
|
+
);
|
|
9843
|
+
}
|
|
9816
9844
|
}
|
|
9817
9845
|
this.supervisor.releaseAbort(turnId, abortController);
|
|
9818
9846
|
}
|
|
@@ -13810,6 +13838,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
13838
|
function containerName(attempt) {
|
|
13811
13839
|
return `cabane-runner-${attempt.conversationId}-${attempt.agentId}`;
|
|
13812
13840
|
}
|
|
13841
|
+
var networkName = containerName;
|
|
13842
|
+
function networkCreateError(stderr) {
|
|
13843
|
+
const said = stderr.trim();
|
|
13844
|
+
if (/address pool|fully subnetted/i.test(said))
|
|
13845
|
+
return new Error(
|
|
13846
|
+
`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}`
|
|
13847
|
+
);
|
|
13848
|
+
return new Error(`docker: creating the runner network failed: ${said || "no reason given"}`);
|
|
13849
|
+
}
|
|
13813
13850
|
function sessionArchivePath(deviceDir, attemptId) {
|
|
13814
13851
|
if (!UUID_RE.test(attemptId)) throw new Error(`docker: not an attempt id: ${attemptId}`);
|
|
13815
13852
|
return join17(deviceDir, "attempts", attemptId, "session.tar");
|
|
@@ -13886,7 +13923,7 @@ function createDockerProvider(opts) {
|
|
|
13886
13923
|
const killGraceMs = opts.killGraceMs ?? DEFAULT_KILL_GRACE_MS2;
|
|
13887
13924
|
const sessionCap = opts.sessionCapBytes ?? DEFAULT_SESSION_CAP_BYTES;
|
|
13888
13925
|
const collectCap = opts.collectCapBytes ?? COLLECT_CAP_BYTES;
|
|
13889
|
-
const
|
|
13926
|
+
const sharedNetwork = `cabane-runners-${opts.deviceKey}`;
|
|
13890
13927
|
const companionLabel = createHash4("sha256").update(opts.companion.key).digest("hex").slice(0, 16);
|
|
13891
13928
|
const proxy = `cabane-egress-${opts.deviceKey}`;
|
|
13892
13929
|
const held = /* @__PURE__ */ new Map();
|
|
@@ -13919,32 +13956,14 @@ function createDockerProvider(opts) {
|
|
|
13919
13956
|
}
|
|
13920
13957
|
throw new Error("docker: the egress proxy never started listening");
|
|
13921
13958
|
};
|
|
13922
|
-
let
|
|
13959
|
+
let proxyReady = null;
|
|
13923
13960
|
const proxyKey = async () => {
|
|
13924
13961
|
const id = (await docker.run(["image", "inspect", "-f", "{{.Id}}", opts.image])).stdout.trim();
|
|
13925
13962
|
return createHash4("sha256").update(JSON.stringify([id || opts.image, opts.companion.key, [...opts.egressAllow].sort()])).digest("hex").slice(0, 16);
|
|
13926
13963
|
};
|
|
13927
|
-
const
|
|
13928
|
-
|
|
13964
|
+
const ensureProxy = () => {
|
|
13965
|
+
proxyReady ??= (async () => {
|
|
13929
13966
|
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
13967
|
const seen = await docker.run([
|
|
13949
13968
|
"inspect",
|
|
13950
13969
|
"-f",
|
|
@@ -13953,6 +13972,7 @@ function createDockerProvider(opts) {
|
|
|
13953
13972
|
]);
|
|
13954
13973
|
const [key, running] = seen.stdout.trim().split(" ");
|
|
13955
13974
|
if (seen.code === 0 && key === allowKey) {
|
|
13975
|
+
await retireSharedNetwork();
|
|
13956
13976
|
if (running !== "true") {
|
|
13957
13977
|
await must(["start", proxy], "starting the egress proxy");
|
|
13958
13978
|
await proxyListening();
|
|
@@ -13987,17 +14007,61 @@ function createDockerProvider(opts) {
|
|
|
13987
14007
|
],
|
|
13988
14008
|
"starting the egress proxy"
|
|
13989
14009
|
);
|
|
13990
|
-
await
|
|
13991
|
-
["network", "connect", "--alias", PROXY_ALIAS, network, proxy],
|
|
13992
|
-
"connecting the proxy"
|
|
13993
|
-
);
|
|
14010
|
+
await retireSharedNetwork();
|
|
13994
14011
|
await proxyListening();
|
|
13995
|
-
log.info({
|
|
14012
|
+
log.info({ proxy, allow: opts.egressAllow }, "runners: egress proxy ready");
|
|
13996
14013
|
})().catch((err) => {
|
|
13997
|
-
|
|
14014
|
+
proxyReady = null;
|
|
13998
14015
|
throw err;
|
|
13999
14016
|
});
|
|
14000
|
-
return
|
|
14017
|
+
return proxyReady;
|
|
14018
|
+
};
|
|
14019
|
+
const retireSharedNetwork = async () => {
|
|
14020
|
+
if ((await docker.run(["network", "inspect", sharedNetwork])).code !== 0) return;
|
|
14021
|
+
await docker.run(["network", "disconnect", "-f", sharedNetwork, proxy]);
|
|
14022
|
+
const on = await docker.run(["ps", "-aq", "--filter", `network=${sharedNetwork}`]);
|
|
14023
|
+
if (on.code === 0 && !on.stdout.trim()) await docker.run(["network", "rm", sharedNetwork]);
|
|
14024
|
+
};
|
|
14025
|
+
const networksOf = async (container) => (await docker.run([
|
|
14026
|
+
"inspect",
|
|
14027
|
+
"-f",
|
|
14028
|
+
"{{range $k, $_ := .NetworkSettings.Networks}}{{$k}} {{end}}",
|
|
14029
|
+
container
|
|
14030
|
+
])).stdout.split(/\s+/).filter(Boolean);
|
|
14031
|
+
const ensurePairNetwork = async (net, attempt) => {
|
|
14032
|
+
if ((await docker.run(["network", "inspect", net])).code !== 0) {
|
|
14033
|
+
const made = await docker.run([
|
|
14034
|
+
"network",
|
|
14035
|
+
"create",
|
|
14036
|
+
// No route off the host, and no address of the host's on the bridge:
|
|
14037
|
+
// the host's own ports are out of reach too. The proxy is the only
|
|
14038
|
+
// other thing on the network.
|
|
14039
|
+
"--internal",
|
|
14040
|
+
"-o",
|
|
14041
|
+
"com.docker.network.bridge.inhibit_ipv4=true",
|
|
14042
|
+
"--label",
|
|
14043
|
+
`cabane.device=${opts.deviceKey}`,
|
|
14044
|
+
"--label",
|
|
14045
|
+
`cabane.conversation=${attempt.conversationId}`,
|
|
14046
|
+
"--label",
|
|
14047
|
+
`cabane.agent=${attempt.agentId}`,
|
|
14048
|
+
net
|
|
14049
|
+
]);
|
|
14050
|
+
if (made.code !== 0) throw networkCreateError(made.stderr);
|
|
14051
|
+
}
|
|
14052
|
+
if (!(await networksOf(proxy)).includes(net))
|
|
14053
|
+
await must(
|
|
14054
|
+
["network", "connect", "--alias", PROXY_ALIAS, net, proxy],
|
|
14055
|
+
"connecting the proxy"
|
|
14056
|
+
);
|
|
14057
|
+
};
|
|
14058
|
+
const settleNetworks = async (name, net) => {
|
|
14059
|
+
const on = await networksOf(name);
|
|
14060
|
+
if (!on.includes(net)) await must(["network", "connect", net, name], "moving the container");
|
|
14061
|
+
const others = on.filter((n) => n !== net);
|
|
14062
|
+
for (const other of others)
|
|
14063
|
+
await must(["network", "disconnect", "-f", other, name], "moving the container");
|
|
14064
|
+
if (others.includes(sharedNetwork)) await retireSharedNetwork();
|
|
14001
14065
|
};
|
|
14002
14066
|
const socketRoot = opts.socketRoot ?? join17(
|
|
14003
14067
|
process.env.XDG_RUNTIME_DIR && existsSync13(process.env.XDG_RUNTIME_DIR) ? process.env.XDG_RUNTIME_DIR : tmpdir2(),
|
|
@@ -14029,7 +14093,7 @@ function createDockerProvider(opts) {
|
|
|
14029
14093
|
"--label",
|
|
14030
14094
|
`cabane.companion=${companionLabel}`,
|
|
14031
14095
|
"--network",
|
|
14032
|
-
|
|
14096
|
+
networkName(attempt),
|
|
14033
14097
|
"--mount",
|
|
14034
14098
|
`type=volume,src=${name}-home,dst=${CONTAINER_HOME}`,
|
|
14035
14099
|
"--mount",
|
|
@@ -14055,6 +14119,17 @@ function createDockerProvider(opts) {
|
|
|
14055
14119
|
await docker.run(["rm", "-f", "-v", name]);
|
|
14056
14120
|
await docker.run(["volume", "rm", "-f", `${name}-home`, `${name}-work`]);
|
|
14057
14121
|
rmSync9(socketDir(name), { recursive: true, force: true });
|
|
14122
|
+
await dropNetwork(name);
|
|
14123
|
+
};
|
|
14124
|
+
const dropNetwork = async (name) => {
|
|
14125
|
+
if ((await docker.run(["network", "inspect", name])).code !== 0) return;
|
|
14126
|
+
await docker.run(["network", "disconnect", "-f", name, proxy]);
|
|
14127
|
+
const gone = await docker.run(["network", "rm", name]);
|
|
14128
|
+
if (gone.code !== 0)
|
|
14129
|
+
log.warn(
|
|
14130
|
+
{ network: name, err: gone.stderr.trim() },
|
|
14131
|
+
"runners: couldn't remove a pair's network"
|
|
14132
|
+
);
|
|
14058
14133
|
};
|
|
14059
14134
|
const containerEnv = () => {
|
|
14060
14135
|
const proxyUrl = `http://${PROXY_ALIAS}:${PROXY_PORT}`;
|
|
@@ -14221,7 +14296,7 @@ function createDockerProvider(opts) {
|
|
|
14221
14296
|
});
|
|
14222
14297
|
await before;
|
|
14223
14298
|
try {
|
|
14224
|
-
await
|
|
14299
|
+
await ensureProxy();
|
|
14225
14300
|
mkdirSync10(socketRoot, { recursive: true, mode: 448 });
|
|
14226
14301
|
mkdirSync10(socketDir(name), { recursive: true, mode: 457 });
|
|
14227
14302
|
let seen = await docker.run([
|
|
@@ -14251,9 +14326,12 @@ function createDockerProvider(opts) {
|
|
|
14251
14326
|
);
|
|
14252
14327
|
seen = { code: 1, stdout: "", stderr: "" };
|
|
14253
14328
|
}
|
|
14329
|
+
await ensurePairNetwork(networkName(attempt), attempt);
|
|
14254
14330
|
if (seen.code !== 0) {
|
|
14255
14331
|
await create(name, attempt);
|
|
14256
14332
|
log.debug({ container: name }, "runners: container created");
|
|
14333
|
+
} else {
|
|
14334
|
+
await settleNetworks(name, networkName(attempt));
|
|
14257
14335
|
}
|
|
14258
14336
|
if (seen.stdout.trim().split(" ")[0] !== "true")
|
|
14259
14337
|
await must(["start", name], "starting the container");
|
|
@@ -14267,7 +14345,8 @@ function createDockerProvider(opts) {
|
|
|
14267
14345
|
return { provider: "docker", id: name };
|
|
14268
14346
|
} catch (err) {
|
|
14269
14347
|
const keep = opts.keepContainers || readPending(name).length > 0;
|
|
14270
|
-
|
|
14348
|
+
const kept = keep && (await docker.run(["inspect", "-f", "{{.Id}}", name])).code === 0;
|
|
14349
|
+
await (kept ? docker.run(["stop", "-t", "1", name]) : keep ? dropNetwork(networkName(attempt)) : destroy(name)).catch(() => {
|
|
14271
14350
|
});
|
|
14272
14351
|
release();
|
|
14273
14352
|
throw err;
|
package/dist/runtime.js
CHANGED
|
@@ -2279,6 +2279,11 @@ function leaseRefusal(err) {
|
|
|
2279
2279
|
function isWriteFenceRefusal(err) {
|
|
2280
2280
|
return err instanceof ApiError && err.status === 409 && apiErrorCode(err) === "not_running";
|
|
2281
2281
|
}
|
|
2282
|
+
function isTurnEndedRefusal(err) {
|
|
2283
|
+
if (!(err instanceof ApiError) || err.status !== 401) return false;
|
|
2284
|
+
const body = err.body;
|
|
2285
|
+
return typeof body === "object" && body !== null && body.reason === "turn_ended";
|
|
2286
|
+
}
|
|
2282
2287
|
var ERROR_BODY_LOG_CAP = 2e3;
|
|
2283
2288
|
function describeErrorBody(body) {
|
|
2284
2289
|
if (body === void 0 || body === null) return void 0;
|
|
@@ -2469,6 +2474,15 @@ var CabaneApi = class {
|
|
|
2469
2474
|
progressed = true;
|
|
2470
2475
|
} catch (err) {
|
|
2471
2476
|
if (err instanceof ApiError && err.status >= 400 && err.status < 500) {
|
|
2477
|
+
if (entry.kind === "active-run" && isTurnEndedRefusal(err)) {
|
|
2478
|
+
this.opts.log?.debug(
|
|
2479
|
+
{ kind: entry.kind, turnId: entry.turnId, ...apiErrorLogFields(err) },
|
|
2480
|
+
"companion: queued settle refused; the turn had already ended"
|
|
2481
|
+
);
|
|
2482
|
+
outbox.remove(entry.turnId, entry.seq);
|
|
2483
|
+
progressed = true;
|
|
2484
|
+
continue;
|
|
2485
|
+
}
|
|
2472
2486
|
this.opts.log?.warn(
|
|
2473
2487
|
{ kind: entry.kind, turnId: entry.turnId, seq: entry.seq, ...apiErrorLogFields(err) },
|
|
2474
2488
|
"Cabane rejected a saved update; it could not be delivered. This companion may need an update: npm i -g @cabane/companion@latest"
|
|
@@ -9984,10 +9998,17 @@ var TurnExecution = class {
|
|
|
9984
9998
|
body
|
|
9985
9999
|
);
|
|
9986
10000
|
} catch (err) {
|
|
9987
|
-
|
|
9988
|
-
|
|
9989
|
-
|
|
9990
|
-
|
|
10001
|
+
if (isTurnEndedRefusal(err)) {
|
|
10002
|
+
turnLog.debug(
|
|
10003
|
+
{ ...apiErrorLogFields(err), turnId },
|
|
10004
|
+
"companion: settle refused; the turn had already ended"
|
|
10005
|
+
);
|
|
10006
|
+
} else {
|
|
10007
|
+
turnLog.warn(
|
|
10008
|
+
{ ...apiErrorLogFields(err), turnId },
|
|
10009
|
+
"Couldn't save the turn's result to Cabane. This companion may need an update: npm i -g @cabane/companion@latest"
|
|
10010
|
+
);
|
|
10011
|
+
}
|
|
9991
10012
|
}
|
|
9992
10013
|
const durationMs = Date.now() - startedAt;
|
|
9993
10014
|
this.supervisor.notifyEnd({ id: payload.messageId, ok: false, durationMs, reason });
|
|
@@ -10795,10 +10816,17 @@ var TurnExecution = class {
|
|
|
10795
10816
|
body
|
|
10796
10817
|
);
|
|
10797
10818
|
} catch (err) {
|
|
10798
|
-
|
|
10799
|
-
|
|
10800
|
-
|
|
10801
|
-
|
|
10819
|
+
if (isTurnEndedRefusal(err)) {
|
|
10820
|
+
turnLog.debug(
|
|
10821
|
+
{ ...apiErrorLogFields(err) },
|
|
10822
|
+
"companion: settle refused; the turn had already ended"
|
|
10823
|
+
);
|
|
10824
|
+
} else {
|
|
10825
|
+
turnLog.warn(
|
|
10826
|
+
{ ...apiErrorLogFields(err) },
|
|
10827
|
+
"Couldn't save the turn's result to Cabane. This companion may need an update: npm i -g @cabane/companion@latest"
|
|
10828
|
+
);
|
|
10829
|
+
}
|
|
10802
10830
|
}
|
|
10803
10831
|
this.supervisor.releaseAbort(turnId, abortController);
|
|
10804
10832
|
}
|
|
@@ -11527,6 +11555,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
11555
|
function containerName(attempt) {
|
|
11528
11556
|
return `cabane-runner-${attempt.conversationId}-${attempt.agentId}`;
|
|
11529
11557
|
}
|
|
11558
|
+
var networkName = containerName;
|
|
11559
|
+
function networkCreateError(stderr) {
|
|
11560
|
+
const said = stderr.trim();
|
|
11561
|
+
if (/address pool|fully subnetted/i.test(said))
|
|
11562
|
+
return new Error(
|
|
11563
|
+
`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}`
|
|
11564
|
+
);
|
|
11565
|
+
return new Error(`docker: creating the runner network failed: ${said || "no reason given"}`);
|
|
11566
|
+
}
|
|
11530
11567
|
function sessionArchivePath(deviceDir, attemptId) {
|
|
11531
11568
|
if (!UUID_RE.test(attemptId)) throw new Error(`docker: not an attempt id: ${attemptId}`);
|
|
11532
11569
|
return join17(deviceDir, "attempts", attemptId, "session.tar");
|
|
@@ -11603,7 +11640,7 @@ function createDockerProvider(opts) {
|
|
|
11603
11640
|
const killGraceMs = opts.killGraceMs ?? DEFAULT_KILL_GRACE_MS2;
|
|
11604
11641
|
const sessionCap = opts.sessionCapBytes ?? DEFAULT_SESSION_CAP_BYTES;
|
|
11605
11642
|
const collectCap = opts.collectCapBytes ?? COLLECT_CAP_BYTES;
|
|
11606
|
-
const
|
|
11643
|
+
const sharedNetwork = `cabane-runners-${opts.deviceKey}`;
|
|
11607
11644
|
const companionLabel = createHash4("sha256").update(opts.companion.key).digest("hex").slice(0, 16);
|
|
11608
11645
|
const proxy = `cabane-egress-${opts.deviceKey}`;
|
|
11609
11646
|
const held = /* @__PURE__ */ new Map();
|
|
@@ -11636,32 +11673,14 @@ function createDockerProvider(opts) {
|
|
|
11636
11673
|
}
|
|
11637
11674
|
throw new Error("docker: the egress proxy never started listening");
|
|
11638
11675
|
};
|
|
11639
|
-
let
|
|
11676
|
+
let proxyReady = null;
|
|
11640
11677
|
const proxyKey = async () => {
|
|
11641
11678
|
const id = (await docker.run(["image", "inspect", "-f", "{{.Id}}", opts.image])).stdout.trim();
|
|
11642
11679
|
return createHash4("sha256").update(JSON.stringify([id || opts.image, opts.companion.key, [...opts.egressAllow].sort()])).digest("hex").slice(0, 16);
|
|
11643
11680
|
};
|
|
11644
|
-
const
|
|
11645
|
-
|
|
11681
|
+
const ensureProxy = () => {
|
|
11682
|
+
proxyReady ??= (async () => {
|
|
11646
11683
|
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
11684
|
const seen = await docker.run([
|
|
11666
11685
|
"inspect",
|
|
11667
11686
|
"-f",
|
|
@@ -11670,6 +11689,7 @@ function createDockerProvider(opts) {
|
|
|
11670
11689
|
]);
|
|
11671
11690
|
const [key, running] = seen.stdout.trim().split(" ");
|
|
11672
11691
|
if (seen.code === 0 && key === allowKey) {
|
|
11692
|
+
await retireSharedNetwork();
|
|
11673
11693
|
if (running !== "true") {
|
|
11674
11694
|
await must(["start", proxy], "starting the egress proxy");
|
|
11675
11695
|
await proxyListening();
|
|
@@ -11704,17 +11724,61 @@ function createDockerProvider(opts) {
|
|
|
11704
11724
|
],
|
|
11705
11725
|
"starting the egress proxy"
|
|
11706
11726
|
);
|
|
11707
|
-
await
|
|
11708
|
-
["network", "connect", "--alias", PROXY_ALIAS, network, proxy],
|
|
11709
|
-
"connecting the proxy"
|
|
11710
|
-
);
|
|
11727
|
+
await retireSharedNetwork();
|
|
11711
11728
|
await proxyListening();
|
|
11712
|
-
log.info({
|
|
11729
|
+
log.info({ proxy, allow: opts.egressAllow }, "runners: egress proxy ready");
|
|
11713
11730
|
})().catch((err) => {
|
|
11714
|
-
|
|
11731
|
+
proxyReady = null;
|
|
11715
11732
|
throw err;
|
|
11716
11733
|
});
|
|
11717
|
-
return
|
|
11734
|
+
return proxyReady;
|
|
11735
|
+
};
|
|
11736
|
+
const retireSharedNetwork = async () => {
|
|
11737
|
+
if ((await docker.run(["network", "inspect", sharedNetwork])).code !== 0) return;
|
|
11738
|
+
await docker.run(["network", "disconnect", "-f", sharedNetwork, proxy]);
|
|
11739
|
+
const on = await docker.run(["ps", "-aq", "--filter", `network=${sharedNetwork}`]);
|
|
11740
|
+
if (on.code === 0 && !on.stdout.trim()) await docker.run(["network", "rm", sharedNetwork]);
|
|
11741
|
+
};
|
|
11742
|
+
const networksOf = async (container) => (await docker.run([
|
|
11743
|
+
"inspect",
|
|
11744
|
+
"-f",
|
|
11745
|
+
"{{range $k, $_ := .NetworkSettings.Networks}}{{$k}} {{end}}",
|
|
11746
|
+
container
|
|
11747
|
+
])).stdout.split(/\s+/).filter(Boolean);
|
|
11748
|
+
const ensurePairNetwork = async (net, attempt) => {
|
|
11749
|
+
if ((await docker.run(["network", "inspect", net])).code !== 0) {
|
|
11750
|
+
const made = await docker.run([
|
|
11751
|
+
"network",
|
|
11752
|
+
"create",
|
|
11753
|
+
// No route off the host, and no address of the host's on the bridge:
|
|
11754
|
+
// the host's own ports are out of reach too. The proxy is the only
|
|
11755
|
+
// other thing on the network.
|
|
11756
|
+
"--internal",
|
|
11757
|
+
"-o",
|
|
11758
|
+
"com.docker.network.bridge.inhibit_ipv4=true",
|
|
11759
|
+
"--label",
|
|
11760
|
+
`cabane.device=${opts.deviceKey}`,
|
|
11761
|
+
"--label",
|
|
11762
|
+
`cabane.conversation=${attempt.conversationId}`,
|
|
11763
|
+
"--label",
|
|
11764
|
+
`cabane.agent=${attempt.agentId}`,
|
|
11765
|
+
net
|
|
11766
|
+
]);
|
|
11767
|
+
if (made.code !== 0) throw networkCreateError(made.stderr);
|
|
11768
|
+
}
|
|
11769
|
+
if (!(await networksOf(proxy)).includes(net))
|
|
11770
|
+
await must(
|
|
11771
|
+
["network", "connect", "--alias", PROXY_ALIAS, net, proxy],
|
|
11772
|
+
"connecting the proxy"
|
|
11773
|
+
);
|
|
11774
|
+
};
|
|
11775
|
+
const settleNetworks = async (name, net) => {
|
|
11776
|
+
const on = await networksOf(name);
|
|
11777
|
+
if (!on.includes(net)) await must(["network", "connect", net, name], "moving the container");
|
|
11778
|
+
const others = on.filter((n) => n !== net);
|
|
11779
|
+
for (const other of others)
|
|
11780
|
+
await must(["network", "disconnect", "-f", other, name], "moving the container");
|
|
11781
|
+
if (others.includes(sharedNetwork)) await retireSharedNetwork();
|
|
11718
11782
|
};
|
|
11719
11783
|
const socketRoot = opts.socketRoot ?? join17(
|
|
11720
11784
|
process.env.XDG_RUNTIME_DIR && existsSync13(process.env.XDG_RUNTIME_DIR) ? process.env.XDG_RUNTIME_DIR : tmpdir2(),
|
|
@@ -11746,7 +11810,7 @@ function createDockerProvider(opts) {
|
|
|
11746
11810
|
"--label",
|
|
11747
11811
|
`cabane.companion=${companionLabel}`,
|
|
11748
11812
|
"--network",
|
|
11749
|
-
|
|
11813
|
+
networkName(attempt),
|
|
11750
11814
|
"--mount",
|
|
11751
11815
|
`type=volume,src=${name}-home,dst=${CONTAINER_HOME}`,
|
|
11752
11816
|
"--mount",
|
|
@@ -11772,6 +11836,17 @@ function createDockerProvider(opts) {
|
|
|
11772
11836
|
await docker.run(["rm", "-f", "-v", name]);
|
|
11773
11837
|
await docker.run(["volume", "rm", "-f", `${name}-home`, `${name}-work`]);
|
|
11774
11838
|
rmSync9(socketDir(name), { recursive: true, force: true });
|
|
11839
|
+
await dropNetwork(name);
|
|
11840
|
+
};
|
|
11841
|
+
const dropNetwork = async (name) => {
|
|
11842
|
+
if ((await docker.run(["network", "inspect", name])).code !== 0) return;
|
|
11843
|
+
await docker.run(["network", "disconnect", "-f", name, proxy]);
|
|
11844
|
+
const gone = await docker.run(["network", "rm", name]);
|
|
11845
|
+
if (gone.code !== 0)
|
|
11846
|
+
log.warn(
|
|
11847
|
+
{ network: name, err: gone.stderr.trim() },
|
|
11848
|
+
"runners: couldn't remove a pair's network"
|
|
11849
|
+
);
|
|
11775
11850
|
};
|
|
11776
11851
|
const containerEnv = () => {
|
|
11777
11852
|
const proxyUrl = `http://${PROXY_ALIAS}:${PROXY_PORT}`;
|
|
@@ -11938,7 +12013,7 @@ function createDockerProvider(opts) {
|
|
|
11938
12013
|
});
|
|
11939
12014
|
await before;
|
|
11940
12015
|
try {
|
|
11941
|
-
await
|
|
12016
|
+
await ensureProxy();
|
|
11942
12017
|
mkdirSync10(socketRoot, { recursive: true, mode: 448 });
|
|
11943
12018
|
mkdirSync10(socketDir(name), { recursive: true, mode: 457 });
|
|
11944
12019
|
let seen = await docker.run([
|
|
@@ -11968,9 +12043,12 @@ function createDockerProvider(opts) {
|
|
|
11968
12043
|
);
|
|
11969
12044
|
seen = { code: 1, stdout: "", stderr: "" };
|
|
11970
12045
|
}
|
|
12046
|
+
await ensurePairNetwork(networkName(attempt), attempt);
|
|
11971
12047
|
if (seen.code !== 0) {
|
|
11972
12048
|
await create(name, attempt);
|
|
11973
12049
|
log.debug({ container: name }, "runners: container created");
|
|
12050
|
+
} else {
|
|
12051
|
+
await settleNetworks(name, networkName(attempt));
|
|
11974
12052
|
}
|
|
11975
12053
|
if (seen.stdout.trim().split(" ")[0] !== "true")
|
|
11976
12054
|
await must(["start", name], "starting the container");
|
|
@@ -11984,7 +12062,8 @@ function createDockerProvider(opts) {
|
|
|
11984
12062
|
return { provider: "docker", id: name };
|
|
11985
12063
|
} catch (err) {
|
|
11986
12064
|
const keep = opts.keepContainers || readPending(name).length > 0;
|
|
11987
|
-
|
|
12065
|
+
const kept = keep && (await docker.run(["inspect", "-f", "{{.Id}}", name])).code === 0;
|
|
12066
|
+
await (kept ? docker.run(["stop", "-t", "1", name]) : keep ? dropNetwork(networkName(attempt)) : destroy(name)).catch(() => {
|
|
11988
12067
|
});
|
|
11989
12068
|
release();
|
|
11990
12069
|
throw err;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cabane/companion",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.128",
|
|
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",
|