@cabane/companion 0.6.124 → 0.6.126
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/README.md +49 -0
- package/dist/cli.js +2560 -463
- package/dist/pairing-config.js +37 -2
- package/dist/runtime.js +2021 -331
- package/package.json +1 -1
package/dist/runtime.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// src/runtime.ts
|
|
2
|
-
import { randomUUID as
|
|
2
|
+
import { randomUUID as randomUUID7 } from "crypto";
|
|
3
3
|
|
|
4
4
|
// src/config.ts
|
|
5
5
|
import {
|
|
@@ -358,10 +358,45 @@ var companionConfigSchema = z2.object({
|
|
|
358
358
|
// wraps the runner in an OS sandbox where the host supports one; `'none'`
|
|
359
359
|
// (the default) runs it as a plain child beside this process, a supported
|
|
360
360
|
// configuration in its own right rather than a degraded one.
|
|
361
|
+
// CT1684: `policy` is what the OS sandbox lets a turn reach, read and hold
|
|
362
|
+
// (`runner/sandbox-policy.ts`). It is validated there rather than here, so a
|
|
363
|
+
// bad policy refuses the turns it governs, with the field named, instead of
|
|
364
|
+
// keeping the companion from starting at all.
|
|
361
365
|
runners: z2.object({
|
|
362
366
|
enabled: z2.boolean().optional(),
|
|
363
|
-
provider: z2.
|
|
364
|
-
sandbox: z2.enum(["os", "none"]).optional()
|
|
367
|
+
provider: z2.enum(["local-process", "docker"]).optional(),
|
|
368
|
+
sandbox: z2.enum(["os", "none"]).optional(),
|
|
369
|
+
// CT1681: the `docker` provider — a container per conversation and agent,
|
|
370
|
+
// from one image, on a network whose only way out is an allow-listed proxy.
|
|
371
|
+
docker: z2.object({
|
|
372
|
+
// The base image a container runs. Default: the one this companion
|
|
373
|
+
// builds from its own Dockerfile on first use. One named here must
|
|
374
|
+
// have that one's layout (the `runner` user, `cabane-companion` on
|
|
375
|
+
// PATH reading `CABANE_COMPANION_ROOT`).
|
|
376
|
+
image: z2.string().min(1).optional(),
|
|
377
|
+
// The companion install mounted into every container. Default, on
|
|
378
|
+
// Linux: this companion's own. Needed for one run from a checkout
|
|
379
|
+
// (a `pnpm --filter @cabane/companion deploy --prod <dir>`).
|
|
380
|
+
companionDir: z2.string().min(1).optional(),
|
|
381
|
+
// Hosts a turn may reach besides the Cabane API, the model providers
|
|
382
|
+
// and the git host. `*.example.com` matches any subdomain.
|
|
383
|
+
egressAllow: z2.array(z2.string().min(1)).optional(),
|
|
384
|
+
// Keep a turn's container between turns and start it again for the
|
|
385
|
+
// next one. Off by default: the container goes after every turn and
|
|
386
|
+
// the runtime's session files are restored into the next.
|
|
387
|
+
keepContainers: z2.boolean().optional(),
|
|
388
|
+
// The Cabane URL as a container reaches it, for a device whose own
|
|
389
|
+
// `baseUrl` is a loopback address the container can't see.
|
|
390
|
+
apiUrl: z2.string().url().refine(isAllowedBaseUrl).optional(),
|
|
391
|
+
// Env set on each turn's runner, `${NAME}` resolved from secrets.json
|
|
392
|
+
// at exec: the runtime's own auth, which the image never carries.
|
|
393
|
+
env: z2.record(z2.string(), z2.string()).optional(),
|
|
394
|
+
// A git credential helper run on this device to answer the
|
|
395
|
+
// container's git for an allowed host, e.g. `!gh auth git-credential`.
|
|
396
|
+
// Absent: git's own configured helpers on this device.
|
|
397
|
+
gitCredentialHelper: z2.string().min(1).optional()
|
|
398
|
+
}).strict().optional(),
|
|
399
|
+
policy: z2.unknown().optional()
|
|
365
400
|
}).strict().optional()
|
|
366
401
|
});
|
|
367
402
|
function runnersEnabled(cfg) {
|
|
@@ -778,7 +813,8 @@ function buildCompanionManifest(opts) {
|
|
|
778
813
|
return {
|
|
779
814
|
runtimes,
|
|
780
815
|
capabilities: { ...DEVICE_MANIFEST.capabilities },
|
|
781
|
-
runtimeIssues: opts.runtimeIssues ?? []
|
|
816
|
+
runtimeIssues: opts.runtimeIssues ?? [],
|
|
817
|
+
...opts.runnerProvider ? { runner: { provider: opts.runnerProvider } } : {}
|
|
782
818
|
};
|
|
783
819
|
}
|
|
784
820
|
function runtimeIssuesFor(gates) {
|
|
@@ -2294,7 +2330,7 @@ import { randomUUID } from "crypto";
|
|
|
2294
2330
|
var bootId = randomUUID();
|
|
2295
2331
|
|
|
2296
2332
|
// src/supervisor.ts
|
|
2297
|
-
import { randomUUID as
|
|
2333
|
+
import { randomUUID as randomUUID6 } from "crypto";
|
|
2298
2334
|
|
|
2299
2335
|
// src/api.ts
|
|
2300
2336
|
var DEVICE_HEADER = "x-cabane-device";
|
|
@@ -3961,8 +3997,8 @@ function readThinking(runtimeOptions) {
|
|
|
3961
3997
|
var CLAUDE_CODE_DEFAULT_MODEL_BARE = "default";
|
|
3962
3998
|
function parseClaudeCodeModel(model) {
|
|
3963
3999
|
if (!model) return null;
|
|
3964
|
-
const
|
|
3965
|
-
const bare =
|
|
4000
|
+
const sep2 = model.indexOf("/");
|
|
4001
|
+
const bare = sep2 === -1 || model.slice(0, sep2) !== "anthropic" ? model : model.slice(sep2 + 1);
|
|
3966
4002
|
return bare === CLAUDE_CODE_DEFAULT_MODEL_BARE ? null : model;
|
|
3967
4003
|
}
|
|
3968
4004
|
|
|
@@ -5313,9 +5349,9 @@ function readOpencodeDialect(runtimeOptions) {
|
|
|
5313
5349
|
|
|
5314
5350
|
// packages/agent-runtime/src/opencode/model.ts
|
|
5315
5351
|
function parseOpencodeModel(model) {
|
|
5316
|
-
const
|
|
5317
|
-
if (
|
|
5318
|
-
return { providerID: model.slice(0,
|
|
5352
|
+
const sep2 = model.indexOf("/");
|
|
5353
|
+
if (sep2 === -1) return { providerID: "", modelID: model };
|
|
5354
|
+
return { providerID: model.slice(0, sep2), modelID: model.slice(sep2 + 1) };
|
|
5319
5355
|
}
|
|
5320
5356
|
|
|
5321
5357
|
// packages/agent-runtime/src/opencode/run-spec.ts
|
|
@@ -5567,10 +5603,10 @@ async function* parseSseStream(body, sessionId, signal) {
|
|
|
5567
5603
|
const { value, done } = await reader.read();
|
|
5568
5604
|
if (done) return;
|
|
5569
5605
|
buf += decoder.decode(value, { stream: true });
|
|
5570
|
-
let
|
|
5571
|
-
while ((
|
|
5572
|
-
const frame = buf.slice(0,
|
|
5573
|
-
buf = buf.slice(
|
|
5606
|
+
let sep2;
|
|
5607
|
+
while ((sep2 = buf.indexOf("\n\n")) !== -1) {
|
|
5608
|
+
const frame = buf.slice(0, sep2);
|
|
5609
|
+
buf = buf.slice(sep2 + 2);
|
|
5574
5610
|
const data = frame.split("\n").filter((l) => l.startsWith("data:")).map((l) => l.slice(5).trim()).join("");
|
|
5575
5611
|
if (!data) continue;
|
|
5576
5612
|
let ev;
|
|
@@ -6530,8 +6566,8 @@ function readCodexDialect(runtimeOptions) {
|
|
|
6530
6566
|
// packages/agent-runtime/src/codex/model.ts
|
|
6531
6567
|
var CODEX_DEFAULT_MODEL_BARES = /* @__PURE__ */ new Set(["default"]);
|
|
6532
6568
|
function parseCodexModel(model) {
|
|
6533
|
-
const
|
|
6534
|
-
const bare =
|
|
6569
|
+
const sep2 = model.indexOf("/");
|
|
6570
|
+
const bare = sep2 === -1 || model.slice(0, sep2) !== "openai" ? model : model.slice(sep2 + 1);
|
|
6535
6571
|
return CODEX_DEFAULT_MODEL_BARES.has(bare) ? null : bare;
|
|
6536
6572
|
}
|
|
6537
6573
|
|
|
@@ -7841,6 +7877,149 @@ import { join as join14 } from "path";
|
|
|
7841
7877
|
// src/runner/launch.ts
|
|
7842
7878
|
import { createInterface } from "readline";
|
|
7843
7879
|
|
|
7880
|
+
// src/runner/sandbox-policy.ts
|
|
7881
|
+
import { NetworkConfigSchema } from "@anthropic-ai/sandbox-runtime";
|
|
7882
|
+
import { globToRegex } from "@anthropic-ai/sandbox-runtime/dist/sandbox/sandbox-utils.js";
|
|
7883
|
+
import { z as z13 } from "zod";
|
|
7884
|
+
var hostPattern = z13.string().min(1);
|
|
7885
|
+
var secretRef = z13.string().regex(/^\$\{[A-Za-z_][A-Za-z0-9_]*\}$/, "must be a ${NAME} reference into secrets.json");
|
|
7886
|
+
var envCredentialSchema = z13.object({ secret: secretRef, hosts: z13.array(hostPattern).min(1) }).strict();
|
|
7887
|
+
var gitCredentialSchema = z13.object({
|
|
7888
|
+
host: z13.string().regex(/^[A-Za-z0-9.-]+(:\d+)?$/, "must be a host name, like github.com"),
|
|
7889
|
+
username: z13.string().min(1).optional(),
|
|
7890
|
+
secret: secretRef
|
|
7891
|
+
}).strict();
|
|
7892
|
+
var sandboxPolicySchema = z13.object({
|
|
7893
|
+
hosts: z13.object({
|
|
7894
|
+
allow: z13.array(hostPattern).optional(),
|
|
7895
|
+
deny: z13.array(hostPattern).optional(),
|
|
7896
|
+
defaults: z13.boolean().optional()
|
|
7897
|
+
}).strict().optional(),
|
|
7898
|
+
filesystem: z13.object({
|
|
7899
|
+
mode: z13.enum(["blocklist", "strict"]).optional(),
|
|
7900
|
+
denyRead: z13.array(z13.string().min(1)).optional(),
|
|
7901
|
+
allowRead: z13.array(z13.string().min(1)).optional(),
|
|
7902
|
+
allowWrite: z13.array(z13.string().min(1)).optional()
|
|
7903
|
+
}).strict().optional(),
|
|
7904
|
+
credentials: z13.object({
|
|
7905
|
+
env: z13.record(z13.string().regex(/^[A-Za-z_][A-Za-z0-9_]*$/), envCredentialSchema).optional(),
|
|
7906
|
+
git: z13.array(gitCredentialSchema).optional()
|
|
7907
|
+
}).strict().optional()
|
|
7908
|
+
}).strict();
|
|
7909
|
+
function parseSandboxPolicy(raw) {
|
|
7910
|
+
if (raw === void 0) return { ok: true, policy: {} };
|
|
7911
|
+
const result = sandboxPolicySchema.safeParse(raw);
|
|
7912
|
+
if (!result.success) {
|
|
7913
|
+
const issue = result.error.issues[0];
|
|
7914
|
+
return { ok: false, error: `${fieldPath(issue.path)}: ${issue.message}` };
|
|
7915
|
+
}
|
|
7916
|
+
const refused = unenforceable(result.data);
|
|
7917
|
+
return refused ? { ok: false, error: refused } : { ok: true, policy: result.data };
|
|
7918
|
+
}
|
|
7919
|
+
function fieldPath(path) {
|
|
7920
|
+
return ["runners", "policy", ...path.map(String)].join(".");
|
|
7921
|
+
}
|
|
7922
|
+
function unenforceable(policy) {
|
|
7923
|
+
const hostLists = [
|
|
7924
|
+
["hosts.allow", "allowedDomains", policy.hosts?.allow],
|
|
7925
|
+
["hosts.deny", "deniedDomains", policy.hosts?.deny],
|
|
7926
|
+
...Object.entries(policy.credentials?.env ?? {}).map(
|
|
7927
|
+
([name, entry]) => [
|
|
7928
|
+
`credentials.env.${name}.hosts`,
|
|
7929
|
+
"allowedDomains",
|
|
7930
|
+
entry.hosts
|
|
7931
|
+
]
|
|
7932
|
+
)
|
|
7933
|
+
];
|
|
7934
|
+
for (const [field, as, hosts] of hostLists) {
|
|
7935
|
+
for (const [i, host] of (hosts ?? []).entries()) {
|
|
7936
|
+
const check = NetworkConfigSchema.safeParse({
|
|
7937
|
+
allowedDomains: as === "allowedDomains" ? [host] : [],
|
|
7938
|
+
deniedDomains: as === "deniedDomains" ? [host] : []
|
|
7939
|
+
});
|
|
7940
|
+
if (!check.success) {
|
|
7941
|
+
const message = check.error.issues[0]?.message ?? "not a host pattern the sandbox accepts";
|
|
7942
|
+
return `runners.policy.${field}.${i}: ${message}`;
|
|
7943
|
+
}
|
|
7944
|
+
}
|
|
7945
|
+
}
|
|
7946
|
+
const pathLists = [
|
|
7947
|
+
["filesystem.denyRead", policy.filesystem?.denyRead],
|
|
7948
|
+
["filesystem.allowRead", policy.filesystem?.allowRead],
|
|
7949
|
+
["filesystem.allowWrite", policy.filesystem?.allowWrite]
|
|
7950
|
+
];
|
|
7951
|
+
for (const [field, paths] of pathLists) {
|
|
7952
|
+
for (const [i, path] of (paths ?? []).entries()) {
|
|
7953
|
+
try {
|
|
7954
|
+
new RegExp(globToRegex(path));
|
|
7955
|
+
} catch (err) {
|
|
7956
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
7957
|
+
return `runners.policy.${field}.${i}: not a pattern the sandbox can match (${message})`;
|
|
7958
|
+
}
|
|
7959
|
+
}
|
|
7960
|
+
}
|
|
7961
|
+
return null;
|
|
7962
|
+
}
|
|
7963
|
+
var placeholderName = (ref) => ref.slice(2, -1);
|
|
7964
|
+
function gitAuthVar(index) {
|
|
7965
|
+
return `CABANE_GIT_AUTH_${index}`;
|
|
7966
|
+
}
|
|
7967
|
+
function resolvePolicyCredentials(policy, store) {
|
|
7968
|
+
const missing = /* @__PURE__ */ new Set();
|
|
7969
|
+
const credentials = [];
|
|
7970
|
+
const lookup2 = (ref) => {
|
|
7971
|
+
const name = placeholderName(ref);
|
|
7972
|
+
const value = store.get(name);
|
|
7973
|
+
if (value === void 0) missing.add(name);
|
|
7974
|
+
return value;
|
|
7975
|
+
};
|
|
7976
|
+
for (const [name, entry] of Object.entries(policy.credentials?.env ?? {})) {
|
|
7977
|
+
const value = lookup2(entry.secret);
|
|
7978
|
+
if (value === void 0) continue;
|
|
7979
|
+
credentials.push({ name, value, hosts: entry.hosts });
|
|
7980
|
+
}
|
|
7981
|
+
(policy.credentials?.git ?? []).forEach((entry, i) => {
|
|
7982
|
+
const secret = lookup2(entry.secret);
|
|
7983
|
+
if (secret === void 0) return;
|
|
7984
|
+
const user = entry.username ?? "x-access-token";
|
|
7985
|
+
credentials.push({
|
|
7986
|
+
name: gitAuthVar(i),
|
|
7987
|
+
value: Buffer.from(`${user}:${secret}`).toString("base64"),
|
|
7988
|
+
hosts: [entry.host.replace(/:\d+$/, "")],
|
|
7989
|
+
gitHost: entry.host
|
|
7990
|
+
});
|
|
7991
|
+
});
|
|
7992
|
+
return { credentials, missing: [...missing].sort() };
|
|
7993
|
+
}
|
|
7994
|
+
var HarnessSandboxRefused = class extends Error {
|
|
7995
|
+
};
|
|
7996
|
+
function policyHasCredentials(policy) {
|
|
7997
|
+
return Object.keys(policy.credentials?.env ?? {}).length > 0 || (policy.credentials?.git ?? []).length > 0;
|
|
7998
|
+
}
|
|
7999
|
+
function postureLines(parsed) {
|
|
8000
|
+
if (!parsed.ok) return [`policy invalid \u2014 turns are refused until it's fixed: ${parsed.error}`];
|
|
8001
|
+
const { policy } = parsed;
|
|
8002
|
+
const mode = policy.filesystem?.mode ?? "blocklist";
|
|
8003
|
+
const defaults = policy.hosts?.defaults !== false;
|
|
8004
|
+
const lines2 = [
|
|
8005
|
+
`filesystem: ${mode}${mode === "strict" ? " (home unreadable except the checkout, harness state and allowRead)" : ""}`,
|
|
8006
|
+
`hosts: Cabane + the turn's model provider${defaults ? " + defaults (GitHub, npm, PyPI, crates.io)" : ""}${policy.hosts?.allow?.length ? ` + ${policy.hosts.allow.join(", ")}` : ""}`
|
|
8007
|
+
];
|
|
8008
|
+
if (policy.hosts?.deny?.length) lines2.push(`hosts denied: ${policy.hosts.deny.join(", ")}`);
|
|
8009
|
+
const extraDeny = policy.filesystem?.denyRead ?? [];
|
|
8010
|
+
if (extraDeny.length) lines2.push(`denied reads (extra): ${extraDeny.join(", ")}`);
|
|
8011
|
+
const allowRead = policy.filesystem?.allowRead ?? [];
|
|
8012
|
+
if (allowRead.length) lines2.push(`reads allowed back: ${allowRead.join(", ")}`);
|
|
8013
|
+
const allowWrite = policy.filesystem?.allowWrite ?? [];
|
|
8014
|
+
if (allowWrite.length) lines2.push(`writes allowed (extra): ${allowWrite.join(", ")}`);
|
|
8015
|
+
const env = Object.keys(policy.credentials?.env ?? {});
|
|
8016
|
+
const git = (policy.credentials?.git ?? []).map((g) => `git@${g.host}`);
|
|
8017
|
+
if (env.length || git.length) {
|
|
8018
|
+
lines2.push(`masked credentials: ${[...env, ...git].join(", ")}`);
|
|
8019
|
+
}
|
|
8020
|
+
return lines2;
|
|
8021
|
+
}
|
|
8022
|
+
|
|
7844
8023
|
// src/runner/opencode-lock.ts
|
|
7845
8024
|
function opencodeLockKey(serverUrl) {
|
|
7846
8025
|
return `opencode:${serverUrl}`;
|
|
@@ -8151,43 +8330,145 @@ function createTurnContainment(turnId, deps = {}) {
|
|
|
8151
8330
|
};
|
|
8152
8331
|
}
|
|
8153
8332
|
|
|
8154
|
-
// src/
|
|
8333
|
+
// src/secrets.ts
|
|
8334
|
+
import { existsSync as existsSync7, readFileSync as readFileSync6 } from "fs";
|
|
8335
|
+
import { join as join10 } from "path";
|
|
8155
8336
|
import { z as z14 } from "zod";
|
|
8337
|
+
var PLACEHOLDER_RE = /\$\{([A-Za-z_][A-Za-z0-9_]*)\}/g;
|
|
8338
|
+
function secretsPath() {
|
|
8339
|
+
return join10(cabaneDir(), "secrets.json");
|
|
8340
|
+
}
|
|
8341
|
+
var secretStoreSchema = z14.record(z14.string(), z14.string());
|
|
8342
|
+
function loadSecretStore() {
|
|
8343
|
+
const path = secretsPath();
|
|
8344
|
+
if (!existsSync7(path)) return makeStore({});
|
|
8345
|
+
let raw;
|
|
8346
|
+
try {
|
|
8347
|
+
raw = readFileSync6(path, "utf8");
|
|
8348
|
+
} catch (err) {
|
|
8349
|
+
throw new ConfigError(
|
|
8350
|
+
`couldn't read ${path}: ${err instanceof Error ? err.message : String(err)}`
|
|
8351
|
+
);
|
|
8352
|
+
}
|
|
8353
|
+
if (raw.trim().length === 0) return makeStore({});
|
|
8354
|
+
let parsed;
|
|
8355
|
+
try {
|
|
8356
|
+
parsed = JSON.parse(raw);
|
|
8357
|
+
} catch (err) {
|
|
8358
|
+
throw new ConfigError(
|
|
8359
|
+
`${path} is not valid JSON: ${err instanceof Error ? err.message : String(err)}. It must be a flat object of "NAME": "value" secret pairs.`
|
|
8360
|
+
);
|
|
8361
|
+
}
|
|
8362
|
+
const result = secretStoreSchema.safeParse(parsed);
|
|
8363
|
+
if (!result.success) {
|
|
8364
|
+
throw new ConfigError(
|
|
8365
|
+
`${path} must be a flat object mapping secret names to string values (e.g. { "GITHUB_TOKEN": "ghp_\u2026" }).`
|
|
8366
|
+
);
|
|
8367
|
+
}
|
|
8368
|
+
return makeStore(result.data);
|
|
8369
|
+
}
|
|
8370
|
+
function loadSecretStoreTolerant(onWarn) {
|
|
8371
|
+
try {
|
|
8372
|
+
return loadSecretStore();
|
|
8373
|
+
} catch (err) {
|
|
8374
|
+
onWarn?.(err instanceof Error ? err.message : String(err));
|
|
8375
|
+
return makeStore({});
|
|
8376
|
+
}
|
|
8377
|
+
}
|
|
8378
|
+
function makeStore(map) {
|
|
8379
|
+
return {
|
|
8380
|
+
get: (name) => map[name],
|
|
8381
|
+
names: () => Object.keys(map).sort()
|
|
8382
|
+
};
|
|
8383
|
+
}
|
|
8384
|
+
function requiredSecretNames(mcpServers) {
|
|
8385
|
+
const names = /* @__PURE__ */ new Set();
|
|
8386
|
+
for (const server of Object.values(mcpServers ?? {})) {
|
|
8387
|
+
const bags = [
|
|
8388
|
+
"env" in server ? server.env : void 0,
|
|
8389
|
+
"headers" in server ? server.headers : void 0
|
|
8390
|
+
];
|
|
8391
|
+
for (const bag of bags) {
|
|
8392
|
+
for (const value of Object.values(bag ?? {})) {
|
|
8393
|
+
for (const [, name] of value.matchAll(PLACEHOLDER_RE)) names.add(name);
|
|
8394
|
+
}
|
|
8395
|
+
}
|
|
8396
|
+
}
|
|
8397
|
+
return [...names].sort();
|
|
8398
|
+
}
|
|
8399
|
+
function resolveMcpSecrets(mcpServers, store) {
|
|
8400
|
+
const missing = /* @__PURE__ */ new Set();
|
|
8401
|
+
const interpolate = (value) => value.replace(PLACEHOLDER_RE, (_match, name) => {
|
|
8402
|
+
const resolved = store.get(name);
|
|
8403
|
+
if (resolved === void 0) {
|
|
8404
|
+
missing.add(name);
|
|
8405
|
+
return _match;
|
|
8406
|
+
}
|
|
8407
|
+
return resolved;
|
|
8408
|
+
});
|
|
8409
|
+
const mapBag = (bag) => {
|
|
8410
|
+
const out = {};
|
|
8411
|
+
for (const [k, v] of Object.entries(bag)) out[k] = interpolate(v);
|
|
8412
|
+
return out;
|
|
8413
|
+
};
|
|
8414
|
+
const servers = {};
|
|
8415
|
+
for (const [name, server] of Object.entries(mcpServers)) {
|
|
8416
|
+
const next = { ...server };
|
|
8417
|
+
if ("env" in next && next.env) next.env = mapBag(next.env);
|
|
8418
|
+
if ("headers" in next && next.headers) next.headers = mapBag(next.headers);
|
|
8419
|
+
servers[name] = next;
|
|
8420
|
+
}
|
|
8421
|
+
return { servers, missing: [...missing].sort() };
|
|
8422
|
+
}
|
|
8423
|
+
var ERROR_REASON_MAX = 200;
|
|
8424
|
+
function missingSecretReason(missing) {
|
|
8425
|
+
const prefix = "missing_secret: ";
|
|
8426
|
+
for (let shown = missing.length; shown >= 0; shown--) {
|
|
8427
|
+
const omitted = missing.length - shown;
|
|
8428
|
+
const parts = [...missing.slice(0, shown), ...omitted > 0 ? [`+${omitted}`] : []];
|
|
8429
|
+
const reason = prefix + parts.join(",");
|
|
8430
|
+
if (reason.length <= ERROR_REASON_MAX) return reason;
|
|
8431
|
+
}
|
|
8432
|
+
return `${prefix}+${missing.length}`;
|
|
8433
|
+
}
|
|
8434
|
+
|
|
8435
|
+
// src/runner/protocol.ts
|
|
8436
|
+
import { z as z16 } from "zod";
|
|
8156
8437
|
|
|
8157
8438
|
// src/run-config.ts
|
|
8158
|
-
import { z as
|
|
8159
|
-
var mcpStdioServerSchema =
|
|
8160
|
-
type:
|
|
8161
|
-
command:
|
|
8162
|
-
args:
|
|
8163
|
-
env:
|
|
8439
|
+
import { z as z15 } from "zod";
|
|
8440
|
+
var mcpStdioServerSchema = z15.object({
|
|
8441
|
+
type: z15.literal("stdio").optional(),
|
|
8442
|
+
command: z15.string().min(1),
|
|
8443
|
+
args: z15.array(z15.string()).optional(),
|
|
8444
|
+
env: z15.record(z15.string(), z15.string()).optional()
|
|
8164
8445
|
});
|
|
8165
|
-
var mcpHttpServerSchema =
|
|
8166
|
-
type:
|
|
8167
|
-
url:
|
|
8168
|
-
headers:
|
|
8446
|
+
var mcpHttpServerSchema = z15.object({
|
|
8447
|
+
type: z15.literal("http"),
|
|
8448
|
+
url: z15.string().url(),
|
|
8449
|
+
headers: z15.record(z15.string(), z15.string()).optional()
|
|
8169
8450
|
});
|
|
8170
|
-
var mcpSseServerSchema =
|
|
8171
|
-
type:
|
|
8172
|
-
url:
|
|
8173
|
-
headers:
|
|
8451
|
+
var mcpSseServerSchema = z15.object({
|
|
8452
|
+
type: z15.literal("sse"),
|
|
8453
|
+
url: z15.string().url(),
|
|
8454
|
+
headers: z15.record(z15.string(), z15.string()).optional()
|
|
8174
8455
|
});
|
|
8175
|
-
var mcpServerDefSchema =
|
|
8176
|
-
var thinkingConfigSchema =
|
|
8177
|
-
|
|
8178
|
-
|
|
8179
|
-
|
|
8456
|
+
var mcpServerDefSchema = z15.union([mcpHttpServerSchema, mcpSseServerSchema, mcpStdioServerSchema]);
|
|
8457
|
+
var thinkingConfigSchema = z15.discriminatedUnion("type", [
|
|
8458
|
+
z15.object({ type: z15.literal("adaptive") }),
|
|
8459
|
+
z15.object({ type: z15.literal("enabled"), budgetTokens: z15.number().int().positive().optional() }),
|
|
8460
|
+
z15.object({ type: z15.literal("disabled") })
|
|
8180
8461
|
]);
|
|
8181
|
-
var effortSchema =
|
|
8182
|
-
var runConfigSchema =
|
|
8462
|
+
var effortSchema = z15.enum(["low", "medium", "high", "xhigh", "max"]);
|
|
8463
|
+
var runConfigSchema = z15.object({
|
|
8183
8464
|
// CT788: the host-access binary replaced the `assistant`/`coding`/`custom` mode
|
|
8184
8465
|
// trio + its custom tool lists — `true` grants the host filesystem/shell, absent
|
|
8185
8466
|
// is the locked surface. Kept in lockstep with `@cabane/shared`'s
|
|
8186
8467
|
// `agentRunConfigSchema` (independent zod, forward-compatible: unknown keys are
|
|
8187
8468
|
// stripped, so an older companion riding a newer server never rejects the config).
|
|
8188
|
-
hostAccess:
|
|
8189
|
-
mcpServers:
|
|
8190
|
-
model:
|
|
8469
|
+
hostAccess: z15.boolean().optional(),
|
|
8470
|
+
mcpServers: z15.record(z15.string(), mcpServerDefSchema).optional(),
|
|
8471
|
+
model: z15.string().min(1).optional(),
|
|
8191
8472
|
thinking: thinkingConfigSchema.optional(),
|
|
8192
8473
|
effort: effortSchema.optional()
|
|
8193
8474
|
});
|
|
@@ -8215,56 +8496,75 @@ var RUNNER_ENV = {
|
|
|
8215
8496
|
// Everything else the turn is constructed from, as JSON: see `RunnerSpec`.
|
|
8216
8497
|
spec: "CABANE_RUNNER_SPEC"
|
|
8217
8498
|
};
|
|
8218
|
-
var runtimeHealthSchema =
|
|
8219
|
-
quotaState:
|
|
8220
|
-
limitedUntil:
|
|
8221
|
-
lastError:
|
|
8499
|
+
var runtimeHealthSchema = z16.object({
|
|
8500
|
+
quotaState: z16.enum(["ok", "limited", "auth_expired", "unknown"]),
|
|
8501
|
+
limitedUntil: z16.string().nullable(),
|
|
8502
|
+
lastError: z16.string().nullable()
|
|
8222
8503
|
});
|
|
8223
|
-
var runnerSpecSchema =
|
|
8224
|
-
deviceId:
|
|
8504
|
+
var runnerSpecSchema = z16.object({
|
|
8505
|
+
deviceId: z16.string().optional(),
|
|
8225
8506
|
// The launcher's boot id: the one its heartbeat carries, so a runner's lease
|
|
8226
8507
|
// matches the device that holds it.
|
|
8227
|
-
bootId:
|
|
8228
|
-
workspaceId:
|
|
8229
|
-
workspaceSlug:
|
|
8230
|
-
agentId:
|
|
8231
|
-
agentUsername:
|
|
8232
|
-
agentName:
|
|
8508
|
+
bootId: z16.string(),
|
|
8509
|
+
workspaceId: z16.string(),
|
|
8510
|
+
workspaceSlug: z16.string(),
|
|
8511
|
+
agentId: z16.string(),
|
|
8512
|
+
agentUsername: z16.string(),
|
|
8513
|
+
agentName: z16.string(),
|
|
8233
8514
|
local: localAgentConfigSchema,
|
|
8234
8515
|
runConfig: runConfigSchema,
|
|
8235
|
-
opencodeServerUrl:
|
|
8236
|
-
claudeCodeAvailable:
|
|
8237
|
-
codexAvailable:
|
|
8238
|
-
idleTimeoutMs:
|
|
8239
|
-
totalTimeoutMs:
|
|
8240
|
-
leaseRenewalMs:
|
|
8241
|
-
preparingRowDelayMs:
|
|
8516
|
+
opencodeServerUrl: z16.string().optional(),
|
|
8517
|
+
claudeCodeAvailable: z16.boolean(),
|
|
8518
|
+
codexAvailable: z16.boolean(),
|
|
8519
|
+
idleTimeoutMs: z16.number().int().positive().optional(),
|
|
8520
|
+
totalTimeoutMs: z16.number().int().positive().optional(),
|
|
8521
|
+
leaseRenewalMs: z16.number().int().positive().optional(),
|
|
8522
|
+
preparingRowDelayMs: z16.number().int().nonnegative().optional(),
|
|
8242
8523
|
// Whether the turn writes a transcript. The directory is the runner's own
|
|
8243
8524
|
// (`transcriptsDir()`), so `CABANE_RUNNER_STATE_DIR` moves it with the outbox.
|
|
8244
|
-
transcripts:
|
|
8525
|
+
transcripts: z16.boolean(),
|
|
8245
8526
|
// Whether the harness starts under the OS sandbox. The launcher sends 'os'
|
|
8246
8527
|
// only when this host can make one.
|
|
8247
|
-
sandbox:
|
|
8248
|
-
|
|
8528
|
+
sandbox: z16.enum(["os", "none"]),
|
|
8529
|
+
// CT1684: the device's sandbox policy, validated by the launcher. Absent
|
|
8530
|
+
// when the sandbox is off.
|
|
8531
|
+
sandboxPolicy: sandboxPolicySchema.optional(),
|
|
8532
|
+
logLevel: z16.string(),
|
|
8249
8533
|
// The launcher's connector-health observations, by runtime, as of the start.
|
|
8250
|
-
connectorHealth:
|
|
8534
|
+
connectorHealth: z16.record(z16.string(), runtimeHealthSchema),
|
|
8251
8535
|
// True when this (conversation, agent) pair was already told its session
|
|
8252
8536
|
// could not be saved: the notice is once per pair, not once per process.
|
|
8253
|
-
sessionWriteNoticeClaimed:
|
|
8537
|
+
sessionWriteNoticeClaimed: z16.boolean(),
|
|
8254
8538
|
// CT1680: the runner's identity, minted with its credential. The start
|
|
8255
8539
|
// PATCH carries it so the lease knows which runner holds the span.
|
|
8256
|
-
runnerId:
|
|
8540
|
+
runnerId: z16.string(),
|
|
8541
|
+
// CT1681: the launcher keeps the runtime's session files beside the attempt
|
|
8542
|
+
// and restores them into this runner's home, so the runner names the attempt
|
|
8543
|
+
// each context was composed for and waits for the launcher to say it's in
|
|
8544
|
+
// place. Absent: nothing to wait for (a runner on the launcher's own disk).
|
|
8545
|
+
attemptState: z16.boolean().optional(),
|
|
8546
|
+
// CT1681: MCP `${NAME}` secrets come from the launcher, by name, instead of
|
|
8547
|
+
// this machine's secrets.json. Absent: the runner reads the device's file.
|
|
8548
|
+
secretsFromLauncher: z16.boolean().optional()
|
|
8257
8549
|
});
|
|
8258
|
-
var dispatchSchema =
|
|
8259
|
-
type:
|
|
8260
|
-
agentId:
|
|
8261
|
-
deviceId:
|
|
8262
|
-
conversationId:
|
|
8263
|
-
messageId:
|
|
8264
|
-
episode:
|
|
8550
|
+
var dispatchSchema = z16.object({
|
|
8551
|
+
type: z16.literal("device:dispatch_requested"),
|
|
8552
|
+
agentId: z16.string(),
|
|
8553
|
+
deviceId: z16.string(),
|
|
8554
|
+
conversationId: z16.string(),
|
|
8555
|
+
messageId: z16.string(),
|
|
8556
|
+
episode: z16.number().int().optional()
|
|
8265
8557
|
});
|
|
8266
|
-
var credentialsSchema =
|
|
8267
|
-
credential:
|
|
8558
|
+
var credentialsSchema = z16.object({
|
|
8559
|
+
credential: z16.string().startsWith("cabrn_"),
|
|
8560
|
+
sandboxCredentials: z16.array(
|
|
8561
|
+
z16.object({
|
|
8562
|
+
name: z16.string(),
|
|
8563
|
+
value: z16.string(),
|
|
8564
|
+
hosts: z16.array(z16.string()),
|
|
8565
|
+
gitHost: z16.string().optional()
|
|
8566
|
+
})
|
|
8567
|
+
).optional()
|
|
8268
8568
|
});
|
|
8269
8569
|
function runnerEnv(input) {
|
|
8270
8570
|
return {
|
|
@@ -8276,7 +8576,10 @@ function runnerEnv(input) {
|
|
|
8276
8576
|
};
|
|
8277
8577
|
}
|
|
8278
8578
|
function runnerStdin(input) {
|
|
8279
|
-
return `${JSON.stringify({
|
|
8579
|
+
return `${JSON.stringify({
|
|
8580
|
+
credential: input.credential,
|
|
8581
|
+
...input.sandboxCredentials?.length ? { sandboxCredentials: input.sandboxCredentials } : {}
|
|
8582
|
+
})}
|
|
8280
8583
|
`;
|
|
8281
8584
|
}
|
|
8282
8585
|
function encodeLauncherMessage(message) {
|
|
@@ -8301,6 +8604,11 @@ function selfRunnerCommand() {
|
|
|
8301
8604
|
if (!script) throw new Error("runners: cannot tell which script started this companion");
|
|
8302
8605
|
return [process.execPath, ...process.execArgv, script, "run-turn"];
|
|
8303
8606
|
}
|
|
8607
|
+
function defaultSecretStore(log) {
|
|
8608
|
+
return loadSecretStoreTolerant(
|
|
8609
|
+
(m) => log.warn({ err: m }, "Could not read the secrets file. Check ~/.cabane/secrets.json.")
|
|
8610
|
+
);
|
|
8611
|
+
}
|
|
8304
8612
|
function healthSnapshot(store) {
|
|
8305
8613
|
const out = {};
|
|
8306
8614
|
for (const r of store?.reports() ?? []) {
|
|
@@ -8334,6 +8642,9 @@ function runnerSpec(opts, launch, duties, runnerId) {
|
|
|
8334
8642
|
...opts.preparingRowDelayMs !== void 0 ? { preparingRowDelayMs: opts.preparingRowDelayMs } : {},
|
|
8335
8643
|
transcripts: opts.transcriptDir !== void 0,
|
|
8336
8644
|
sandbox: launch.sandbox ?? "none",
|
|
8645
|
+
...launch.provider.bindAttempt ? { attemptState: true } : {},
|
|
8646
|
+
...launch.provider.inheritsEnvironment === false ? { secretsFromLauncher: true } : {},
|
|
8647
|
+
...launch.sandbox === "os" && launch.policy?.ok ? { sandboxPolicy: launch.policy.policy } : {},
|
|
8337
8648
|
logLevel: opts.log.level,
|
|
8338
8649
|
connectorHealth: healthSnapshot(launch.connectorHealth),
|
|
8339
8650
|
sessionWriteNoticeClaimed: duties.sessionWriteNoticeClaimed()
|
|
@@ -8440,6 +8751,32 @@ async function settleUnstarted(opts, payload, turnId, errorReason, log) {
|
|
|
8440
8751
|
);
|
|
8441
8752
|
}
|
|
8442
8753
|
}
|
|
8754
|
+
function sandboxInputs(launch, log) {
|
|
8755
|
+
const policy = launch.policy ?? { ok: true, policy: {} };
|
|
8756
|
+
if (launch.sandbox !== "os") {
|
|
8757
|
+
if (policy.ok && policyHasCredentials(policy.policy)) {
|
|
8758
|
+
log.debug(
|
|
8759
|
+
"runners: runners.policy names credentials, but turns here run without the OS sandbox; none are given to the harness"
|
|
8760
|
+
);
|
|
8761
|
+
}
|
|
8762
|
+
return { ok: true, credentials: [] };
|
|
8763
|
+
}
|
|
8764
|
+
if (!policy.ok) {
|
|
8765
|
+
log.warn({ error: policy.error }, "runners: the sandbox policy is invalid; refusing the turn");
|
|
8766
|
+
return { ok: false, reason: `setup_failed: ${policy.error}`.slice(0, 200) };
|
|
8767
|
+
}
|
|
8768
|
+
if (!policyHasCredentials(policy.policy)) return { ok: true, credentials: [] };
|
|
8769
|
+
const store = (launch.secretStore ?? defaultSecretStore)(log);
|
|
8770
|
+
const { credentials, missing } = resolvePolicyCredentials(policy.policy, store);
|
|
8771
|
+
if (missing.length) {
|
|
8772
|
+
log.debug(
|
|
8773
|
+
{ missing },
|
|
8774
|
+
"runners: the sandbox policy names secrets this device does not declare"
|
|
8775
|
+
);
|
|
8776
|
+
return { ok: false, reason: missingSecretReason(missing) };
|
|
8777
|
+
}
|
|
8778
|
+
return { ok: true, credentials };
|
|
8779
|
+
}
|
|
8443
8780
|
async function runTurnInRunner(opts, launch, duties, payload, handleOpts) {
|
|
8444
8781
|
const startedAt = Date.now();
|
|
8445
8782
|
const { turnId } = handleOpts;
|
|
@@ -8463,7 +8800,8 @@ async function runTurnInRunner(opts, launch, duties, payload, handleOpts) {
|
|
|
8463
8800
|
agentId: payload.agentId,
|
|
8464
8801
|
turnId
|
|
8465
8802
|
};
|
|
8466
|
-
const
|
|
8803
|
+
const sandboxed = sandboxInputs(launch, log);
|
|
8804
|
+
const minted = sandboxed.ok ? await mintForRunner(opts, payload, handleOpts, controller.signal, log) : { ok: false, reason: sandboxed.reason, errorReason: sandboxed.reason };
|
|
8467
8805
|
if (!minted.ok) {
|
|
8468
8806
|
controller.signal.removeEventListener("abort", forwardStop);
|
|
8469
8807
|
duties.releaseAbort(turnId, controller);
|
|
@@ -8481,15 +8819,42 @@ async function runTurnInRunner(opts, launch, duties, payload, handleOpts) {
|
|
|
8481
8819
|
const remote = [];
|
|
8482
8820
|
const lockOwner = (id) => `${turnId}#${id}`;
|
|
8483
8821
|
const locks = launch.locks;
|
|
8822
|
+
let leasedAttemptId = null;
|
|
8484
8823
|
try {
|
|
8485
8824
|
handle = await launch.provider.acquire(attempt);
|
|
8825
|
+
} catch (err) {
|
|
8826
|
+
const reason2 = `runner_unavailable: ${describeError(err)}`;
|
|
8827
|
+
log.warn(
|
|
8828
|
+
{ turnId, provider: launch.provider.name, err: describeError(err) },
|
|
8829
|
+
"runner: no place to run the turn"
|
|
8830
|
+
);
|
|
8831
|
+
controller.signal.removeEventListener("abort", forwardStop);
|
|
8832
|
+
duties.releaseAbort(turnId, controller);
|
|
8833
|
+
await settleUnstarted(opts, payload, turnId, reason2, log);
|
|
8834
|
+
await launch.reportRunnerGone?.({
|
|
8835
|
+
workspaceId: opts.workspaceId,
|
|
8836
|
+
conversationId: payload.conversationId,
|
|
8837
|
+
agentId: payload.agentId,
|
|
8838
|
+
messageId: payload.messageId,
|
|
8839
|
+
runnerId: minted.runnerId,
|
|
8840
|
+
reason: "provider_failed"
|
|
8841
|
+
});
|
|
8842
|
+
const durationMs2 = Date.now() - startedAt;
|
|
8843
|
+
duties.notifyEnd({ id: payload.messageId, ok: false, durationMs: durationMs2, reason: reason2 });
|
|
8844
|
+
return { ok: false, durationMs: durationMs2, reason: reason2 };
|
|
8845
|
+
}
|
|
8846
|
+
try {
|
|
8847
|
+
const place = handle;
|
|
8486
8848
|
const env = {};
|
|
8487
|
-
|
|
8488
|
-
|
|
8849
|
+
if (launch.provider.inheritsEnvironment !== false) {
|
|
8850
|
+
for (const [k, v] of Object.entries(launch.env ?? process.env)) {
|
|
8851
|
+
if (v !== void 0) env[k] = v;
|
|
8852
|
+
}
|
|
8489
8853
|
}
|
|
8490
8854
|
const input = {
|
|
8491
|
-
baseUrl: opts.baseUrl,
|
|
8855
|
+
baseUrl: launch.runnerBaseUrl ?? opts.baseUrl,
|
|
8492
8856
|
credential: minted.credential,
|
|
8857
|
+
sandboxCredentials: sandboxed.ok ? sandboxed.credentials : [],
|
|
8493
8858
|
dispatch: payload,
|
|
8494
8859
|
turnId,
|
|
8495
8860
|
resumed: handleOpts.resumed,
|
|
@@ -8566,6 +8931,38 @@ async function runTurnInRunner(opts, launch, duties, payload, handleOpts) {
|
|
|
8566
8931
|
case "remote_session":
|
|
8567
8932
|
remote.push({ serverUrl: event.serverUrl, sessionId: event.sessionId });
|
|
8568
8933
|
return;
|
|
8934
|
+
case "attempt": {
|
|
8935
|
+
const bind = launch.provider.bindAttempt?.(place, event.attemptId) ?? Promise.resolve();
|
|
8936
|
+
void bind.then(
|
|
8937
|
+
() => running.stdin.write(encodeLauncherMessage({ cabaneLauncher: "bound", id: event.id })),
|
|
8938
|
+
(err) => {
|
|
8939
|
+
const refused = describeError(err);
|
|
8940
|
+
log.warn(
|
|
8941
|
+
{ attemptId: event.attemptId, err: refused },
|
|
8942
|
+
"runner: couldn't make the place ready for the turn's attempt; not running it there"
|
|
8943
|
+
);
|
|
8944
|
+
running.stdin.write(
|
|
8945
|
+
encodeLauncherMessage({ cabaneLauncher: "bound", id: event.id, refused })
|
|
8946
|
+
);
|
|
8947
|
+
}
|
|
8948
|
+
);
|
|
8949
|
+
return;
|
|
8950
|
+
}
|
|
8951
|
+
case "leased":
|
|
8952
|
+
leasedAttemptId = event.attemptId;
|
|
8953
|
+
return;
|
|
8954
|
+
case "secrets": {
|
|
8955
|
+
const store = (launch.secretStore ?? defaultSecretStore)(log);
|
|
8956
|
+
const values = {};
|
|
8957
|
+
for (const name of event.names) {
|
|
8958
|
+
const value = store.get(name);
|
|
8959
|
+
if (value !== void 0) values[name] = value;
|
|
8960
|
+
}
|
|
8961
|
+
running.stdin.write(
|
|
8962
|
+
encodeLauncherMessage({ cabaneLauncher: "secrets", id: event.id, values })
|
|
8963
|
+
);
|
|
8964
|
+
return;
|
|
8965
|
+
}
|
|
8569
8966
|
}
|
|
8570
8967
|
});
|
|
8571
8968
|
const stderrDone = lines(running.stderr, (line) => relayRunnerLog(line, opts.log));
|
|
@@ -8576,7 +8973,14 @@ async function runTurnInRunner(opts, launch, duties, payload, handleOpts) {
|
|
|
8576
8973
|
child?.stdin.end();
|
|
8577
8974
|
controller.signal.removeEventListener("abort", forwardStop);
|
|
8578
8975
|
duties.releaseAbort(turnId, controller);
|
|
8579
|
-
if (handle)
|
|
8976
|
+
if (handle) {
|
|
8977
|
+
await launch.provider.collect?.(handle, { attemptId: leasedAttemptId }).catch(
|
|
8978
|
+
(err) => log.warn({ err: describeError(err) }, "runner: couldn't collect what the runner left")
|
|
8979
|
+
);
|
|
8980
|
+
await launch.provider.release(handle, { destroy: false }).catch(
|
|
8981
|
+
(err) => log.warn({ err: describeError(err) }, "runner: couldn't give its place back")
|
|
8982
|
+
);
|
|
8983
|
+
}
|
|
8580
8984
|
}
|
|
8581
8985
|
if (result) return result;
|
|
8582
8986
|
const durationMs = Date.now() - startedAt;
|
|
@@ -8635,7 +9039,7 @@ import { existsSync as existsSync9 } from "fs";
|
|
|
8635
9039
|
|
|
8636
9040
|
// src/turn-control-tools.ts
|
|
8637
9041
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
8638
|
-
import { z as
|
|
9042
|
+
import { z as z17 } from "zod";
|
|
8639
9043
|
var COMPANION_LOCAL_MCP_SERVER = "cabane_companion";
|
|
8640
9044
|
var COMPANION_LOCAL_TOOL_GLOB = `mcp__${COMPANION_LOCAL_MCP_SERVER}__*`;
|
|
8641
9045
|
var SKIP_TURN_TOOL = "skip_turn";
|
|
@@ -8698,7 +9102,7 @@ function createTurnControlMcpServer(acts, skipState) {
|
|
|
8698
9102
|
SKIP_TURN_TOOL,
|
|
8699
9103
|
TURN_CONTROL_TOOL_DESCRIPTIONS.skip_turn,
|
|
8700
9104
|
{
|
|
8701
|
-
reason:
|
|
9105
|
+
reason: z17.string().min(1).max(MAX_SKIP_REASON_BYTES).describe(TURN_CONTROL_FIELD_DESCRIPTIONS.skip_turn.reason)
|
|
8702
9106
|
},
|
|
8703
9107
|
async (args) => {
|
|
8704
9108
|
skipState.skipped = true;
|
|
@@ -8714,42 +9118,42 @@ function createTurnControlMcpServer(acts, skipState) {
|
|
|
8714
9118
|
ASK_TOOL,
|
|
8715
9119
|
TURN_CONTROL_TOOL_DESCRIPTIONS.ask,
|
|
8716
9120
|
{
|
|
8717
|
-
targetUserId:
|
|
8718
|
-
question:
|
|
8719
|
-
headline:
|
|
8720
|
-
options:
|
|
8721
|
-
|
|
8722
|
-
|
|
8723
|
-
|
|
8724
|
-
label:
|
|
8725
|
-
move:
|
|
8726
|
-
task:
|
|
8727
|
-
status:
|
|
9121
|
+
targetUserId: z17.string().uuid().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask.targetUserId),
|
|
9122
|
+
question: z17.string().min(1).max(MAX_ASK_QUESTION_BYTES).optional().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask.question),
|
|
9123
|
+
headline: z17.string().min(1).max(MAX_ASK_HEADLINE_BYTES).optional().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask.headline),
|
|
9124
|
+
options: z17.array(
|
|
9125
|
+
z17.union([
|
|
9126
|
+
z17.string().min(1).max(MAX_ASK_OPTION_BYTES),
|
|
9127
|
+
z17.object({
|
|
9128
|
+
label: z17.string().min(1).max(MAX_ASK_OPTION_BYTES).describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask["options.label"]),
|
|
9129
|
+
move: z17.object({
|
|
9130
|
+
task: z17.string().min(1).max(200).describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask["options.move.task"]),
|
|
9131
|
+
status: z17.enum(TASK_STAGES).describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask["options.move.status"])
|
|
8728
9132
|
}).describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask["options.move"])
|
|
8729
9133
|
})
|
|
8730
9134
|
])
|
|
8731
9135
|
).min(MIN_ASK_OPTIONS).max(MAX_ASK_OPTIONS).optional().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask.options),
|
|
8732
|
-
questions:
|
|
8733
|
-
|
|
8734
|
-
headline:
|
|
8735
|
-
body:
|
|
8736
|
-
options:
|
|
8737
|
-
|
|
8738
|
-
|
|
8739
|
-
|
|
8740
|
-
label:
|
|
8741
|
-
move:
|
|
8742
|
-
task:
|
|
8743
|
-
status:
|
|
9136
|
+
questions: z17.array(
|
|
9137
|
+
z17.object({
|
|
9138
|
+
headline: z17.string().min(1).max(MAX_ASK_HEADLINE_BYTES).describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask["questions.headline"]),
|
|
9139
|
+
body: z17.string().min(1).max(MAX_ASK_QUESTION_BYTES).optional().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask["questions.body"]),
|
|
9140
|
+
options: z17.array(
|
|
9141
|
+
z17.union([
|
|
9142
|
+
z17.string().min(1).max(MAX_ASK_OPTION_BYTES),
|
|
9143
|
+
z17.object({
|
|
9144
|
+
label: z17.string().min(1).max(MAX_ASK_OPTION_BYTES).describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask["questions.options.label"]),
|
|
9145
|
+
move: z17.object({
|
|
9146
|
+
task: z17.string().min(1).max(200).describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask["questions.options.move.task"]),
|
|
9147
|
+
status: z17.enum(TASK_STAGES).describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask["questions.options.move.status"])
|
|
8744
9148
|
}).describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask["questions.options.move"])
|
|
8745
9149
|
})
|
|
8746
9150
|
])
|
|
8747
9151
|
).min(MIN_ASK_OPTIONS).max(MAX_ASK_OPTIONS).optional().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask["questions.options"])
|
|
8748
9152
|
})
|
|
8749
9153
|
).min(1).max(MAX_ASK_ITEMS).optional().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask.questions),
|
|
8750
|
-
replaces:
|
|
8751
|
-
askId:
|
|
8752
|
-
reason:
|
|
9154
|
+
replaces: z17.object({
|
|
9155
|
+
askId: z17.string().uuid().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask["replaces.askId"]),
|
|
9156
|
+
reason: z17.string().min(1).max(MAX_WITHDRAW_REASON_BYTES).describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask["replaces.reason"])
|
|
8753
9157
|
}).optional().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask.replaces)
|
|
8754
9158
|
},
|
|
8755
9159
|
async (args) => {
|
|
@@ -8790,9 +9194,9 @@ function createTurnControlMcpServer(acts, skipState) {
|
|
|
8790
9194
|
WAKE_ME_TOOL,
|
|
8791
9195
|
TURN_CONTROL_TOOL_DESCRIPTIONS.wake_me,
|
|
8792
9196
|
{
|
|
8793
|
-
afterSeconds:
|
|
8794
|
-
at:
|
|
8795
|
-
note:
|
|
9197
|
+
afterSeconds: z17.number().int().positive().optional().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.wake_me.afterSeconds),
|
|
9198
|
+
at: z17.string().datetime({ offset: true }).optional().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.wake_me.at),
|
|
9199
|
+
note: z17.string().min(1).max(MAX_WAKE_NOTE_BYTES).describe(TURN_CONTROL_FIELD_DESCRIPTIONS.wake_me.note)
|
|
8796
9200
|
},
|
|
8797
9201
|
async (args) => {
|
|
8798
9202
|
const hasAfter = args.afterSeconds !== void 0;
|
|
@@ -8826,8 +9230,8 @@ function createTurnControlMcpServer(acts, skipState) {
|
|
|
8826
9230
|
WITHDRAW_ASK_TOOL,
|
|
8827
9231
|
TURN_CONTROL_TOOL_DESCRIPTIONS.withdraw_ask,
|
|
8828
9232
|
{
|
|
8829
|
-
askId:
|
|
8830
|
-
reason:
|
|
9233
|
+
askId: z17.string().uuid().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.withdraw_ask.askId),
|
|
9234
|
+
reason: z17.string().min(1).max(MAX_WITHDRAW_REASON_BYTES).describe(TURN_CONTROL_FIELD_DESCRIPTIONS.withdraw_ask.reason)
|
|
8831
9235
|
},
|
|
8832
9236
|
async (args) => performAct(acts, {
|
|
8833
9237
|
kind: "withdraw_ask",
|
|
@@ -8894,11 +9298,11 @@ function trimSlash2(s) {
|
|
|
8894
9298
|
// src/codex-instructions.ts
|
|
8895
9299
|
import { mkdtemp, rm, writeFile } from "fs/promises";
|
|
8896
9300
|
import { tmpdir } from "os";
|
|
8897
|
-
import { join as
|
|
9301
|
+
import { join as join11 } from "path";
|
|
8898
9302
|
var PREFIX = "cabane-codex-instructions-";
|
|
8899
9303
|
async function writeCodexInstructionsFile(contents) {
|
|
8900
|
-
const dir2 = await mkdtemp(
|
|
8901
|
-
const path =
|
|
9304
|
+
const dir2 = await mkdtemp(join11(tmpdir(), PREFIX));
|
|
9305
|
+
const path = join11(dir2, "instructions.md");
|
|
8902
9306
|
await writeFile(path, contents, { encoding: "utf8", mode: 384 });
|
|
8903
9307
|
return {
|
|
8904
9308
|
path,
|
|
@@ -9004,22 +9408,22 @@ function reportFailure(config, body) {
|
|
|
9004
9408
|
}
|
|
9005
9409
|
|
|
9006
9410
|
// src/prepared.ts
|
|
9007
|
-
import { mkdirSync as mkdirSync7, readFileSync as
|
|
9008
|
-
import { join as
|
|
9411
|
+
import { mkdirSync as mkdirSync7, readFileSync as readFileSync7, rmSync as rmSync5, writeFileSync as writeFileSync5, existsSync as existsSync8 } from "fs";
|
|
9412
|
+
import { join as join12 } from "path";
|
|
9009
9413
|
function dirFor(workspaceId) {
|
|
9010
|
-
return
|
|
9414
|
+
return join12(cabaneDir(), "prepared", encodeURIComponent(workspaceId));
|
|
9011
9415
|
}
|
|
9012
9416
|
function conversationDir(workspaceId, conversationId) {
|
|
9013
|
-
return
|
|
9417
|
+
return join12(dirFor(workspaceId), encodeURIComponent(conversationId));
|
|
9014
9418
|
}
|
|
9015
9419
|
function pathFor2(workspaceId, conversationId, agentId) {
|
|
9016
|
-
return
|
|
9420
|
+
return join12(conversationDir(workspaceId, conversationId), `${encodeURIComponent(agentId)}.json`);
|
|
9017
9421
|
}
|
|
9018
9422
|
function readPrepared(workspaceId, conversationId, agentId) {
|
|
9019
9423
|
const path = pathFor2(workspaceId, conversationId, agentId);
|
|
9020
|
-
if (!
|
|
9424
|
+
if (!existsSync8(path)) return null;
|
|
9021
9425
|
try {
|
|
9022
|
-
const parsed = JSON.parse(
|
|
9426
|
+
const parsed = JSON.parse(readFileSync7(path, "utf8"));
|
|
9023
9427
|
if (parsed && typeof parsed.cwd === "string" && parsed.cwd.length > 0) {
|
|
9024
9428
|
return {
|
|
9025
9429
|
cwd: parsed.cwd,
|
|
@@ -9043,123 +9447,32 @@ function clearPrepared(workspaceId, conversationId, agentId) {
|
|
|
9043
9447
|
rmSync5(pathFor2(workspaceId, conversationId, agentId), { force: true });
|
|
9044
9448
|
}
|
|
9045
9449
|
|
|
9046
|
-
// src/
|
|
9047
|
-
import {
|
|
9048
|
-
import { join as
|
|
9049
|
-
|
|
9050
|
-
|
|
9051
|
-
function secretsPath() {
|
|
9052
|
-
return join12(cabaneDir(), "secrets.json");
|
|
9053
|
-
}
|
|
9054
|
-
var secretStoreSchema = z16.record(z16.string(), z16.string());
|
|
9055
|
-
function loadSecretStore() {
|
|
9056
|
-
const path = secretsPath();
|
|
9057
|
-
if (!existsSync8(path)) return makeStore({});
|
|
9058
|
-
let raw;
|
|
9059
|
-
try {
|
|
9060
|
-
raw = readFileSync7(path, "utf8");
|
|
9061
|
-
} catch (err) {
|
|
9062
|
-
throw new ConfigError(
|
|
9063
|
-
`couldn't read ${path}: ${err instanceof Error ? err.message : String(err)}`
|
|
9064
|
-
);
|
|
9065
|
-
}
|
|
9066
|
-
if (raw.trim().length === 0) return makeStore({});
|
|
9067
|
-
let parsed;
|
|
9068
|
-
try {
|
|
9069
|
-
parsed = JSON.parse(raw);
|
|
9070
|
-
} catch (err) {
|
|
9071
|
-
throw new ConfigError(
|
|
9072
|
-
`${path} is not valid JSON: ${err instanceof Error ? err.message : String(err)}. It must be a flat object of "NAME": "value" secret pairs.`
|
|
9073
|
-
);
|
|
9074
|
-
}
|
|
9075
|
-
const result = secretStoreSchema.safeParse(parsed);
|
|
9076
|
-
if (!result.success) {
|
|
9077
|
-
throw new ConfigError(
|
|
9078
|
-
`${path} must be a flat object mapping secret names to string values (e.g. { "GITHUB_TOKEN": "ghp_\u2026" }).`
|
|
9079
|
-
);
|
|
9080
|
-
}
|
|
9081
|
-
return makeStore(result.data);
|
|
9082
|
-
}
|
|
9083
|
-
function loadSecretStoreTolerant(onWarn) {
|
|
9084
|
-
try {
|
|
9085
|
-
return loadSecretStore();
|
|
9086
|
-
} catch (err) {
|
|
9087
|
-
onWarn?.(err instanceof Error ? err.message : String(err));
|
|
9088
|
-
return makeStore({});
|
|
9089
|
-
}
|
|
9090
|
-
}
|
|
9091
|
-
function makeStore(map) {
|
|
9092
|
-
return {
|
|
9093
|
-
get: (name) => map[name],
|
|
9094
|
-
names: () => Object.keys(map).sort()
|
|
9095
|
-
};
|
|
9450
|
+
// src/transcript-writer.ts
|
|
9451
|
+
import { appendFileSync, chmodSync as chmodSync4, copyFileSync, mkdirSync as mkdirSync8, readdirSync, rmSync as rmSync6 } from "fs";
|
|
9452
|
+
import { basename, dirname as dirname6, join as join13 } from "path";
|
|
9453
|
+
function transcriptsDir() {
|
|
9454
|
+
return join13(runnerStateDir(), "transcripts");
|
|
9096
9455
|
}
|
|
9097
|
-
|
|
9098
|
-
|
|
9099
|
-
|
|
9100
|
-
|
|
9101
|
-
|
|
9102
|
-
|
|
9103
|
-
|
|
9104
|
-
|
|
9105
|
-
|
|
9106
|
-
|
|
9456
|
+
var RETAIN = 200;
|
|
9457
|
+
var ANOMALY_RETAIN = 50;
|
|
9458
|
+
var TranscriptWriter = class {
|
|
9459
|
+
path;
|
|
9460
|
+
broken = false;
|
|
9461
|
+
onWarn;
|
|
9462
|
+
constructor(dir2, meta, onWarn) {
|
|
9463
|
+
this.onWarn = onWarn;
|
|
9464
|
+
this.path = join13(dir2, fileName(meta));
|
|
9465
|
+
try {
|
|
9466
|
+
mkdirSync8(dir2, { recursive: true });
|
|
9467
|
+
try {
|
|
9468
|
+
chmodSync4(dir2, 448);
|
|
9469
|
+
} catch {
|
|
9107
9470
|
}
|
|
9471
|
+
pruneOld(dir2, RETAIN);
|
|
9472
|
+
} catch (err) {
|
|
9473
|
+
this.fail(err);
|
|
9108
9474
|
}
|
|
9109
|
-
|
|
9110
|
-
return [...names].sort();
|
|
9111
|
-
}
|
|
9112
|
-
function resolveMcpSecrets(mcpServers, store) {
|
|
9113
|
-
const missing = /* @__PURE__ */ new Set();
|
|
9114
|
-
const interpolate = (value) => value.replace(PLACEHOLDER_RE, (_match, name) => {
|
|
9115
|
-
const resolved = store.get(name);
|
|
9116
|
-
if (resolved === void 0) {
|
|
9117
|
-
missing.add(name);
|
|
9118
|
-
return _match;
|
|
9119
|
-
}
|
|
9120
|
-
return resolved;
|
|
9121
|
-
});
|
|
9122
|
-
const mapBag = (bag) => {
|
|
9123
|
-
const out = {};
|
|
9124
|
-
for (const [k, v] of Object.entries(bag)) out[k] = interpolate(v);
|
|
9125
|
-
return out;
|
|
9126
|
-
};
|
|
9127
|
-
const servers = {};
|
|
9128
|
-
for (const [name, server] of Object.entries(mcpServers)) {
|
|
9129
|
-
const next = { ...server };
|
|
9130
|
-
if ("env" in next && next.env) next.env = mapBag(next.env);
|
|
9131
|
-
if ("headers" in next && next.headers) next.headers = mapBag(next.headers);
|
|
9132
|
-
servers[name] = next;
|
|
9133
|
-
}
|
|
9134
|
-
return { servers, missing: [...missing].sort() };
|
|
9135
|
-
}
|
|
9136
|
-
|
|
9137
|
-
// src/transcript-writer.ts
|
|
9138
|
-
import { appendFileSync, chmodSync as chmodSync4, copyFileSync, mkdirSync as mkdirSync8, readdirSync, rmSync as rmSync6 } from "fs";
|
|
9139
|
-
import { basename, dirname as dirname6, join as join13 } from "path";
|
|
9140
|
-
function transcriptsDir() {
|
|
9141
|
-
return join13(runnerStateDir(), "transcripts");
|
|
9142
|
-
}
|
|
9143
|
-
var RETAIN = 200;
|
|
9144
|
-
var ANOMALY_RETAIN = 50;
|
|
9145
|
-
var TranscriptWriter = class {
|
|
9146
|
-
path;
|
|
9147
|
-
broken = false;
|
|
9148
|
-
onWarn;
|
|
9149
|
-
constructor(dir2, meta, onWarn) {
|
|
9150
|
-
this.onWarn = onWarn;
|
|
9151
|
-
this.path = join13(dir2, fileName(meta));
|
|
9152
|
-
try {
|
|
9153
|
-
mkdirSync8(dir2, { recursive: true });
|
|
9154
|
-
try {
|
|
9155
|
-
chmodSync4(dir2, 448);
|
|
9156
|
-
} catch {
|
|
9157
|
-
}
|
|
9158
|
-
pruneOld(dir2, RETAIN);
|
|
9159
|
-
} catch (err) {
|
|
9160
|
-
this.fail(err);
|
|
9161
|
-
}
|
|
9162
|
-
this.line({ type: "_meta", ...meta });
|
|
9475
|
+
this.line({ type: "_meta", ...meta });
|
|
9163
9476
|
}
|
|
9164
9477
|
// Append a raw SDK message verbatim.
|
|
9165
9478
|
write(msg) {
|
|
@@ -9437,17 +9750,6 @@ async function absorbBundledLoss(ctx, aborted, outcome) {
|
|
|
9437
9750
|
|
|
9438
9751
|
// src/turn-execution.ts
|
|
9439
9752
|
var PREPARING_TOOL_NAME = "preparing";
|
|
9440
|
-
var ERROR_REASON_MAX = 200;
|
|
9441
|
-
function missingSecretReason(missing) {
|
|
9442
|
-
const prefix = "missing_secret: ";
|
|
9443
|
-
for (let shown = missing.length; shown >= 0; shown--) {
|
|
9444
|
-
const omitted = missing.length - shown;
|
|
9445
|
-
const parts = [...missing.slice(0, shown), ...omitted > 0 ? [`+${omitted}`] : []];
|
|
9446
|
-
const reason = prefix + parts.join(",");
|
|
9447
|
-
if (reason.length <= ERROR_REASON_MAX) return reason;
|
|
9448
|
-
}
|
|
9449
|
-
return `${prefix}+${missing.length}`;
|
|
9450
|
-
}
|
|
9451
9753
|
var RUNTIME_UNAVAILABLE_PREFIX = "**This agent's runtime isn't available on this companion.** The model this agent uses needs a runtime this device isn't running, so I can't run this turn here. Details:";
|
|
9452
9754
|
var NO_TURN_TOKEN_REASON = "no_turn_token: the server issued no turn credential for this turn";
|
|
9453
9755
|
var UNEXPECTED_ROLE_REASON = `refused the wake trigger (role "system" isn't dispatchable on this companion) \u2014 the companion is likely running outdated code; refresh it, then re-address the agent`;
|
|
@@ -9741,6 +10043,12 @@ var TurnExecution = class {
|
|
|
9741
10043
|
this.resumedSpoke = turnContext.turnSpoke === true || outboxSpoke;
|
|
9742
10044
|
if (this.resumedSpoke) turnLog.debug({ turnId }, "dispatcher: resumed span already spoke");
|
|
9743
10045
|
}
|
|
10046
|
+
try {
|
|
10047
|
+
await this.opts.inRunner?.onContext?.(turnContext.attemptId ?? null);
|
|
10048
|
+
} catch (err) {
|
|
10049
|
+
const reason = `runner_unavailable: ${err instanceof Error ? err.message : String(err)}`;
|
|
10050
|
+
throw this.concluded(reason, reason);
|
|
10051
|
+
}
|
|
9744
10052
|
}
|
|
9745
10053
|
gateTrigger() {
|
|
9746
10054
|
const { payload, turnLog } = this;
|
|
@@ -9758,7 +10066,8 @@ var TurnExecution = class {
|
|
|
9758
10066
|
}
|
|
9759
10067
|
async resolveSecrets() {
|
|
9760
10068
|
const { payload, workspaceId, turnLog } = this;
|
|
9761
|
-
const
|
|
10069
|
+
const fromLauncher = this.opts.inRunner?.secrets;
|
|
10070
|
+
const secretStore = this.opts.secretStore ?? (fromLauncher ? await fromLauncher(requiredSecretNames(this.turnContext.mcpServers)) : null) ?? loadSecretStoreTolerant(
|
|
9762
10071
|
(m) => turnLog.warn({ err: m }, "Could not read the secrets file. Check ~/.cabane/secrets.json.")
|
|
9763
10072
|
);
|
|
9764
10073
|
const { servers: resolvedMcpServers, missing } = resolveMcpSecrets(
|
|
@@ -9918,42 +10227,48 @@ var TurnExecution = class {
|
|
|
9918
10227
|
const { payload, workspaceId, turnId, turnLog } = this;
|
|
9919
10228
|
const abortController = this.abortController;
|
|
9920
10229
|
this.supervisor.registerAbort(turnId, abortController);
|
|
10230
|
+
let started;
|
|
9921
10231
|
try {
|
|
9922
|
-
await this.opts.api.setActiveRun(
|
|
9923
|
-
|
|
9924
|
-
|
|
9925
|
-
|
|
9926
|
-
|
|
9927
|
-
|
|
9928
|
-
|
|
9929
|
-
|
|
9930
|
-
|
|
9931
|
-
|
|
9932
|
-
|
|
9933
|
-
|
|
9934
|
-
|
|
9935
|
-
|
|
9936
|
-
|
|
9937
|
-
|
|
9938
|
-
|
|
9939
|
-
|
|
9940
|
-
|
|
9941
|
-
|
|
9942
|
-
|
|
9943
|
-
|
|
9944
|
-
|
|
9945
|
-
|
|
9946
|
-
|
|
9947
|
-
|
|
9948
|
-
|
|
9949
|
-
|
|
9950
|
-
|
|
9951
|
-
|
|
9952
|
-
|
|
9953
|
-
|
|
9954
|
-
|
|
9955
|
-
|
|
9956
|
-
|
|
10232
|
+
started = await this.opts.api.setActiveRun(
|
|
10233
|
+
workspaceId,
|
|
10234
|
+
payload.conversationId,
|
|
10235
|
+
payload.agentId,
|
|
10236
|
+
{
|
|
10237
|
+
activeRunStartedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
10238
|
+
// CT1679: a runner leases under its launcher's boot id — the one the
|
|
10239
|
+
// device heartbeats — or the lost-run sweep reads it as a restart.
|
|
10240
|
+
bootId: this.opts.inRunner?.bootId ?? bootId,
|
|
10241
|
+
// CT1680: the runner holding the span, beside the device's boot id.
|
|
10242
|
+
...this.opts.inRunner ? { runnerId: this.opts.inRunner.runnerId } : {},
|
|
10243
|
+
// CT1380: the durable record that this turn resumed, and from where.
|
|
10244
|
+
...this.resumedFromSeq !== void 0 ? { resumedFromSeq: this.resumedFromSeq } : {},
|
|
10245
|
+
// CT1454: the issuance the context THIS attempt runs on came from. Read
|
|
10246
|
+
// off `this.turnContext`, which is the fetch that composed this request —
|
|
10247
|
+
// so a retransmitted start echoes the same id and reuses the same record,
|
|
10248
|
+
// and a resumed attempt echoes its own, newer one. The post-admission
|
|
10249
|
+
// fresh-session recompose (`freshRequest`) deliberately does not touch it:
|
|
10250
|
+
// it adopts only the prompt, never the run-config, so its issuance must
|
|
10251
|
+
// not become this attempt's evidence.
|
|
10252
|
+
...this.turnContext.issuanceId ? { issuanceId: this.turnContext.issuanceId } : {},
|
|
10253
|
+
// CT33: hand the server this turn's id so the new-run chokepoint's
|
|
10254
|
+
// `closeAbandonedTurns` sweep excludes it. The prepare hook may have
|
|
10255
|
+
// already emitted a "preparing" activity row for this turn above (it
|
|
10256
|
+
// runs before this flip), which would otherwise make the sweep mistake
|
|
10257
|
+
// this live turn for an abandoned one and close it with a `stopped`.
|
|
10258
|
+
turnId,
|
|
10259
|
+
// The newest message this turn's read covered: the lease records it,
|
|
10260
|
+
// and settle moves the cursor to it. Omitted when the server sent
|
|
10261
|
+
// none (an older API), so the cursor stays where it was.
|
|
10262
|
+
...this.turnContext.readThrough !== void 0 ? { readThrough: this.turnContext.readThrough } : {},
|
|
10263
|
+
...this.turnContext.readRevision !== void 0 ? { readRevision: this.turnContext.readRevision } : {},
|
|
10264
|
+
// Rooms: the attempt this request's session belongs to (null = fresh).
|
|
10265
|
+
...this.turnContext.attemptId !== void 0 ? { attemptId: this.turnContext.attemptId } : {},
|
|
10266
|
+
// CT1543: the frame this start acts on. The turn-context fetch already
|
|
10267
|
+
// checked it, but that was before an assembly this turn awaited — the
|
|
10268
|
+
// lease checks it again where the stamp is actually taken.
|
|
10269
|
+
...payload.episode !== void 0 ? { dispatchMessageId: payload.messageId, dispatchEpisode: payload.episode } : {}
|
|
10270
|
+
}
|
|
10271
|
+
);
|
|
9957
10272
|
} catch (err) {
|
|
9958
10273
|
if (leaseRefusal(err) === "dispatch_context_stale" && !this.recomposedForLease) {
|
|
9959
10274
|
this.recomposedForLease = true;
|
|
@@ -9978,6 +10293,7 @@ var TurnExecution = class {
|
|
|
9978
10293
|
throw this.concluded(reason, reason);
|
|
9979
10294
|
}
|
|
9980
10295
|
this.admitted = true;
|
|
10296
|
+
if (started?.attemptId) this.opts.inRunner?.onLeased?.(started.attemptId);
|
|
9981
10297
|
}
|
|
9982
10298
|
buildRequest() {
|
|
9983
10299
|
const { payload, workspaceId } = this;
|
|
@@ -10022,7 +10338,18 @@ var TurnExecution = class {
|
|
|
10022
10338
|
const { payload, workspaceId, turnId, turnLog } = this;
|
|
10023
10339
|
await guardBundledBinary(this.integrityContext(), (r, e) => this.concluded(r, e), this.outcome);
|
|
10024
10340
|
const onWarn = (msg, meta) => turnLog.debug(meta ?? {}, msg);
|
|
10025
|
-
|
|
10341
|
+
let sandbox = null;
|
|
10342
|
+
try {
|
|
10343
|
+
sandbox = this.opts.harnessSandbox ? await this.opts.harnessSandbox({
|
|
10344
|
+
cwd: this.effectiveCwd,
|
|
10345
|
+
runtime: this.turnContext.runtime,
|
|
10346
|
+
log: turnLog
|
|
10347
|
+
}) : null;
|
|
10348
|
+
} catch (err) {
|
|
10349
|
+
if (!(err instanceof HarnessSandboxRefused)) throw err;
|
|
10350
|
+
const reason = `setup_failed: ${err.message}`.slice(0, 200);
|
|
10351
|
+
throw this.concluded(reason, reason);
|
|
10352
|
+
}
|
|
10026
10353
|
const adapters = [];
|
|
10027
10354
|
if (this.opts.claudeCodeAvailable?.() ?? true) {
|
|
10028
10355
|
this.containment = createTurnContainment(this.turnId, {
|
|
@@ -10741,6 +11068,1254 @@ function createLocalProcessProvider(opts = {}) {
|
|
|
10741
11068
|
};
|
|
10742
11069
|
}
|
|
10743
11070
|
|
|
11071
|
+
// src/providers/docker.ts
|
|
11072
|
+
import { createHash as createHash4, randomUUID as randomUUID5 } from "crypto";
|
|
11073
|
+
import { constants as constants2, tmpdir as tmpdir2 } from "os";
|
|
11074
|
+
import {
|
|
11075
|
+
createReadStream,
|
|
11076
|
+
createWriteStream as createWriteStream2,
|
|
11077
|
+
existsSync as existsSync13,
|
|
11078
|
+
mkdirSync as mkdirSync10,
|
|
11079
|
+
readdirSync as readdirSync4,
|
|
11080
|
+
readFileSync as readFileSync9,
|
|
11081
|
+
renameSync as renameSync5,
|
|
11082
|
+
rmSync as rmSync9,
|
|
11083
|
+
statSync as statSync3,
|
|
11084
|
+
writeFileSync as writeFileSync6
|
|
11085
|
+
} from "fs";
|
|
11086
|
+
import { join as join17 } from "path";
|
|
11087
|
+
import { Writable } from "stream";
|
|
11088
|
+
|
|
11089
|
+
// src/providers/docker-cli.ts
|
|
11090
|
+
import { spawn as spawn7 } from "child_process";
|
|
11091
|
+
function cliEnv(extra) {
|
|
11092
|
+
const keep = [
|
|
11093
|
+
"PATH",
|
|
11094
|
+
"HOME",
|
|
11095
|
+
"DOCKER_HOST",
|
|
11096
|
+
"DOCKER_CONFIG",
|
|
11097
|
+
"DOCKER_CONTEXT",
|
|
11098
|
+
"DOCKER_CERT_PATH"
|
|
11099
|
+
];
|
|
11100
|
+
const env = {};
|
|
11101
|
+
for (const k of keep) if (process.env[k] !== void 0) env[k] = process.env[k];
|
|
11102
|
+
return { ...env, ...extra };
|
|
11103
|
+
}
|
|
11104
|
+
function dockerCli(bin = "docker") {
|
|
11105
|
+
return {
|
|
11106
|
+
run(args, opts = {}) {
|
|
11107
|
+
return new Promise((resolve2) => {
|
|
11108
|
+
let child;
|
|
11109
|
+
try {
|
|
11110
|
+
child = spawn7(bin, [...args], {
|
|
11111
|
+
env: cliEnv(opts.env),
|
|
11112
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
11113
|
+
});
|
|
11114
|
+
} catch (err) {
|
|
11115
|
+
resolve2({ code: 127, stdout: "", stderr: String(err) });
|
|
11116
|
+
return;
|
|
11117
|
+
}
|
|
11118
|
+
let stdout = "";
|
|
11119
|
+
let stderr = "";
|
|
11120
|
+
const timer = opts.timeoutMs ? setTimeout(() => child.kill("SIGKILL"), opts.timeoutMs) : void 0;
|
|
11121
|
+
if (opts.stdout) child.stdout?.pipe(opts.stdout);
|
|
11122
|
+
else child.stdout?.on("data", (chunk) => stdout += String(chunk));
|
|
11123
|
+
child.stderr?.on("data", (chunk) => stderr += String(chunk));
|
|
11124
|
+
child.stdin?.on("error", () => {
|
|
11125
|
+
});
|
|
11126
|
+
child.once("error", (err) => {
|
|
11127
|
+
if (timer) clearTimeout(timer);
|
|
11128
|
+
resolve2({ code: 127, stdout, stderr: stderr || err.message });
|
|
11129
|
+
});
|
|
11130
|
+
child.once("close", (code, signal) => {
|
|
11131
|
+
if (timer) clearTimeout(timer);
|
|
11132
|
+
resolve2({ code: code ?? (signal ? 137 : 1), stdout, stderr });
|
|
11133
|
+
});
|
|
11134
|
+
const input = opts.input;
|
|
11135
|
+
if (input === void 0) child.stdin?.end();
|
|
11136
|
+
else if (typeof input === "string" || Buffer.isBuffer(input)) child.stdin?.end(input);
|
|
11137
|
+
else input.pipe(child.stdin);
|
|
11138
|
+
});
|
|
11139
|
+
},
|
|
11140
|
+
spawn(args, env) {
|
|
11141
|
+
return spawn7(bin, [...args], { env: cliEnv(env), stdio: ["pipe", "pipe", "pipe"] });
|
|
11142
|
+
}
|
|
11143
|
+
};
|
|
11144
|
+
}
|
|
11145
|
+
|
|
11146
|
+
// src/runner/git-credential.ts
|
|
11147
|
+
import { spawn as spawn8 } from "child_process";
|
|
11148
|
+
import { chmodSync as chmodSync5, rmSync as rmSync7 } from "fs";
|
|
11149
|
+
import { connect as connect3, createServer as createServer3 } from "net";
|
|
11150
|
+
|
|
11151
|
+
// src/runner/egress-proxy.ts
|
|
11152
|
+
import { createServer as createServer2, request as httpRequest } from "http";
|
|
11153
|
+
import { connect as connect2 } from "net";
|
|
11154
|
+
function egressAllowed(host, allow, port, defaultPort = 443) {
|
|
11155
|
+
const h = host.toLowerCase().replace(/\.$/, "");
|
|
11156
|
+
if (!h) return false;
|
|
11157
|
+
const p = port ?? defaultPort;
|
|
11158
|
+
return allow.some((entry) => {
|
|
11159
|
+
const raw = entry.toLowerCase().trim();
|
|
11160
|
+
const m = /^(.*):(\d{1,5})$/.exec(raw);
|
|
11161
|
+
const name = m ? m[1] : raw;
|
|
11162
|
+
const wanted = m ? Number(m[2]) : defaultPort;
|
|
11163
|
+
if (p !== wanted) return false;
|
|
11164
|
+
if (name.startsWith("*.")) return h.endsWith(name.slice(1));
|
|
11165
|
+
return h === name;
|
|
11166
|
+
});
|
|
11167
|
+
}
|
|
11168
|
+
|
|
11169
|
+
// src/runner/git-credential.ts
|
|
11170
|
+
function parseCredential(text) {
|
|
11171
|
+
const out = {};
|
|
11172
|
+
for (const line of text.split("\n")) {
|
|
11173
|
+
const eq = line.indexOf("=");
|
|
11174
|
+
if (eq <= 0) continue;
|
|
11175
|
+
out[line.slice(0, eq)] = line.slice(eq + 1);
|
|
11176
|
+
}
|
|
11177
|
+
return out;
|
|
11178
|
+
}
|
|
11179
|
+
function formatCredential(fields) {
|
|
11180
|
+
return Object.entries(fields).map(([k, v]) => `${k}=${v}`).join("\n") + "\n\n";
|
|
11181
|
+
}
|
|
11182
|
+
function deviceGitFill(helper, timeoutMs = 15e3) {
|
|
11183
|
+
return (request) => new Promise((resolve2) => {
|
|
11184
|
+
const args = [
|
|
11185
|
+
...helper ? ["-c", "credential.helper=", "-c", `credential.helper=${helper}`] : [],
|
|
11186
|
+
"credential",
|
|
11187
|
+
"fill"
|
|
11188
|
+
];
|
|
11189
|
+
const child = spawn8("git", args, {
|
|
11190
|
+
stdio: ["pipe", "pipe", "ignore"],
|
|
11191
|
+
env: { ...process.env, GIT_TERMINAL_PROMPT: "0", GIT_ASKPASS: "", SSH_ASKPASS: "" }
|
|
11192
|
+
});
|
|
11193
|
+
let out = "";
|
|
11194
|
+
const timer = setTimeout(() => child.kill("SIGKILL"), timeoutMs);
|
|
11195
|
+
child.stdout.on("data", (chunk) => out += String(chunk));
|
|
11196
|
+
child.once("error", () => resolve2(null));
|
|
11197
|
+
child.once("close", (code) => {
|
|
11198
|
+
clearTimeout(timer);
|
|
11199
|
+
resolve2(code === 0 ? out : null);
|
|
11200
|
+
});
|
|
11201
|
+
child.stdin.end(formatCredential(request));
|
|
11202
|
+
});
|
|
11203
|
+
}
|
|
11204
|
+
async function serveGitCredentials(opts) {
|
|
11205
|
+
const { socketPath, hosts, fill, log } = opts;
|
|
11206
|
+
rmSync7(socketPath, { force: true });
|
|
11207
|
+
const server = createServer3({ allowHalfOpen: true }, (socket) => {
|
|
11208
|
+
let input = "";
|
|
11209
|
+
socket.setEncoding("utf8");
|
|
11210
|
+
socket.on("data", (chunk) => input += chunk);
|
|
11211
|
+
socket.on("error", () => socket.destroy());
|
|
11212
|
+
socket.on("end", () => {
|
|
11213
|
+
void (async () => {
|
|
11214
|
+
const [op = "", ...rest] = input.split("\n");
|
|
11215
|
+
const request = parseCredential(rest.join("\n"));
|
|
11216
|
+
if (op !== "get" || request.protocol !== "https" || !request.host) {
|
|
11217
|
+
socket.end();
|
|
11218
|
+
return;
|
|
11219
|
+
}
|
|
11220
|
+
if (!egressAllowed(request.host.replace(/:\d+$/, ""), hosts)) {
|
|
11221
|
+
log?.warn(
|
|
11222
|
+
{ host: request.host },
|
|
11223
|
+
"runner: git asked for a credential for a host it may not use"
|
|
11224
|
+
);
|
|
11225
|
+
socket.end();
|
|
11226
|
+
return;
|
|
11227
|
+
}
|
|
11228
|
+
const answer = await fill({
|
|
11229
|
+
protocol: "https",
|
|
11230
|
+
host: request.host,
|
|
11231
|
+
...request.path ? { path: request.path } : {},
|
|
11232
|
+
...request.username ? { username: request.username } : {}
|
|
11233
|
+
});
|
|
11234
|
+
if (!answer) {
|
|
11235
|
+
log?.warn(
|
|
11236
|
+
{ host: request.host },
|
|
11237
|
+
"runner: this device's git has no credential for the host"
|
|
11238
|
+
);
|
|
11239
|
+
socket.end();
|
|
11240
|
+
return;
|
|
11241
|
+
}
|
|
11242
|
+
const filled = parseCredential(answer);
|
|
11243
|
+
socket.end(
|
|
11244
|
+
formatCredential({
|
|
11245
|
+
protocol: "https",
|
|
11246
|
+
host: request.host,
|
|
11247
|
+
...filled.username ? { username: filled.username } : {},
|
|
11248
|
+
...filled.password ? { password: filled.password } : {}
|
|
11249
|
+
})
|
|
11250
|
+
);
|
|
11251
|
+
})();
|
|
11252
|
+
});
|
|
11253
|
+
});
|
|
11254
|
+
await new Promise((resolve2, reject) => {
|
|
11255
|
+
server.once("error", reject);
|
|
11256
|
+
server.listen(socketPath, () => {
|
|
11257
|
+
server.off("error", reject);
|
|
11258
|
+
resolve2();
|
|
11259
|
+
});
|
|
11260
|
+
});
|
|
11261
|
+
chmodSync5(socketPath, 438);
|
|
11262
|
+
return {
|
|
11263
|
+
close: () => new Promise((resolve2) => {
|
|
11264
|
+
server.close(() => resolve2());
|
|
11265
|
+
rmSync7(socketPath, { force: true });
|
|
11266
|
+
})
|
|
11267
|
+
};
|
|
11268
|
+
}
|
|
11269
|
+
|
|
11270
|
+
// src/runner/collected-outbox.ts
|
|
11271
|
+
import {
|
|
11272
|
+
copyFileSync as copyFileSync2,
|
|
11273
|
+
existsSync as existsSync11,
|
|
11274
|
+
mkdirSync as mkdirSync9,
|
|
11275
|
+
readdirSync as readdirSync3,
|
|
11276
|
+
readFileSync as readFileSync8,
|
|
11277
|
+
renameSync as renameSync4,
|
|
11278
|
+
rmSync as rmSync8
|
|
11279
|
+
} from "fs";
|
|
11280
|
+
import { join as join15 } from "path";
|
|
11281
|
+
function routes(t) {
|
|
11282
|
+
const conv = `/api/workspaces/${t.workspaceId}/conversations/${t.conversationId}`;
|
|
11283
|
+
const agent = `${conv}/participants/agents/${t.agentId}`;
|
|
11284
|
+
return {
|
|
11285
|
+
message: { method: "POST", path: `${conv}/messages` },
|
|
11286
|
+
activity: { method: "POST", path: `${agent}/activity` },
|
|
11287
|
+
thinking: { method: "POST", path: `${agent}/thinking` },
|
|
11288
|
+
text: { method: "POST", path: `${agent}/text` },
|
|
11289
|
+
"active-run": { method: "PATCH", path: `${agent}/active-run` }
|
|
11290
|
+
};
|
|
11291
|
+
}
|
|
11292
|
+
function admitCollectedEntry(raw, turn) {
|
|
11293
|
+
if (!raw || typeof raw !== "object") return null;
|
|
11294
|
+
const entry = raw;
|
|
11295
|
+
const kind = entry.kind;
|
|
11296
|
+
if (!kind || !(kind in routes(turn))) return null;
|
|
11297
|
+
const route = routes(turn)[kind];
|
|
11298
|
+
if (entry.method !== route.method || entry.path !== route.path) return null;
|
|
11299
|
+
if (typeof entry.seq !== "number" || typeof entry.enqueuedAt !== "number") return null;
|
|
11300
|
+
const body = entry.body;
|
|
11301
|
+
if (!body || typeof body !== "object" || typeof body.turnId !== "string") return null;
|
|
11302
|
+
if (!turn.turnIds.includes(body.turnId)) return null;
|
|
11303
|
+
if (kind === "active-run") {
|
|
11304
|
+
if (body.activeRunStartedAt !== null) return null;
|
|
11305
|
+
if (entry.turnId !== `activerun:${turn.conversationId}:${turn.agentId}`) return null;
|
|
11306
|
+
} else if (entry.turnId !== body.turnId) {
|
|
11307
|
+
return null;
|
|
11308
|
+
}
|
|
11309
|
+
return entry;
|
|
11310
|
+
}
|
|
11311
|
+
var atomicMove = (from, to) => {
|
|
11312
|
+
try {
|
|
11313
|
+
renameSync4(from, to);
|
|
11314
|
+
} catch {
|
|
11315
|
+
const partial = `${to}.partial`;
|
|
11316
|
+
copyFileSync2(from, partial);
|
|
11317
|
+
renameSync4(partial, to);
|
|
11318
|
+
rmSync8(from, { force: true });
|
|
11319
|
+
}
|
|
11320
|
+
};
|
|
11321
|
+
function publishStaged(from, to, move = atomicMove) {
|
|
11322
|
+
if (!existsSync11(from)) return { published: 0, complete: true };
|
|
11323
|
+
const staged = readdirSync3(from).filter((n) => n.endsWith(".json")).map((name) => {
|
|
11324
|
+
let entry = {};
|
|
11325
|
+
try {
|
|
11326
|
+
entry = JSON.parse(readFileSync8(join15(from, name), "utf8"));
|
|
11327
|
+
} catch {
|
|
11328
|
+
}
|
|
11329
|
+
return { name, entry };
|
|
11330
|
+
});
|
|
11331
|
+
const settle = (e) => e.kind === "active-run" ? 1 : 0;
|
|
11332
|
+
staged.sort(
|
|
11333
|
+
(a, b) => settle(a.entry) - settle(b.entry) || (a.entry.enqueuedAt ?? 0) - (b.entry.enqueuedAt ?? 0) || String(a.entry.turnId).localeCompare(String(b.entry.turnId)) || (a.entry.seq ?? 0) - (b.entry.seq ?? 0)
|
|
11334
|
+
);
|
|
11335
|
+
if (staged.length) mkdirSync9(to, { recursive: true });
|
|
11336
|
+
let published = 0;
|
|
11337
|
+
for (const { name } of staged) {
|
|
11338
|
+
try {
|
|
11339
|
+
move(join15(from, name), join15(to, name));
|
|
11340
|
+
} catch {
|
|
11341
|
+
return { published, complete: false };
|
|
11342
|
+
}
|
|
11343
|
+
published++;
|
|
11344
|
+
}
|
|
11345
|
+
rmSync8(from, { recursive: true, force: true });
|
|
11346
|
+
return { published, complete: true };
|
|
11347
|
+
}
|
|
11348
|
+
|
|
11349
|
+
// src/providers/runner-base.ts
|
|
11350
|
+
import { createHash as createHash3 } from "crypto";
|
|
11351
|
+
import { existsSync as existsSync12, realpathSync as realpathSync2 } from "fs";
|
|
11352
|
+
import { dirname as dirname7, join as join16, sep } from "path";
|
|
11353
|
+
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
11354
|
+
var COMPANION_MOUNT = "/opt/cabane/companion";
|
|
11355
|
+
var BASE_DOCKERFILE = `FROM node:22-slim
|
|
11356
|
+
RUN apt-get update \\
|
|
11357
|
+
&& apt-get install -y --no-install-recommends git curl ca-certificates procps \\
|
|
11358
|
+
&& rm -rf /var/lib/apt/lists/* \\
|
|
11359
|
+
&& corepack enable
|
|
11360
|
+
RUN usermod -l runner -d /home/runner -m node \\
|
|
11361
|
+
&& groupmod -n runner node \\
|
|
11362
|
+
&& mkdir -p /work /var/lib/cabane-runner /run/cabane ${COMPANION_MOUNT} \\
|
|
11363
|
+
&& chown runner:runner /work /var/lib/cabane-runner
|
|
11364
|
+
RUN mkdir -p /home/runner/.codex /opt/cabane \\
|
|
11365
|
+
&& printf '[features]\\nuse_legacy_landlock = true\\n' > /opt/cabane/codex-config.toml \\
|
|
11366
|
+
&& cp /opt/cabane/codex-config.toml /home/runner/.codex/config.toml \\
|
|
11367
|
+
&& chown -R runner:runner /home/runner/.codex
|
|
11368
|
+
RUN printf '#!/bin/sh\\nexec node "\${CABANE_COMPANION_ROOT:-${COMPANION_MOUNT}}/dist/cli.js" "$@"\\n' > /usr/local/bin/cabane-companion \\
|
|
11369
|
+
&& chmod 755 /usr/local/bin/cabane-companion
|
|
11370
|
+
USER runner
|
|
11371
|
+
WORKDIR /home/runner
|
|
11372
|
+
CMD ["sleep", "infinity"]
|
|
11373
|
+
`;
|
|
11374
|
+
function baseImageTag(dockerfile = BASE_DOCKERFILE) {
|
|
11375
|
+
return `cabane-runner-base:${createHash3("sha256").update(dockerfile).digest("hex").slice(0, 12)}`;
|
|
11376
|
+
}
|
|
11377
|
+
async function ensureBaseImage(docker, image, log) {
|
|
11378
|
+
if ((await docker.run(["image", "inspect", image])).code === 0) return true;
|
|
11379
|
+
if (image !== baseImageTag()) {
|
|
11380
|
+
log.error({ image }, "runners: the configured runner image isn't on this engine");
|
|
11381
|
+
return false;
|
|
11382
|
+
}
|
|
11383
|
+
log.info({ image }, "runners: building the runner base image (once per base)");
|
|
11384
|
+
const built = await docker.run(["build", "-t", image, "-"], {
|
|
11385
|
+
input: BASE_DOCKERFILE,
|
|
11386
|
+
timeoutMs: 20 * 6e4
|
|
11387
|
+
});
|
|
11388
|
+
if (built.code !== 0) {
|
|
11389
|
+
log.error(
|
|
11390
|
+
{ image, err: built.stderr.trim().slice(-2e3) },
|
|
11391
|
+
"runners: couldn't build the runner base image; no turn can run until it is"
|
|
11392
|
+
);
|
|
11393
|
+
return false;
|
|
11394
|
+
}
|
|
11395
|
+
return true;
|
|
11396
|
+
}
|
|
11397
|
+
function ownInstall(moduleFile = fileURLToPath3(import.meta.url)) {
|
|
11398
|
+
let dir2 = dirname7(moduleFile);
|
|
11399
|
+
for (let i = 0; i < 4; i++) {
|
|
11400
|
+
if (existsSync12(join16(dir2, "dist", "cli.js")) && existsSync12(join16(dir2, "package.json"))) break;
|
|
11401
|
+
dir2 = dirname7(dir2);
|
|
11402
|
+
}
|
|
11403
|
+
if (!existsSync12(join16(dir2, "dist", "cli.js"))) return null;
|
|
11404
|
+
const sdk = join16(dir2, "node_modules", "@anthropic-ai", "claude-agent-sdk");
|
|
11405
|
+
if (!existsSync12(sdk)) return null;
|
|
11406
|
+
const root = realpathSync2(dir2);
|
|
11407
|
+
return realpathSync2(sdk).startsWith(root + sep) ? root : null;
|
|
11408
|
+
}
|
|
11409
|
+
function companionSource(input) {
|
|
11410
|
+
if (input.configured) {
|
|
11411
|
+
return { kind: "dir", path: input.configured, key: `dir:${input.configured}` };
|
|
11412
|
+
}
|
|
11413
|
+
if (input.platform === "linux") {
|
|
11414
|
+
if (input.own)
|
|
11415
|
+
return { kind: "dir", path: input.own, key: `dir:${input.own}:${input.version}` };
|
|
11416
|
+
return {
|
|
11417
|
+
error: "this companion runs from a checkout, which a container can't mount on its own; build one with `pnpm --filter @cabane/companion deploy --prod <dir>` and set runners.docker.companionDir"
|
|
11418
|
+
};
|
|
11419
|
+
}
|
|
11420
|
+
if (input.version === "0.0.0") {
|
|
11421
|
+
return {
|
|
11422
|
+
error: "an unreleased companion can't be installed into a container from npm; set runners.docker.companionDir to a Linux install of it"
|
|
11423
|
+
};
|
|
11424
|
+
}
|
|
11425
|
+
return {
|
|
11426
|
+
kind: "volume",
|
|
11427
|
+
volume: `cabane-companion-${input.version}`,
|
|
11428
|
+
version: input.version,
|
|
11429
|
+
key: `volume:${input.version}`
|
|
11430
|
+
};
|
|
11431
|
+
}
|
|
11432
|
+
function companionRoot(source) {
|
|
11433
|
+
return source.kind === "dir" ? COMPANION_MOUNT : `${COMPANION_MOUNT}/lib/node_modules/@cabane/companion`;
|
|
11434
|
+
}
|
|
11435
|
+
function companionMountArgs(source) {
|
|
11436
|
+
const mount = source.kind === "dir" ? `type=bind,src=${source.path},dst=${COMPANION_MOUNT},readonly` : `type=volume,src=${source.volume},dst=${COMPANION_MOUNT},readonly`;
|
|
11437
|
+
return ["--mount", mount, "-e", `CABANE_COMPANION_ROOT=${companionRoot(source)}`];
|
|
11438
|
+
}
|
|
11439
|
+
var INSTALL_MARKER = `${COMPANION_MOUNT}/.cabane-install-complete`;
|
|
11440
|
+
function volumeInstallScript(version, install = `npm install -g --prefix ${COMPANION_MOUNT} @cabane/companion@${version}`) {
|
|
11441
|
+
const root = `${COMPANION_MOUNT}/lib/node_modules/@cabane/companion`;
|
|
11442
|
+
return [
|
|
11443
|
+
`find ${COMPANION_MOUNT} -mindepth 1 -delete`,
|
|
11444
|
+
install,
|
|
11445
|
+
// Installed is not the same as runnable: the package must start and say
|
|
11446
|
+
// what it runs before the volume counts as done.
|
|
11447
|
+
`node ${root}/dist/cli.js run-turn --manifest > /dev/null`,
|
|
11448
|
+
`printf %s ${version} > ${INSTALL_MARKER}`
|
|
11449
|
+
].join(" && ");
|
|
11450
|
+
}
|
|
11451
|
+
var volumeMount = (source) => `type=volume,src=${source.volume},dst=${COMPANION_MOUNT}`;
|
|
11452
|
+
async function ensureCompanionVolume(docker, image, source, log, script = volumeInstallScript) {
|
|
11453
|
+
if (source.kind !== "volume") return true;
|
|
11454
|
+
const done = await docker.run([
|
|
11455
|
+
"run",
|
|
11456
|
+
"--rm",
|
|
11457
|
+
"--network",
|
|
11458
|
+
"none",
|
|
11459
|
+
"--mount",
|
|
11460
|
+
volumeMount(source),
|
|
11461
|
+
image,
|
|
11462
|
+
"sh",
|
|
11463
|
+
"-c",
|
|
11464
|
+
`[ "$(cat ${INSTALL_MARKER} 2>/dev/null)" = "$1" ]`,
|
|
11465
|
+
"sh",
|
|
11466
|
+
source.version
|
|
11467
|
+
]);
|
|
11468
|
+
if (done.code === 0) return true;
|
|
11469
|
+
log.info(
|
|
11470
|
+
{ version: source.version },
|
|
11471
|
+
"runners: installing this companion version for its containers"
|
|
11472
|
+
);
|
|
11473
|
+
const installed = await docker.run(
|
|
11474
|
+
[
|
|
11475
|
+
"run",
|
|
11476
|
+
"--rm",
|
|
11477
|
+
"-u",
|
|
11478
|
+
"root",
|
|
11479
|
+
"--mount",
|
|
11480
|
+
volumeMount(source),
|
|
11481
|
+
image,
|
|
11482
|
+
"sh",
|
|
11483
|
+
"-c",
|
|
11484
|
+
script(source.version)
|
|
11485
|
+
],
|
|
11486
|
+
{ timeoutMs: 20 * 6e4 }
|
|
11487
|
+
);
|
|
11488
|
+
if (installed.code !== 0) {
|
|
11489
|
+
log.error(
|
|
11490
|
+
{ version: source.version, err: installed.stderr.trim().slice(-2e3) },
|
|
11491
|
+
"runners: couldn't install the companion for its containers; the next start tries again"
|
|
11492
|
+
);
|
|
11493
|
+
return false;
|
|
11494
|
+
}
|
|
11495
|
+
return true;
|
|
11496
|
+
}
|
|
11497
|
+
async function invalidateCompanionVolume(docker, image, source) {
|
|
11498
|
+
if (source.kind !== "volume") return;
|
|
11499
|
+
await docker.run([
|
|
11500
|
+
"run",
|
|
11501
|
+
"--rm",
|
|
11502
|
+
"-u",
|
|
11503
|
+
"root",
|
|
11504
|
+
"--network",
|
|
11505
|
+
"none",
|
|
11506
|
+
"--mount",
|
|
11507
|
+
volumeMount(source),
|
|
11508
|
+
image,
|
|
11509
|
+
"rm",
|
|
11510
|
+
"-f",
|
|
11511
|
+
INSTALL_MARKER
|
|
11512
|
+
]);
|
|
11513
|
+
}
|
|
11514
|
+
|
|
11515
|
+
// src/providers/docker.ts
|
|
11516
|
+
var CONTAINER_HOME = "/home/runner";
|
|
11517
|
+
var CONTAINER_WORK = "/work";
|
|
11518
|
+
var CONTAINER_STATE = "/var/lib/cabane-runner";
|
|
11519
|
+
var CONTAINER_SOCKETS = "/run/cabane";
|
|
11520
|
+
var PROXY_ALIAS = "cabane-egress";
|
|
11521
|
+
var PROXY_PORT = 3128;
|
|
11522
|
+
var DEFAULT_KILL_GRACE_MS2 = 3e4;
|
|
11523
|
+
var REAP_GRACE_S = 3;
|
|
11524
|
+
var DEFAULT_SESSION_CAP_BYTES = 512 * 1024 * 1024;
|
|
11525
|
+
var COLLECT_CAP_BYTES = 128 * 1024 * 1024;
|
|
11526
|
+
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
|
+
function containerName(attempt) {
|
|
11528
|
+
return `cabane-runner-${attempt.conversationId}-${attempt.agentId}`;
|
|
11529
|
+
}
|
|
11530
|
+
function sessionArchivePath(deviceDir, attemptId) {
|
|
11531
|
+
if (!UUID_RE.test(attemptId)) throw new Error(`docker: not an attempt id: ${attemptId}`);
|
|
11532
|
+
return join17(deviceDir, "attempts", attemptId, "session.tar");
|
|
11533
|
+
}
|
|
11534
|
+
function pruneOtherAttempts(deviceDir, pair, keep) {
|
|
11535
|
+
const root = join17(deviceDir, "attempts");
|
|
11536
|
+
if (!existsSync13(root)) return;
|
|
11537
|
+
for (const id of readdirSync4(root)) {
|
|
11538
|
+
if (id === keep || !UUID_RE.test(id)) continue;
|
|
11539
|
+
let pairOf = "";
|
|
11540
|
+
try {
|
|
11541
|
+
pairOf = readFileSync9(join17(root, id, "pair"), "utf8");
|
|
11542
|
+
} catch {
|
|
11543
|
+
continue;
|
|
11544
|
+
}
|
|
11545
|
+
if (pairOf === `${pair.conversationId}/${pair.agentId}`) {
|
|
11546
|
+
rmSync9(join17(root, id), { recursive: true, force: true });
|
|
11547
|
+
}
|
|
11548
|
+
}
|
|
11549
|
+
}
|
|
11550
|
+
var CappedFile = class extends Writable {
|
|
11551
|
+
constructor(path, cap) {
|
|
11552
|
+
super();
|
|
11553
|
+
this.path = path;
|
|
11554
|
+
this.cap = cap;
|
|
11555
|
+
this.out = createWriteStream2(path);
|
|
11556
|
+
}
|
|
11557
|
+
path;
|
|
11558
|
+
cap;
|
|
11559
|
+
bytes = 0;
|
|
11560
|
+
over = false;
|
|
11561
|
+
out;
|
|
11562
|
+
_write(chunk, _enc, done) {
|
|
11563
|
+
this.bytes += chunk.length;
|
|
11564
|
+
if (this.bytes > this.cap) {
|
|
11565
|
+
this.over = true;
|
|
11566
|
+
done();
|
|
11567
|
+
return;
|
|
11568
|
+
}
|
|
11569
|
+
this.out.write(chunk, done);
|
|
11570
|
+
}
|
|
11571
|
+
_final(done) {
|
|
11572
|
+
this.out.end(done);
|
|
11573
|
+
}
|
|
11574
|
+
};
|
|
11575
|
+
var DUMP_SCRIPT = `
|
|
11576
|
+
const fs = require('fs'), path = require('path');
|
|
11577
|
+
const dir = process.argv[1], cap = Number(process.argv[2]);
|
|
11578
|
+
let names;
|
|
11579
|
+
try { names = fs.readdirSync(dir); }
|
|
11580
|
+
catch (e) {
|
|
11581
|
+
if (e.code === 'ENOENT') { process.stdout.write(JSON.stringify({ complete: true, files: [] })); process.exit(0); }
|
|
11582
|
+
process.stderr.write(String(e.message)); process.exit(2);
|
|
11583
|
+
}
|
|
11584
|
+
let total = 0, complete = true; const files = [];
|
|
11585
|
+
for (const name of names) {
|
|
11586
|
+
const p = path.join(dir, name);
|
|
11587
|
+
let st;
|
|
11588
|
+
try { st = fs.lstatSync(p); } catch (e) { process.stderr.write(String(e.message)); process.exit(2); }
|
|
11589
|
+
if (!st.isFile()) continue;
|
|
11590
|
+
// Always at least one file per read, so one over the cap is still collected.
|
|
11591
|
+
if (files.length && total + st.size > cap) { complete = false; continue; }
|
|
11592
|
+
let data;
|
|
11593
|
+
try { data = fs.readFileSync(p); } catch (e) { process.stderr.write(String(e.message)); process.exit(2); }
|
|
11594
|
+
total += st.size;
|
|
11595
|
+
files.push({ name, data: data.toString('base64') });
|
|
11596
|
+
}
|
|
11597
|
+
process.stdout.write(JSON.stringify({ complete, files }));
|
|
11598
|
+
`;
|
|
11599
|
+
var SAFE_NAME_RE = /^[\w.%@+-]+$/;
|
|
11600
|
+
function createDockerProvider(opts) {
|
|
11601
|
+
const docker = opts.docker ?? dockerCli();
|
|
11602
|
+
const log = opts.log;
|
|
11603
|
+
const killGraceMs = opts.killGraceMs ?? DEFAULT_KILL_GRACE_MS2;
|
|
11604
|
+
const sessionCap = opts.sessionCapBytes ?? DEFAULT_SESSION_CAP_BYTES;
|
|
11605
|
+
const collectCap = opts.collectCapBytes ?? COLLECT_CAP_BYTES;
|
|
11606
|
+
const network = `cabane-runners-${opts.deviceKey}`;
|
|
11607
|
+
const companionLabel = createHash4("sha256").update(opts.companion.key).digest("hex").slice(0, 16);
|
|
11608
|
+
const proxy = `cabane-egress-${opts.deviceKey}`;
|
|
11609
|
+
const held = /* @__PURE__ */ new Map();
|
|
11610
|
+
const queues = /* @__PURE__ */ new Map();
|
|
11611
|
+
const must = async (args, what) => {
|
|
11612
|
+
const r = await docker.run(args);
|
|
11613
|
+
if (r.code !== 0)
|
|
11614
|
+
throw new Error(`docker: ${what} failed: ${r.stderr.trim() || `exit ${r.code}`}`);
|
|
11615
|
+
return r.stdout;
|
|
11616
|
+
};
|
|
11617
|
+
const execIn = (name, args, extra = {}) => docker.run(
|
|
11618
|
+
["exec", ...extra.input !== void 0 ? ["-i"] : [], "-u", "runner", name, ...args],
|
|
11619
|
+
extra
|
|
11620
|
+
);
|
|
11621
|
+
const proxyListening = async () => {
|
|
11622
|
+
for (let i = 0; i < 40; i++) {
|
|
11623
|
+
const r = await docker.run([
|
|
11624
|
+
"exec",
|
|
11625
|
+
proxy,
|
|
11626
|
+
"curl",
|
|
11627
|
+
"-s",
|
|
11628
|
+
"-o",
|
|
11629
|
+
"/dev/null",
|
|
11630
|
+
"--max-time",
|
|
11631
|
+
"1",
|
|
11632
|
+
`http://127.0.0.1:${PROXY_PORT}/`
|
|
11633
|
+
]);
|
|
11634
|
+
if (r.code === 0) return;
|
|
11635
|
+
await new Promise((resolve2) => setTimeout(resolve2, 250));
|
|
11636
|
+
}
|
|
11637
|
+
throw new Error("docker: the egress proxy never started listening");
|
|
11638
|
+
};
|
|
11639
|
+
let networkReady = null;
|
|
11640
|
+
const proxyKey = async () => {
|
|
11641
|
+
const id = (await docker.run(["image", "inspect", "-f", "{{.Id}}", opts.image])).stdout.trim();
|
|
11642
|
+
return createHash4("sha256").update(JSON.stringify([id || opts.image, opts.companion.key, [...opts.egressAllow].sort()])).digest("hex").slice(0, 16);
|
|
11643
|
+
};
|
|
11644
|
+
const ensureNetwork = () => {
|
|
11645
|
+
networkReady ??= (async () => {
|
|
11646
|
+
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
|
+
const seen = await docker.run([
|
|
11666
|
+
"inspect",
|
|
11667
|
+
"-f",
|
|
11668
|
+
'{{index .Config.Labels "cabane.allow"}} {{.State.Running}}',
|
|
11669
|
+
proxy
|
|
11670
|
+
]);
|
|
11671
|
+
const [key, running] = seen.stdout.trim().split(" ");
|
|
11672
|
+
if (seen.code === 0 && key === allowKey) {
|
|
11673
|
+
if (running !== "true") {
|
|
11674
|
+
await must(["start", proxy], "starting the egress proxy");
|
|
11675
|
+
await proxyListening();
|
|
11676
|
+
}
|
|
11677
|
+
return;
|
|
11678
|
+
}
|
|
11679
|
+
if (seen.code === 0) await docker.run(["rm", "-f", proxy]);
|
|
11680
|
+
await must(
|
|
11681
|
+
[
|
|
11682
|
+
"run",
|
|
11683
|
+
"-d",
|
|
11684
|
+
"--name",
|
|
11685
|
+
proxy,
|
|
11686
|
+
"--label",
|
|
11687
|
+
`cabane.allow=${allowKey}`,
|
|
11688
|
+
"--label",
|
|
11689
|
+
`cabane.device=${opts.deviceKey}`,
|
|
11690
|
+
"--restart",
|
|
11691
|
+
"unless-stopped",
|
|
11692
|
+
"--cap-drop",
|
|
11693
|
+
"ALL",
|
|
11694
|
+
"--security-opt",
|
|
11695
|
+
"no-new-privileges",
|
|
11696
|
+
"-e",
|
|
11697
|
+
`CABANE_EGRESS_ALLOW=${JSON.stringify(opts.egressAllow)}`,
|
|
11698
|
+
...companionMountArgs(opts.companion),
|
|
11699
|
+
opts.image,
|
|
11700
|
+
"cabane-companion",
|
|
11701
|
+
"egress-proxy",
|
|
11702
|
+
"--port",
|
|
11703
|
+
String(PROXY_PORT)
|
|
11704
|
+
],
|
|
11705
|
+
"starting the egress proxy"
|
|
11706
|
+
);
|
|
11707
|
+
await must(
|
|
11708
|
+
["network", "connect", "--alias", PROXY_ALIAS, network, proxy],
|
|
11709
|
+
"connecting the proxy"
|
|
11710
|
+
);
|
|
11711
|
+
await proxyListening();
|
|
11712
|
+
log.info({ network, proxy, allow: opts.egressAllow }, "runners: egress proxy ready");
|
|
11713
|
+
})().catch((err) => {
|
|
11714
|
+
networkReady = null;
|
|
11715
|
+
throw err;
|
|
11716
|
+
});
|
|
11717
|
+
return networkReady;
|
|
11718
|
+
};
|
|
11719
|
+
const socketRoot = opts.socketRoot ?? join17(
|
|
11720
|
+
process.env.XDG_RUNTIME_DIR && existsSync13(process.env.XDG_RUNTIME_DIR) ? process.env.XDG_RUNTIME_DIR : tmpdir2(),
|
|
11721
|
+
`cabane-runners-${opts.deviceKey}`
|
|
11722
|
+
);
|
|
11723
|
+
const socketDir = (name) => join17(socketRoot, createHash4("sha256").update(name).digest("hex").slice(0, 12));
|
|
11724
|
+
const create = async (name, attempt) => {
|
|
11725
|
+
mkdirSync10(socketRoot, { recursive: true, mode: 448 });
|
|
11726
|
+
mkdirSync10(socketDir(name), { recursive: true, mode: 457 });
|
|
11727
|
+
const mounts = (opts.mounts ?? []).filter((m) => m.source.startsWith("/") && existsSync13(m.source)).flatMap((m) => ["--mount", `type=bind,src=${m.source},dst=${m.target},readonly`]);
|
|
11728
|
+
await must(
|
|
11729
|
+
[
|
|
11730
|
+
"create",
|
|
11731
|
+
"--name",
|
|
11732
|
+
name,
|
|
11733
|
+
"--init",
|
|
11734
|
+
"--hostname",
|
|
11735
|
+
"cabane-runner",
|
|
11736
|
+
"--label",
|
|
11737
|
+
"cabane.runner=1",
|
|
11738
|
+
"--label",
|
|
11739
|
+
`cabane.device=${opts.deviceKey}`,
|
|
11740
|
+
"--label",
|
|
11741
|
+
`cabane.workspace=${attempt.workspaceId}`,
|
|
11742
|
+
"--label",
|
|
11743
|
+
`cabane.conversation=${attempt.conversationId}`,
|
|
11744
|
+
"--label",
|
|
11745
|
+
`cabane.agent=${attempt.agentId}`,
|
|
11746
|
+
"--label",
|
|
11747
|
+
`cabane.companion=${companionLabel}`,
|
|
11748
|
+
"--network",
|
|
11749
|
+
network,
|
|
11750
|
+
"--mount",
|
|
11751
|
+
`type=volume,src=${name}-home,dst=${CONTAINER_HOME}`,
|
|
11752
|
+
"--mount",
|
|
11753
|
+
`type=volume,src=${name}-work,dst=${CONTAINER_WORK}`,
|
|
11754
|
+
"--mount",
|
|
11755
|
+
`type=bind,src=${socketDir(name)},dst=${CONTAINER_SOCKETS},readonly`,
|
|
11756
|
+
...mounts,
|
|
11757
|
+
...companionMountArgs(opts.companion),
|
|
11758
|
+
"--cap-drop",
|
|
11759
|
+
"ALL",
|
|
11760
|
+
"--security-opt",
|
|
11761
|
+
"no-new-privileges",
|
|
11762
|
+
"--pids-limit",
|
|
11763
|
+
"4096",
|
|
11764
|
+
opts.image,
|
|
11765
|
+
"sleep",
|
|
11766
|
+
"infinity"
|
|
11767
|
+
],
|
|
11768
|
+
"creating the container"
|
|
11769
|
+
);
|
|
11770
|
+
};
|
|
11771
|
+
const destroy = async (name) => {
|
|
11772
|
+
await docker.run(["rm", "-f", "-v", name]);
|
|
11773
|
+
await docker.run(["volume", "rm", "-f", `${name}-home`, `${name}-work`]);
|
|
11774
|
+
rmSync9(socketDir(name), { recursive: true, force: true });
|
|
11775
|
+
};
|
|
11776
|
+
const containerEnv = () => {
|
|
11777
|
+
const proxyUrl = `http://${PROXY_ALIAS}:${PROXY_PORT}`;
|
|
11778
|
+
const git = [
|
|
11779
|
+
["credential.helper", ""],
|
|
11780
|
+
// The helper speaks a socket, not the network: no proxy, and none of
|
|
11781
|
+
// Node's warning about it in git's output.
|
|
11782
|
+
["credential.helper", "!NODE_USE_ENV_PROXY= cabane-companion git-credential"],
|
|
11783
|
+
...opts.gitHosts.flatMap((h) => [
|
|
11784
|
+
[`url.https://${h}/.insteadOf`, `git@${h}:`],
|
|
11785
|
+
[`url.https://${h}/.insteadOf`, `ssh://git@${h}/`]
|
|
11786
|
+
])
|
|
11787
|
+
];
|
|
11788
|
+
return {
|
|
11789
|
+
HOME: CONTAINER_HOME,
|
|
11790
|
+
CABANE_COMPANION_HOME: CONTAINER_STATE,
|
|
11791
|
+
CABANE_RUNNER_STATE_DIR: CONTAINER_STATE,
|
|
11792
|
+
HTTPS_PROXY: proxyUrl,
|
|
11793
|
+
HTTP_PROXY: proxyUrl,
|
|
11794
|
+
https_proxy: proxyUrl,
|
|
11795
|
+
http_proxy: proxyUrl,
|
|
11796
|
+
NO_PROXY: "localhost,127.0.0.1,::1",
|
|
11797
|
+
no_proxy: "localhost,127.0.0.1,::1",
|
|
11798
|
+
// Node's own fetch — the runner's wire — honours the proxy only when asked.
|
|
11799
|
+
NODE_USE_ENV_PROXY: "1",
|
|
11800
|
+
CABANE_GIT_CREDENTIAL_SOCKET: `${CONTAINER_SOCKETS}/git.sock`,
|
|
11801
|
+
GIT_TERMINAL_PROMPT: "0",
|
|
11802
|
+
GIT_CONFIG_COUNT: String(git.length),
|
|
11803
|
+
...Object.fromEntries(
|
|
11804
|
+
git.flatMap(([k, v], i) => [
|
|
11805
|
+
[`GIT_CONFIG_KEY_${i}`, k],
|
|
11806
|
+
[`GIT_CONFIG_VALUE_${i}`, v]
|
|
11807
|
+
])
|
|
11808
|
+
)
|
|
11809
|
+
};
|
|
11810
|
+
};
|
|
11811
|
+
const heldFor = (handle) => {
|
|
11812
|
+
const h = held.get(handle.id);
|
|
11813
|
+
if (!h) throw new Error(`docker: ${handle.id} is not held`);
|
|
11814
|
+
return h;
|
|
11815
|
+
};
|
|
11816
|
+
const writeMarker = (name, attemptId) => execIn(name, [
|
|
11817
|
+
"sh",
|
|
11818
|
+
"-c",
|
|
11819
|
+
`mkdir -p ${CONTAINER_STATE} && printf %s "$1" > ${CONTAINER_STATE}/attempt`,
|
|
11820
|
+
"sh",
|
|
11821
|
+
attemptId
|
|
11822
|
+
]);
|
|
11823
|
+
const readDir = async (name, from) => {
|
|
11824
|
+
const r = await execIn(name, [
|
|
11825
|
+
"sh",
|
|
11826
|
+
"-c",
|
|
11827
|
+
'[ -e "$1" ] && chmod -R u+rwX "$1"; exec node -e "$2" "$1" "$3"',
|
|
11828
|
+
"sh",
|
|
11829
|
+
from,
|
|
11830
|
+
DUMP_SCRIPT,
|
|
11831
|
+
String(collectCap)
|
|
11832
|
+
]);
|
|
11833
|
+
if (r.code !== 0) {
|
|
11834
|
+
log.warn(
|
|
11835
|
+
{ container: name, dir: from, err: r.stderr.trim() },
|
|
11836
|
+
"runners: couldn't read what the runner left"
|
|
11837
|
+
);
|
|
11838
|
+
return null;
|
|
11839
|
+
}
|
|
11840
|
+
try {
|
|
11841
|
+
const dump = JSON.parse(r.stdout);
|
|
11842
|
+
if (typeof dump.complete !== "boolean" || !Array.isArray(dump.files)) return null;
|
|
11843
|
+
return {
|
|
11844
|
+
complete: dump.complete,
|
|
11845
|
+
files: dump.files.filter(
|
|
11846
|
+
(f) => typeof f.name === "string" && typeof f.data === "string"
|
|
11847
|
+
).map((f) => ({ name: f.name, data: Buffer.from(f.data, "base64") }))
|
|
11848
|
+
};
|
|
11849
|
+
} catch {
|
|
11850
|
+
return null;
|
|
11851
|
+
}
|
|
11852
|
+
};
|
|
11853
|
+
const removeIn = (name, from, names) => names.length ? execIn(name, ["sh", "-c", 'cd "$1" && shift && rm -f -- "$@"', "sh", from, ...names]) : Promise.resolve(null);
|
|
11854
|
+
const pendingPath = (name) => join17(opts.deviceDir, "runners-pending", `${name}.json`);
|
|
11855
|
+
const readPending = (name) => {
|
|
11856
|
+
try {
|
|
11857
|
+
const ids = JSON.parse(readFileSync9(pendingPath(name), "utf8"));
|
|
11858
|
+
return Array.isArray(ids) ? ids.filter((id) => typeof id === "string") : [];
|
|
11859
|
+
} catch {
|
|
11860
|
+
return [];
|
|
11861
|
+
}
|
|
11862
|
+
};
|
|
11863
|
+
const writePending = (name, ids) => {
|
|
11864
|
+
if (!ids.length) {
|
|
11865
|
+
rmSync9(pendingPath(name), { force: true });
|
|
11866
|
+
return;
|
|
11867
|
+
}
|
|
11868
|
+
mkdirSync10(join17(opts.deviceDir, "runners-pending"), { recursive: true });
|
|
11869
|
+
writeFileSync6(pendingPath(name), JSON.stringify([...new Set(ids)]));
|
|
11870
|
+
};
|
|
11871
|
+
const keepSession = async (name, attempt, attemptId) => {
|
|
11872
|
+
const archive = sessionArchivePath(opts.deviceDir, attemptId);
|
|
11873
|
+
mkdirSync10(join17(archive, ".."), { recursive: true });
|
|
11874
|
+
const partial = `${archive}.${process.pid}.partial`;
|
|
11875
|
+
const file = new CappedFile(partial, sessionCap);
|
|
11876
|
+
const r = await execIn(
|
|
11877
|
+
name,
|
|
11878
|
+
[
|
|
11879
|
+
"sh",
|
|
11880
|
+
"-c",
|
|
11881
|
+
'cd "$HOME" && set -- && for p in .claude/projects .codex; do [ -e "$p" ] && set -- "$@" "$p"; done; [ $# -gt 0 ] || exit 3; exec tar -cf - --exclude=.codex/auth.json "$@"'
|
|
11882
|
+
],
|
|
11883
|
+
{ stdout: file }
|
|
11884
|
+
);
|
|
11885
|
+
await new Promise(
|
|
11886
|
+
(resolve2) => file.writableFinished ? resolve2() : file.once("finish", resolve2)
|
|
11887
|
+
);
|
|
11888
|
+
if (r.code === 0 && !file.over) {
|
|
11889
|
+
renameSync5(partial, archive);
|
|
11890
|
+
writeFileSync6(join17(archive, "..", "pair"), `${attempt.conversationId}/${attempt.agentId}`);
|
|
11891
|
+
pruneOtherAttempts(opts.deviceDir, attempt, attemptId);
|
|
11892
|
+
await writeMarker(name, attemptId);
|
|
11893
|
+
log.debug({ container: name, attemptId, bytes: file.bytes }, "runners: session files kept");
|
|
11894
|
+
return;
|
|
11895
|
+
}
|
|
11896
|
+
rmSync9(partial, { force: true });
|
|
11897
|
+
if (r.code === 3 && !file.over) return;
|
|
11898
|
+
dropArchive(attemptId);
|
|
11899
|
+
log.warn(
|
|
11900
|
+
file.over ? { container: name, attemptId, capBytes: sessionCap } : { container: name, attemptId, err: r.stderr.trim() },
|
|
11901
|
+
file.over ? "runners: the session files are over the size cap and weren't kept; the next new container starts the session over" : "runners: couldn't keep the session files; the next new container starts the session over"
|
|
11902
|
+
);
|
|
11903
|
+
};
|
|
11904
|
+
const stagingDir = (name) => join17(opts.deviceDir, "runners-pending", `${name}.outbox`);
|
|
11905
|
+
const ensureRunning = async (name) => {
|
|
11906
|
+
const seen = await docker.run(["inspect", "-f", "{{.State.Running}}", name]);
|
|
11907
|
+
if (seen.code !== 0) return false;
|
|
11908
|
+
if (seen.stdout.trim() === "true") return true;
|
|
11909
|
+
return (await docker.run(["start", name])).code === 0;
|
|
11910
|
+
};
|
|
11911
|
+
const dropArchive = (attemptId) => {
|
|
11912
|
+
rmSync9(join17(sessionArchivePath(opts.deviceDir, attemptId), ".."), {
|
|
11913
|
+
recursive: true,
|
|
11914
|
+
force: true
|
|
11915
|
+
});
|
|
11916
|
+
};
|
|
11917
|
+
const RETIRE_SCRIPT = [
|
|
11918
|
+
`chmod -R u+rwX /work ${CONTAINER_HOME}`,
|
|
11919
|
+
"find /work -mindepth 1 -delete",
|
|
11920
|
+
`find ${CONTAINER_HOME} -mindepth 1 -delete`,
|
|
11921
|
+
`cp -a /etc/skel/. ${CONTAINER_HOME}/`,
|
|
11922
|
+
`mkdir -p ${CONTAINER_HOME}/.codex`,
|
|
11923
|
+
`cp /opt/cabane/codex-config.toml ${CONTAINER_HOME}/.codex/config.toml`,
|
|
11924
|
+
`rm -rf ${CONTAINER_STATE}/prepared`
|
|
11925
|
+
].join(" && ");
|
|
11926
|
+
const provider = {
|
|
11927
|
+
name: "docker",
|
|
11928
|
+
inheritsEnvironment: false,
|
|
11929
|
+
async acquire(attempt) {
|
|
11930
|
+
const name = containerName(attempt);
|
|
11931
|
+
const before = queues.get(name) ?? Promise.resolve();
|
|
11932
|
+
let release;
|
|
11933
|
+
const mine = new Promise((resolve2) => release = resolve2);
|
|
11934
|
+
const tail = before.then(() => mine);
|
|
11935
|
+
queues.set(name, tail);
|
|
11936
|
+
void tail.then(() => {
|
|
11937
|
+
if (queues.get(name) === tail) queues.delete(name);
|
|
11938
|
+
});
|
|
11939
|
+
await before;
|
|
11940
|
+
try {
|
|
11941
|
+
await ensureNetwork();
|
|
11942
|
+
mkdirSync10(socketRoot, { recursive: true, mode: 448 });
|
|
11943
|
+
mkdirSync10(socketDir(name), { recursive: true, mode: 457 });
|
|
11944
|
+
let seen = await docker.run([
|
|
11945
|
+
"inspect",
|
|
11946
|
+
"-f",
|
|
11947
|
+
'{{.State.Running}} {{index .Config.Labels "cabane.companion"}}',
|
|
11948
|
+
name
|
|
11949
|
+
]);
|
|
11950
|
+
if (seen.code === 0 && seen.stdout.trim().split(" ")[1] !== companionLabel) {
|
|
11951
|
+
held.set(name, { attempt, release: () => {
|
|
11952
|
+
}, collected: false });
|
|
11953
|
+
let collected = false;
|
|
11954
|
+
try {
|
|
11955
|
+
await provider.collect({ provider: "docker", id: name }, { attemptId: null });
|
|
11956
|
+
collected = held.get(name)?.collected === true;
|
|
11957
|
+
} finally {
|
|
11958
|
+
held.delete(name);
|
|
11959
|
+
}
|
|
11960
|
+
if (!collected)
|
|
11961
|
+
throw new Error(
|
|
11962
|
+
"docker: this conversation's container runs another companion and what it holds couldn't be collected"
|
|
11963
|
+
);
|
|
11964
|
+
await destroy(name);
|
|
11965
|
+
log.info(
|
|
11966
|
+
{ container: name },
|
|
11967
|
+
"runners: replaced a container made with another companion"
|
|
11968
|
+
);
|
|
11969
|
+
seen = { code: 1, stdout: "", stderr: "" };
|
|
11970
|
+
}
|
|
11971
|
+
if (seen.code !== 0) {
|
|
11972
|
+
await create(name, attempt);
|
|
11973
|
+
log.debug({ container: name }, "runners: container created");
|
|
11974
|
+
}
|
|
11975
|
+
if (seen.stdout.trim().split(" ")[0] !== "true")
|
|
11976
|
+
await must(["start", name], "starting the container");
|
|
11977
|
+
const git = await serveGitCredentials({
|
|
11978
|
+
socketPath: join17(socketDir(name), "git.sock"),
|
|
11979
|
+
hosts: opts.gitHosts,
|
|
11980
|
+
fill: opts.gitFill,
|
|
11981
|
+
log
|
|
11982
|
+
});
|
|
11983
|
+
held.set(name, { attempt, release, git, collected: false });
|
|
11984
|
+
return { provider: "docker", id: name };
|
|
11985
|
+
} catch (err) {
|
|
11986
|
+
const keep = opts.keepContainers || readPending(name).length > 0;
|
|
11987
|
+
await (keep ? docker.run(["stop", "-t", "1", name]) : destroy(name)).catch(() => {
|
|
11988
|
+
});
|
|
11989
|
+
release();
|
|
11990
|
+
throw err;
|
|
11991
|
+
}
|
|
11992
|
+
},
|
|
11993
|
+
async bindAttempt(handle, attemptId) {
|
|
11994
|
+
const name = handle.id;
|
|
11995
|
+
const marker = await execIn(name, ["cat", `${CONTAINER_STATE}/attempt`]);
|
|
11996
|
+
const bound = marker.code === 0 ? marker.stdout.trim() : "";
|
|
11997
|
+
if (bound === (attemptId ?? "")) return;
|
|
11998
|
+
const { attempt } = heldFor(handle);
|
|
11999
|
+
pruneOtherAttempts(opts.deviceDir, attempt, attemptId ?? "");
|
|
12000
|
+
if (bound) {
|
|
12001
|
+
const wiped = await execIn(name, ["sh", "-c", RETIRE_SCRIPT]);
|
|
12002
|
+
if (wiped.code !== 0)
|
|
12003
|
+
throw new Error(`docker: couldn't clear the retired attempt: ${wiped.stderr.trim()}`);
|
|
12004
|
+
log.info(
|
|
12005
|
+
{ container: name, retired: bound, attemptId },
|
|
12006
|
+
"runners: retired attempt cleared"
|
|
12007
|
+
);
|
|
12008
|
+
}
|
|
12009
|
+
if (attemptId && UUID_RE.test(attemptId)) {
|
|
12010
|
+
const archive = sessionArchivePath(opts.deviceDir, attemptId);
|
|
12011
|
+
if (existsSync13(archive)) {
|
|
12012
|
+
const r = await execIn(name, ["tar", "-x", "-C", CONTAINER_HOME, "-f", "-"], {
|
|
12013
|
+
input: createReadStream(archive)
|
|
12014
|
+
});
|
|
12015
|
+
if (r.code !== 0) {
|
|
12016
|
+
log.warn(
|
|
12017
|
+
{ container: name, attemptId, err: r.stderr.trim() },
|
|
12018
|
+
"runners: couldn't restore the session files; the runtime will start its session over"
|
|
12019
|
+
);
|
|
12020
|
+
} else {
|
|
12021
|
+
log.info(
|
|
12022
|
+
{ container: name, attemptId, bytes: statSync3(archive).size },
|
|
12023
|
+
"runners: session files restored"
|
|
12024
|
+
);
|
|
12025
|
+
}
|
|
12026
|
+
} else {
|
|
12027
|
+
log.info(
|
|
12028
|
+
{ container: name, attemptId },
|
|
12029
|
+
"runners: no session files kept for this attempt on this device"
|
|
12030
|
+
);
|
|
12031
|
+
}
|
|
12032
|
+
}
|
|
12033
|
+
await writeMarker(name, attemptId ?? "");
|
|
12034
|
+
},
|
|
12035
|
+
exec(handle, argv, env, { signal, stdin }) {
|
|
12036
|
+
const name = handle.id;
|
|
12037
|
+
const pidFile = `/tmp/cabane-runner-${randomUUID5()}.pid`;
|
|
12038
|
+
const passed = { ...env, ...opts.runtimeEnv?.() ?? {} };
|
|
12039
|
+
const literal = containerEnv();
|
|
12040
|
+
const args = [
|
|
12041
|
+
"exec",
|
|
12042
|
+
"-i",
|
|
12043
|
+
"-u",
|
|
12044
|
+
"runner",
|
|
12045
|
+
"-w",
|
|
12046
|
+
CONTAINER_HOME,
|
|
12047
|
+
...Object.entries(literal).flatMap(([k, v]) => ["-e", `${k}=${v}`]),
|
|
12048
|
+
...Object.keys(passed).flatMap((k) => ["-e", k]),
|
|
12049
|
+
name,
|
|
12050
|
+
"sh",
|
|
12051
|
+
"-c",
|
|
12052
|
+
'echo $$ > "$0" && exec "$@"',
|
|
12053
|
+
pidFile,
|
|
12054
|
+
...argv
|
|
12055
|
+
];
|
|
12056
|
+
const child = docker.spawn(args, passed);
|
|
12057
|
+
child.stdin?.on("error", () => {
|
|
12058
|
+
});
|
|
12059
|
+
if (stdin) child.stdin?.write(stdin);
|
|
12060
|
+
let forceTimer;
|
|
12061
|
+
const stop = () => {
|
|
12062
|
+
void execIn(name, ["sh", "-c", 'kill -TERM "$(cat "$1")"', "sh", pidFile]);
|
|
12063
|
+
forceTimer = setTimeout(() => void docker.run(["kill", name]), killGraceMs);
|
|
12064
|
+
forceTimer.unref?.();
|
|
12065
|
+
};
|
|
12066
|
+
const exit = new Promise((resolve2) => {
|
|
12067
|
+
let settled = false;
|
|
12068
|
+
const done = (code) => {
|
|
12069
|
+
if (settled) return;
|
|
12070
|
+
settled = true;
|
|
12071
|
+
signal.removeEventListener("abort", stop);
|
|
12072
|
+
if (forceTimer) clearTimeout(forceTimer);
|
|
12073
|
+
resolve2(code);
|
|
12074
|
+
};
|
|
12075
|
+
child.once("error", () => done(1));
|
|
12076
|
+
child.once("close", (code, sig) => {
|
|
12077
|
+
const signum = sig ? constants2.signals[sig] : void 0;
|
|
12078
|
+
done(code ?? (signum !== void 0 ? 128 + signum : 1));
|
|
12079
|
+
});
|
|
12080
|
+
if (signal.aborted) stop();
|
|
12081
|
+
else signal.addEventListener("abort", stop, { once: true });
|
|
12082
|
+
});
|
|
12083
|
+
if (!child.stdout || !child.stderr || !child.stdin)
|
|
12084
|
+
throw new Error("docker: runner stdio missing");
|
|
12085
|
+
return {
|
|
12086
|
+
exit,
|
|
12087
|
+
...child.pid !== void 0 ? { pid: child.pid } : {},
|
|
12088
|
+
stdout: child.stdout,
|
|
12089
|
+
stderr: child.stderr,
|
|
12090
|
+
stdin: child.stdin,
|
|
12091
|
+
// The runner is the only thing this user runs in the container, so
|
|
12092
|
+
// everything it left is everything the user has left.
|
|
12093
|
+
async reap() {
|
|
12094
|
+
await execIn(name, [
|
|
12095
|
+
"sh",
|
|
12096
|
+
"-c",
|
|
12097
|
+
`kill -TERM -1 2>/dev/null; sleep ${REAP_GRACE_S}; kill -KILL -1 2>/dev/null; true`
|
|
12098
|
+
]);
|
|
12099
|
+
}
|
|
12100
|
+
};
|
|
12101
|
+
},
|
|
12102
|
+
async collect(handle, { attemptId }) {
|
|
12103
|
+
const name = handle.id;
|
|
12104
|
+
const held2 = heldFor(handle);
|
|
12105
|
+
const { attempt } = held2;
|
|
12106
|
+
const turnIds = [.../* @__PURE__ */ new Set([attempt.turnId, ...readPending(name)])];
|
|
12107
|
+
const running = await ensureRunning(name);
|
|
12108
|
+
if (attemptId && UUID_RE.test(attemptId)) {
|
|
12109
|
+
if (!running) {
|
|
12110
|
+
dropArchive(attemptId);
|
|
12111
|
+
log.warn(
|
|
12112
|
+
{ container: name, attemptId },
|
|
12113
|
+
"runners: couldn't reach the container to keep the session files; the next turn starts the session over"
|
|
12114
|
+
);
|
|
12115
|
+
} else {
|
|
12116
|
+
await keepSession(name, attempt, attemptId);
|
|
12117
|
+
}
|
|
12118
|
+
}
|
|
12119
|
+
if (!running) {
|
|
12120
|
+
writePending(name, turnIds);
|
|
12121
|
+
return;
|
|
12122
|
+
}
|
|
12123
|
+
const transcripts = await readDir(name, `${CONTAINER_STATE}/transcripts`);
|
|
12124
|
+
if (transcripts) {
|
|
12125
|
+
const kept = transcripts.files.filter((f) => SAFE_NAME_RE.test(f.name));
|
|
12126
|
+
if (kept.length) mkdirSync10(join17(opts.hostStateDir, "transcripts"), { recursive: true });
|
|
12127
|
+
for (const f of kept) writeFileSync6(join17(opts.hostStateDir, "transcripts", f.name), f.data);
|
|
12128
|
+
await removeIn(
|
|
12129
|
+
name,
|
|
12130
|
+
`${CONTAINER_STATE}/transcripts`,
|
|
12131
|
+
kept.map((f) => f.name)
|
|
12132
|
+
);
|
|
12133
|
+
}
|
|
12134
|
+
const outboxRel = join17("outbox", encodeURIComponent(attempt.agentId));
|
|
12135
|
+
const outbox = await readDir(name, `${CONTAINER_STATE}/${outboxRel}`);
|
|
12136
|
+
let unpublished = false;
|
|
12137
|
+
if (outbox) {
|
|
12138
|
+
const scope = {
|
|
12139
|
+
workspaceId: attempt.workspaceId,
|
|
12140
|
+
conversationId: attempt.conversationId,
|
|
12141
|
+
agentId: attempt.agentId,
|
|
12142
|
+
turnIds
|
|
12143
|
+
};
|
|
12144
|
+
let queued = 0;
|
|
12145
|
+
let refused = 0;
|
|
12146
|
+
for (const f of outbox.files) {
|
|
12147
|
+
let entry = null;
|
|
12148
|
+
try {
|
|
12149
|
+
entry = admitCollectedEntry(JSON.parse(f.data.toString("utf8")), scope);
|
|
12150
|
+
} catch {
|
|
12151
|
+
entry = null;
|
|
12152
|
+
}
|
|
12153
|
+
if (!entry) {
|
|
12154
|
+
refused++;
|
|
12155
|
+
continue;
|
|
12156
|
+
}
|
|
12157
|
+
const staged = stagingDir(name);
|
|
12158
|
+
mkdirSync10(staged, { recursive: true });
|
|
12159
|
+
writeFileSync6(
|
|
12160
|
+
join17(staged, `${encodeURIComponent(entry.turnId)}__${entry.seq}.json`),
|
|
12161
|
+
`${JSON.stringify(entry)}
|
|
12162
|
+
`
|
|
12163
|
+
);
|
|
12164
|
+
}
|
|
12165
|
+
await removeIn(
|
|
12166
|
+
name,
|
|
12167
|
+
`${CONTAINER_STATE}/${outboxRel}`,
|
|
12168
|
+
outbox.files.map((f) => f.name)
|
|
12169
|
+
);
|
|
12170
|
+
if (outbox.complete) {
|
|
12171
|
+
const published = publishStaged(
|
|
12172
|
+
stagingDir(name),
|
|
12173
|
+
join17(opts.hostStateDir, outboxRel),
|
|
12174
|
+
opts.moveFile
|
|
12175
|
+
);
|
|
12176
|
+
queued = published.published;
|
|
12177
|
+
if (!published.complete) {
|
|
12178
|
+
unpublished = true;
|
|
12179
|
+
log.warn(
|
|
12180
|
+
{ container: name },
|
|
12181
|
+
"runners: couldn't hand all of the runner's outbox to the companion; the rest waits for the next collection"
|
|
12182
|
+
);
|
|
12183
|
+
}
|
|
12184
|
+
}
|
|
12185
|
+
if (refused) {
|
|
12186
|
+
log.warn(
|
|
12187
|
+
{ container: name, refused },
|
|
12188
|
+
"runners: refused outbox entries a runner's own credential couldn't have sent"
|
|
12189
|
+
);
|
|
12190
|
+
}
|
|
12191
|
+
if (queued) {
|
|
12192
|
+
log.info(
|
|
12193
|
+
{ container: name, queued },
|
|
12194
|
+
"runners: delivering what the runner left in its outbox"
|
|
12195
|
+
);
|
|
12196
|
+
opts.onOutboxCollected?.(attempt.agentId);
|
|
12197
|
+
}
|
|
12198
|
+
}
|
|
12199
|
+
held2.collected = !!transcripts?.complete && !!outbox?.complete && !unpublished;
|
|
12200
|
+
writePending(name, held2.collected ? [] : turnIds);
|
|
12201
|
+
},
|
|
12202
|
+
async release(handle, { destroy: over }) {
|
|
12203
|
+
const h = held.get(handle.id);
|
|
12204
|
+
held.delete(handle.id);
|
|
12205
|
+
try {
|
|
12206
|
+
await h?.git?.close();
|
|
12207
|
+
if (h && !h.collected) {
|
|
12208
|
+
log.warn(
|
|
12209
|
+
{ container: handle.id },
|
|
12210
|
+
"runners: kept the container: what its runner left couldn't be collected yet"
|
|
12211
|
+
);
|
|
12212
|
+
await docker.run(["stop", "-t", "10", handle.id]);
|
|
12213
|
+
} else if (over || !opts.keepContainers) await destroy(handle.id);
|
|
12214
|
+
else await docker.run(["stop", "-t", "10", handle.id]);
|
|
12215
|
+
} finally {
|
|
12216
|
+
h?.release();
|
|
12217
|
+
}
|
|
12218
|
+
}
|
|
12219
|
+
};
|
|
12220
|
+
return provider;
|
|
12221
|
+
}
|
|
12222
|
+
|
|
12223
|
+
// src/providers/docker-setup.ts
|
|
12224
|
+
import { createHash as createHash5 } from "crypto";
|
|
12225
|
+
var MODEL_HOSTS = [
|
|
12226
|
+
"api.anthropic.com",
|
|
12227
|
+
"api.openai.com",
|
|
12228
|
+
"chatgpt.com",
|
|
12229
|
+
"auth.openai.com"
|
|
12230
|
+
];
|
|
12231
|
+
var GIT_HOSTS = ["github.com"];
|
|
12232
|
+
function dockerRunnerSettings(config) {
|
|
12233
|
+
const docker = config.runners?.docker ?? {};
|
|
12234
|
+
const apiUrl = docker.apiUrl ?? config.baseUrl;
|
|
12235
|
+
const apiHost = new URL(apiUrl).host;
|
|
12236
|
+
const egressAllow = [
|
|
12237
|
+
.../* @__PURE__ */ new Set([apiHost, ...MODEL_HOSTS, ...GIT_HOSTS, ...docker.egressAllow ?? []])
|
|
12238
|
+
];
|
|
12239
|
+
return {
|
|
12240
|
+
image: docker.image ?? baseImageTag(),
|
|
12241
|
+
keepContainers: docker.keepContainers === true,
|
|
12242
|
+
apiUrl,
|
|
12243
|
+
egressAllow,
|
|
12244
|
+
gitHosts: [...GIT_HOSTS]
|
|
12245
|
+
};
|
|
12246
|
+
}
|
|
12247
|
+
function loopbackApi(apiUrl) {
|
|
12248
|
+
const host = new URL(apiUrl).hostname;
|
|
12249
|
+
return host === "localhost" || host === "::1" || host === "[::1]" || host.startsWith("127.");
|
|
12250
|
+
}
|
|
12251
|
+
function deviceKey(config) {
|
|
12252
|
+
const seed = config.deviceId ?? config.baseUrl;
|
|
12253
|
+
return createHash5("sha256").update(seed).digest("hex").slice(0, 12);
|
|
12254
|
+
}
|
|
12255
|
+
function resolveRuntimeEnv(env, store, log) {
|
|
12256
|
+
const out = {};
|
|
12257
|
+
for (const [name, template] of Object.entries(env ?? {})) {
|
|
12258
|
+
const missing = [];
|
|
12259
|
+
const value = template.replace(/\$\{([A-Za-z_][A-Za-z0-9_]*)\}/g, (_m, key) => {
|
|
12260
|
+
const v = store.get(key);
|
|
12261
|
+
if (v === void 0) missing.push(key);
|
|
12262
|
+
return v ?? "";
|
|
12263
|
+
});
|
|
12264
|
+
if (missing.length) {
|
|
12265
|
+
log?.warn({ name, missing }, "runners: a runner env value names secrets this device lacks");
|
|
12266
|
+
continue;
|
|
12267
|
+
}
|
|
12268
|
+
out[name] = value;
|
|
12269
|
+
}
|
|
12270
|
+
return out;
|
|
12271
|
+
}
|
|
12272
|
+
async function probeRunner(docker, image, source, log) {
|
|
12273
|
+
const r = await docker.run(
|
|
12274
|
+
[
|
|
12275
|
+
"run",
|
|
12276
|
+
"--rm",
|
|
12277
|
+
"--network",
|
|
12278
|
+
"none",
|
|
12279
|
+
...companionMountArgs(source),
|
|
12280
|
+
image,
|
|
12281
|
+
"cabane-companion",
|
|
12282
|
+
"run-turn",
|
|
12283
|
+
"--manifest"
|
|
12284
|
+
],
|
|
12285
|
+
{ timeoutMs: 6e4 }
|
|
12286
|
+
);
|
|
12287
|
+
try {
|
|
12288
|
+
const manifest = JSON.parse(r.stdout.trim());
|
|
12289
|
+
log.info(
|
|
12290
|
+
{ image, version: manifest.version, bundled: manifest.bundled },
|
|
12291
|
+
"runners: runner ready"
|
|
12292
|
+
);
|
|
12293
|
+
return manifest;
|
|
12294
|
+
} catch {
|
|
12295
|
+
log.error(
|
|
12296
|
+
{ image, err: r.stderr.trim().slice(-2e3) },
|
|
12297
|
+
"runners: the companion didn't start in a runner container"
|
|
12298
|
+
);
|
|
12299
|
+
return null;
|
|
12300
|
+
}
|
|
12301
|
+
}
|
|
12302
|
+
function imageResolution(manifest, image, runtime) {
|
|
12303
|
+
const status = manifest?.bundled[runtime] ?? "platform_package_missing";
|
|
12304
|
+
return {
|
|
12305
|
+
runtime,
|
|
12306
|
+
status,
|
|
12307
|
+
path: status === "present" ? `${image} (bundled)` : null,
|
|
12308
|
+
packageName: null,
|
|
12309
|
+
platform: "linux",
|
|
12310
|
+
arch: "image"
|
|
12311
|
+
};
|
|
12312
|
+
}
|
|
12313
|
+
function containerHookPath(hostPath) {
|
|
12314
|
+
const name = hostPath.split("/").pop() || "hook";
|
|
12315
|
+
const tag = createHash5("sha256").update(hostPath).digest("hex").slice(0, 8);
|
|
12316
|
+
return `/opt/cabane/hooks/${tag}-${name}`;
|
|
12317
|
+
}
|
|
12318
|
+
|
|
10744
12319
|
// src/runner/device-locks.ts
|
|
10745
12320
|
var DeviceLocks = class {
|
|
10746
12321
|
holders = /* @__PURE__ */ new Map();
|
|
@@ -10798,10 +12373,20 @@ var DeviceLocks = class {
|
|
|
10798
12373
|
|
|
10799
12374
|
// src/runner/harness-sandbox.ts
|
|
10800
12375
|
import { lookup } from "dns/promises";
|
|
10801
|
-
import {
|
|
12376
|
+
import {
|
|
12377
|
+
chmodSync as chmodSync6,
|
|
12378
|
+
mkdirSync as mkdirSync11,
|
|
12379
|
+
mkdtempSync,
|
|
12380
|
+
readFileSync as readFileSync10,
|
|
12381
|
+
realpathSync as realpathSync3,
|
|
12382
|
+
rmSync as rmSync10,
|
|
12383
|
+
writeFileSync as writeFileSync7
|
|
12384
|
+
} from "fs";
|
|
12385
|
+
import { createRequire as createRequire2 } from "module";
|
|
10802
12386
|
import { isIP } from "net";
|
|
10803
|
-
import { homedir as homedir3, networkInterfaces, tmpdir as
|
|
10804
|
-
import { join as
|
|
12387
|
+
import { homedir as homedir3, networkInterfaces, tmpdir as tmpdir3, userInfo as userInfo2 } from "os";
|
|
12388
|
+
import { dirname as dirname8, join as join18 } from "path";
|
|
12389
|
+
import { fileURLToPath as fileURLToPath4 } from "url";
|
|
10805
12390
|
import { SandboxManager } from "@anthropic-ai/sandbox-runtime";
|
|
10806
12391
|
function osSandboxUsable(log) {
|
|
10807
12392
|
if (!SandboxManager.isSupportedPlatform()) {
|
|
@@ -10900,15 +12485,15 @@ var DispatchClaims = class {
|
|
|
10900
12485
|
|
|
10901
12486
|
// src/outbox.ts
|
|
10902
12487
|
import {
|
|
10903
|
-
existsSync as
|
|
10904
|
-
mkdirSync as
|
|
10905
|
-
readdirSync as
|
|
10906
|
-
readFileSync as
|
|
10907
|
-
renameSync as
|
|
10908
|
-
rmSync as
|
|
10909
|
-
writeFileSync as
|
|
12488
|
+
existsSync as existsSync14,
|
|
12489
|
+
mkdirSync as mkdirSync12,
|
|
12490
|
+
readdirSync as readdirSync5,
|
|
12491
|
+
readFileSync as readFileSync11,
|
|
12492
|
+
renameSync as renameSync6,
|
|
12493
|
+
rmSync as rmSync11,
|
|
12494
|
+
writeFileSync as writeFileSync8
|
|
10910
12495
|
} from "fs";
|
|
10911
|
-
import { join as
|
|
12496
|
+
import { join as join19 } from "path";
|
|
10912
12497
|
var MAX_ENTRIES = 2e3;
|
|
10913
12498
|
var MAX_AGE_MS = 24 * 60 * 60 * 1e3;
|
|
10914
12499
|
var Outbox = class {
|
|
@@ -10921,25 +12506,25 @@ var Outbox = class {
|
|
|
10921
12506
|
// Resolved lazily (per call, like cursor.ts) so tests that swap HOME between
|
|
10922
12507
|
// cases route writes at the right tmpdir.
|
|
10923
12508
|
dir() {
|
|
10924
|
-
return
|
|
12509
|
+
return join19(runnerStateDir(), "outbox", encodeURIComponent(this.workspaceId));
|
|
10925
12510
|
}
|
|
10926
12511
|
fileFor(turnId, seq) {
|
|
10927
|
-
return
|
|
12512
|
+
return join19(this.dir(), `${encodeURIComponent(turnId)}__${seq}.json`);
|
|
10928
12513
|
}
|
|
10929
12514
|
// Persist a commit for later draining. Atomic (temp file + rename) so a
|
|
10930
12515
|
// concurrent `list()` never reads a half-written entry, then enforces the
|
|
10931
12516
|
// per-workspace bounds.
|
|
10932
12517
|
persist(entry) {
|
|
10933
12518
|
const dir2 = this.dir();
|
|
10934
|
-
|
|
12519
|
+
mkdirSync12(dir2, { recursive: true });
|
|
10935
12520
|
const target = this.fileFor(entry.turnId, entry.seq);
|
|
10936
12521
|
const tmp = `${target}.${process.pid}.tmp`;
|
|
10937
12522
|
try {
|
|
10938
|
-
|
|
10939
|
-
|
|
12523
|
+
writeFileSync8(tmp, JSON.stringify(entry) + "\n", "utf8");
|
|
12524
|
+
renameSync6(tmp, target);
|
|
10940
12525
|
} catch (err) {
|
|
10941
12526
|
try {
|
|
10942
|
-
|
|
12527
|
+
rmSync11(tmp, { force: true });
|
|
10943
12528
|
} catch {
|
|
10944
12529
|
}
|
|
10945
12530
|
this.log?.warn(
|
|
@@ -10956,19 +12541,19 @@ var Outbox = class {
|
|
|
10956
12541
|
// wedging the drain.
|
|
10957
12542
|
list() {
|
|
10958
12543
|
const dir2 = this.dir();
|
|
10959
|
-
if (!
|
|
12544
|
+
if (!existsSync14(dir2)) return [];
|
|
10960
12545
|
let names;
|
|
10961
12546
|
try {
|
|
10962
|
-
names =
|
|
12547
|
+
names = readdirSync5(dir2);
|
|
10963
12548
|
} catch {
|
|
10964
12549
|
return [];
|
|
10965
12550
|
}
|
|
10966
12551
|
const entries = [];
|
|
10967
12552
|
for (const name of names) {
|
|
10968
12553
|
if (!name.endsWith(".json")) continue;
|
|
10969
|
-
const full =
|
|
12554
|
+
const full = join19(dir2, name);
|
|
10970
12555
|
try {
|
|
10971
|
-
const parsed = JSON.parse(
|
|
12556
|
+
const parsed = JSON.parse(readFileSync11(full, "utf8"));
|
|
10972
12557
|
if (parsed && typeof parsed.turnId === "string" && typeof parsed.seq === "number" && typeof parsed.path === "string") {
|
|
10973
12558
|
entries.push(parsed);
|
|
10974
12559
|
} else {
|
|
@@ -11002,8 +12587,8 @@ var Outbox = class {
|
|
|
11002
12587
|
// different: it is positive evidence that nothing was ever queued here.
|
|
11003
12588
|
maxSeqForTurn(turnId) {
|
|
11004
12589
|
const dir2 = this.dir();
|
|
11005
|
-
if (!
|
|
11006
|
-
const names =
|
|
12590
|
+
if (!existsSync14(dir2)) return 0;
|
|
12591
|
+
const names = readdirSync5(dir2);
|
|
11007
12592
|
const prefix = `${encodeURIComponent(turnId)}__`;
|
|
11008
12593
|
let max = 0;
|
|
11009
12594
|
for (const name of names) {
|
|
@@ -11016,15 +12601,15 @@ var Outbox = class {
|
|
|
11016
12601
|
// Remove a delivered (or terminally-discarded) entry. No-op if already gone.
|
|
11017
12602
|
remove(turnId, seq) {
|
|
11018
12603
|
try {
|
|
11019
|
-
|
|
12604
|
+
rmSync11(this.fileFor(turnId, seq), { force: true });
|
|
11020
12605
|
} catch {
|
|
11021
12606
|
}
|
|
11022
12607
|
}
|
|
11023
12608
|
size() {
|
|
11024
12609
|
const dir2 = this.dir();
|
|
11025
|
-
if (!
|
|
12610
|
+
if (!existsSync14(dir2)) return 0;
|
|
11026
12611
|
try {
|
|
11027
|
-
return
|
|
12612
|
+
return readdirSync5(dir2).filter((n) => n.endsWith(".json")).length;
|
|
11028
12613
|
} catch {
|
|
11029
12614
|
return 0;
|
|
11030
12615
|
}
|
|
@@ -11035,7 +12620,7 @@ var Outbox = class {
|
|
|
11035
12620
|
"A saved update is unreadable and couldn't be delivered. Check the conversation for missing replies."
|
|
11036
12621
|
);
|
|
11037
12622
|
try {
|
|
11038
|
-
|
|
12623
|
+
rmSync11(full, { force: true });
|
|
11039
12624
|
} catch {
|
|
11040
12625
|
}
|
|
11041
12626
|
}
|
|
@@ -11178,8 +12763,8 @@ function sleep3(ms) {
|
|
|
11178
12763
|
}
|
|
11179
12764
|
|
|
11180
12765
|
// src/version.ts
|
|
11181
|
-
import { createRequire as
|
|
11182
|
-
var pkg =
|
|
12766
|
+
import { createRequire as createRequire3 } from "module";
|
|
12767
|
+
var pkg = createRequire3(import.meta.url)("../package.json");
|
|
11183
12768
|
var COMPANION_VERSION = pkg.version;
|
|
11184
12769
|
|
|
11185
12770
|
// src/supervisor.ts
|
|
@@ -11282,8 +12867,27 @@ var CompanionSupervisor = class {
|
|
|
11282
12867
|
if (opts.runnerCommand) this.runnerCommand = opts.runnerCommand;
|
|
11283
12868
|
if (opts.runnerProvider) this.providerForRunners = opts.runnerProvider;
|
|
11284
12869
|
this.unrunnableRetryDelaysMs = opts.unrunnableRetryDelaysMs ?? DEFAULT_UNRUNNABLE_RETRY_DELAYS_MS;
|
|
12870
|
+
if (this.dockerRunners()) {
|
|
12871
|
+
const { image } = dockerRunnerSettings(this.config);
|
|
12872
|
+
this.resolveBundled = (runtime) => imageResolution(this.runnerImage, image, runtime);
|
|
12873
|
+
}
|
|
11285
12874
|
this.latchBootBundledState();
|
|
11286
12875
|
}
|
|
12876
|
+
// CT1681: what a runner container said it can run, probed once at start;
|
|
12877
|
+
// null until then, and when no container could be started.
|
|
12878
|
+
runnerImage = null;
|
|
12879
|
+
// CT1681: the companion every container mounts — this one's own install.
|
|
12880
|
+
runnerCompanion() {
|
|
12881
|
+
return companionSource({
|
|
12882
|
+
configured: this.config.runners?.docker?.companionDir,
|
|
12883
|
+
platform: process.platform,
|
|
12884
|
+
version: COMPANION_VERSION,
|
|
12885
|
+
own: ownInstall()
|
|
12886
|
+
});
|
|
12887
|
+
}
|
|
12888
|
+
dockerRunners() {
|
|
12889
|
+
return runnersEnabled(this.config) && this.config.runners?.provider === "docker";
|
|
12890
|
+
}
|
|
11287
12891
|
// Stand up the data plane: pair check, initial assignments pull, then the
|
|
11288
12892
|
// heartbeat + poll loops. A companion with no device token (logged out) does
|
|
11289
12893
|
// nothing but say so.
|
|
@@ -11292,6 +12896,31 @@ var CompanionSupervisor = class {
|
|
|
11292
12896
|
{ protocolVersion: TURN_PROTOCOL_VERSION, version: COMPANION_VERSION },
|
|
11293
12897
|
`Companion ${COMPANION_VERSION} starting on ${this.config.deviceLabel ?? hostname2()}`
|
|
11294
12898
|
);
|
|
12899
|
+
if (this.dockerRunners() && !this.providerForRunners) {
|
|
12900
|
+
const { image, apiUrl } = dockerRunnerSettings(this.config);
|
|
12901
|
+
if (loopbackApi(apiUrl)) {
|
|
12902
|
+
this.log.error(
|
|
12903
|
+
{ apiUrl },
|
|
12904
|
+
"runners: a container can't reach this machine's loopback; set runners.docker.apiUrl to an address it can"
|
|
12905
|
+
);
|
|
12906
|
+
}
|
|
12907
|
+
const source = this.runnerCompanion();
|
|
12908
|
+
const docker = dockerCli();
|
|
12909
|
+
if ("error" in source) {
|
|
12910
|
+
this.log.error({ reason: source.error }, "runners: no companion to put in a container");
|
|
12911
|
+
} else if (await ensureBaseImage(docker, image, this.log) && await ensureCompanionVolume(docker, image, source, this.log)) {
|
|
12912
|
+
this.runnerImage = await probeRunner(docker, image, source, this.log);
|
|
12913
|
+
if (!this.runnerImage) await invalidateCompanionVolume(docker, image, source);
|
|
12914
|
+
}
|
|
12915
|
+
this.latchBootBundledState();
|
|
12916
|
+
}
|
|
12917
|
+
if (this.runnerSandbox() === "os") {
|
|
12918
|
+
const policy = this.sandboxPolicy();
|
|
12919
|
+
const posture = postureLines(policy);
|
|
12920
|
+
const summary = posture.join("; ");
|
|
12921
|
+
if (policy.ok) this.log.info({ posture }, `runners: OS sandbox policy \u2014 ${summary}`);
|
|
12922
|
+
else this.log.error({ posture }, `runners: ${summary}`);
|
|
12923
|
+
}
|
|
11295
12924
|
const startupHarnessRefresh = this.refreshHarnessStatuses();
|
|
11296
12925
|
this.trackHeartbeat(startupHarnessRefresh);
|
|
11297
12926
|
if (!this.config.deviceToken) {
|
|
@@ -11399,7 +13028,9 @@ var CompanionSupervisor = class {
|
|
|
11399
13028
|
runtimeIssues: this.runtimeIssues(),
|
|
11400
13029
|
// CT571/CT586: each runtime's `version` from the latest harness probe
|
|
11401
13030
|
// (fail-soft to null). Informational only — the server matches on name.
|
|
11402
|
-
versions: this.harnessVersions
|
|
13031
|
+
versions: this.harnessVersions,
|
|
13032
|
+
// CT1681: shown on the device page.
|
|
13033
|
+
...runnersEnabled(this.config) ? { runnerProvider: this.config.runners?.provider ?? "local-process" } : {}
|
|
11403
13034
|
}),
|
|
11404
13035
|
// CT566: echo the last classified credential state per runtime, when the
|
|
11405
13036
|
// companion has seen any. Omitted (undefined) until the first observed
|
|
@@ -11731,22 +13362,79 @@ var CompanionSupervisor = class {
|
|
|
11731
13362
|
// CT1679: `runners.sandbox`, checked against this host once. A host that
|
|
11732
13363
|
// can't make the sandbox says so once and runs its turns unwrapped.
|
|
11733
13364
|
sandboxUsable;
|
|
13365
|
+
// CT1684: `runners.policy`, parsed once. An invalid one is kept as the
|
|
13366
|
+
// error, which every sandboxed turn is refused with.
|
|
13367
|
+
parsedSandboxPolicy;
|
|
13368
|
+
sandboxPolicy() {
|
|
13369
|
+
this.parsedSandboxPolicy ??= parseSandboxPolicy(this.config.runners?.policy);
|
|
13370
|
+
return this.parsedSandboxPolicy;
|
|
13371
|
+
}
|
|
11734
13372
|
runnerSandbox() {
|
|
11735
|
-
if (this.config.runners?.sandbox !== "os") return "none";
|
|
13373
|
+
if (this.config.runners?.sandbox !== "os" || this.dockerRunners()) return "none";
|
|
11736
13374
|
this.sandboxUsable ??= osSandboxUsable(this.log);
|
|
11737
13375
|
return this.sandboxUsable ? "os" : "none";
|
|
11738
13376
|
}
|
|
11739
13377
|
// CT1679: one provider for the device, built on first use.
|
|
11740
13378
|
runnerProvider() {
|
|
11741
|
-
this.providerForRunners ??= createLocalProcessProvider();
|
|
13379
|
+
this.providerForRunners ??= this.dockerRunners() ? this.createDockerRunners() : createLocalProcessProvider();
|
|
11742
13380
|
return this.providerForRunners;
|
|
11743
13381
|
}
|
|
13382
|
+
createDockerRunners() {
|
|
13383
|
+
const settings = dockerRunnerSettings(this.config);
|
|
13384
|
+
const docker = this.config.runners?.docker ?? {};
|
|
13385
|
+
const hooks = [
|
|
13386
|
+
this.config.prepareHook?.command,
|
|
13387
|
+
...Object.values(this.config.agents ?? {}).map((a) => a.prepareHook?.command)
|
|
13388
|
+
].filter((c) => typeof c === "string");
|
|
13389
|
+
const source = this.runnerCompanion();
|
|
13390
|
+
return createDockerProvider({
|
|
13391
|
+
image: settings.image,
|
|
13392
|
+
// Without a companion nothing is advertised, so nothing is sent; a turn
|
|
13393
|
+
// that arrives anyway fails at the mount and says so.
|
|
13394
|
+
companion: "error" in source ? { kind: "dir", path: "/nonexistent", key: "none" } : source,
|
|
13395
|
+
deviceKey: deviceKey(this.config),
|
|
13396
|
+
keepContainers: settings.keepContainers,
|
|
13397
|
+
egressAllow: settings.egressAllow,
|
|
13398
|
+
gitHosts: settings.gitHosts,
|
|
13399
|
+
gitFill: deviceGitFill(docker.gitCredentialHelper),
|
|
13400
|
+
runtimeEnv: () => resolveRuntimeEnv(
|
|
13401
|
+
docker.env,
|
|
13402
|
+
loadSecretStoreTolerant(
|
|
13403
|
+
(m) => this.log.warn(
|
|
13404
|
+
{ err: m },
|
|
13405
|
+
"Could not read the secrets file. Check ~/.cabane/secrets.json."
|
|
13406
|
+
)
|
|
13407
|
+
),
|
|
13408
|
+
this.log
|
|
13409
|
+
),
|
|
13410
|
+
mounts: [...new Set(hooks)].map((source2) => ({ source: source2, target: containerHookPath(source2) })),
|
|
13411
|
+
deviceDir: cabaneDir(),
|
|
13412
|
+
hostStateDir: runnerStateDir(),
|
|
13413
|
+
onOutboxCollected: (agentId) => {
|
|
13414
|
+
for (const wr of this.workspaces.values()) wr.agents.get(agentId)?.kickDrain();
|
|
13415
|
+
},
|
|
13416
|
+
log: this.log
|
|
13417
|
+
});
|
|
13418
|
+
}
|
|
13419
|
+
// CT1681: the runner command for this device's provider: the image's own
|
|
13420
|
+
// companion in a container, this process's otherwise.
|
|
13421
|
+
runnerCommandFor() {
|
|
13422
|
+
if (this.runnerCommand) return this.runnerCommand;
|
|
13423
|
+
return this.dockerRunners() ? ["cabane-companion", "run-turn"] : selfRunnerCommand();
|
|
13424
|
+
}
|
|
11744
13425
|
buildDispatcher(ctx) {
|
|
11745
|
-
const
|
|
13426
|
+
const hostLocal = localAgentConfig(this.config, {
|
|
11746
13427
|
agentId: ctx.agentId,
|
|
11747
13428
|
agentUsername: ctx.agentUsername,
|
|
11748
13429
|
workspaceSlug: ctx.workspaceSlug
|
|
11749
13430
|
});
|
|
13431
|
+
const local = this.dockerRunners() && hostLocal.prepareHook ? {
|
|
13432
|
+
...hostLocal,
|
|
13433
|
+
prepareHook: {
|
|
13434
|
+
...hostLocal.prepareHook,
|
|
13435
|
+
command: containerHookPath(hostLocal.prepareHook.command)
|
|
13436
|
+
}
|
|
13437
|
+
} : hostLocal;
|
|
11750
13438
|
if (this.dispatcherFactory) return this.dispatcherFactory({ ...ctx, local });
|
|
11751
13439
|
return new Dispatcher({
|
|
11752
13440
|
deviceId: this.deviceId ?? void 0,
|
|
@@ -11795,11 +13483,13 @@ var CompanionSupervisor = class {
|
|
|
11795
13483
|
...runnersEnabled(this.config) ? {
|
|
11796
13484
|
runner: {
|
|
11797
13485
|
provider: this.runnerProvider(),
|
|
11798
|
-
command: this.
|
|
13486
|
+
command: this.runnerCommandFor(),
|
|
13487
|
+
...this.dockerRunners() ? { runnerBaseUrl: dockerRunnerSettings(this.config).apiUrl } : {},
|
|
11799
13488
|
reportRunnerGone: (report) => this.reportRunnerGone(report),
|
|
11800
13489
|
connectorHealth: this.connectorHealth,
|
|
11801
13490
|
locks: this.runnerLocks,
|
|
11802
|
-
sandbox: this.runnerSandbox()
|
|
13491
|
+
sandbox: this.runnerSandbox(),
|
|
13492
|
+
policy: this.sandboxPolicy()
|
|
11803
13493
|
}
|
|
11804
13494
|
} : {}
|
|
11805
13495
|
});
|
|
@@ -12119,7 +13809,7 @@ var CompanionSupervisor = class {
|
|
|
12119
13809
|
);
|
|
12120
13810
|
}
|
|
12121
13811
|
const resumedTurnId = recoveredTurnId ?? (dispatchId ? turnIdForEvent(workspaceId, dispatchId) : null);
|
|
12122
|
-
const turnId = resumedTurnId ??
|
|
13812
|
+
const turnId = resumedTurnId ?? randomUUID6();
|
|
12123
13813
|
if (dispatchId) {
|
|
12124
13814
|
const liveTurnIds = rememberTurnId(workspaceId, dispatchId, turnId);
|
|
12125
13815
|
if (liveTurnIds > TURN_ID_OVERFLOW_WARN) {
|
|
@@ -12424,9 +14114,9 @@ async function waitForBoundedHeartbeat(heartbeat) {
|
|
|
12424
14114
|
}
|
|
12425
14115
|
function defaultReexec() {
|
|
12426
14116
|
clearRuntimeState();
|
|
12427
|
-
void import("child_process").then(({ spawn:
|
|
14117
|
+
void import("child_process").then(({ spawn: spawn9 }) => {
|
|
12428
14118
|
try {
|
|
12429
|
-
const child =
|
|
14119
|
+
const child = spawn9(process.execPath, process.argv.slice(1), {
|
|
12430
14120
|
stdio: "inherit",
|
|
12431
14121
|
detached: false
|
|
12432
14122
|
});
|
|
@@ -12496,22 +14186,22 @@ function handleUncaught(log, err, origin) {
|
|
|
12496
14186
|
}
|
|
12497
14187
|
|
|
12498
14188
|
// src/crash-marker.ts
|
|
12499
|
-
import { existsSync as
|
|
12500
|
-
import { join as
|
|
14189
|
+
import { existsSync as existsSync15, mkdirSync as mkdirSync13, readFileSync as readFileSync12, rmSync as rmSync12, writeFileSync as writeFileSync9 } from "fs";
|
|
14190
|
+
import { join as join20 } from "path";
|
|
12501
14191
|
function crashMarkerPath() {
|
|
12502
|
-
return
|
|
14192
|
+
return join20(cabaneDir(), "last-error.json");
|
|
12503
14193
|
}
|
|
12504
14194
|
function recordCrash(rec) {
|
|
12505
14195
|
try {
|
|
12506
|
-
|
|
12507
|
-
|
|
14196
|
+
mkdirSync13(cabaneDir(), { recursive: true });
|
|
14197
|
+
writeFileSync9(crashMarkerPath(), JSON.stringify(rec, null, 2) + "\n");
|
|
12508
14198
|
} catch {
|
|
12509
14199
|
}
|
|
12510
14200
|
}
|
|
12511
14201
|
function clearCrash() {
|
|
12512
14202
|
try {
|
|
12513
14203
|
const path = crashMarkerPath();
|
|
12514
|
-
if (
|
|
14204
|
+
if (existsSync15(path)) rmSync12(path, { force: true });
|
|
12515
14205
|
} catch {
|
|
12516
14206
|
}
|
|
12517
14207
|
}
|
|
@@ -12554,7 +14244,7 @@ async function createCompanionRuntime(opts = {}) {
|
|
|
12554
14244
|
opencodeServerUrl: cfg.opencode?.serverUrl,
|
|
12555
14245
|
codex: isCodexEnabled(cfg)
|
|
12556
14246
|
});
|
|
12557
|
-
const instanceId =
|
|
14247
|
+
const instanceId = randomUUID7();
|
|
12558
14248
|
const startedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
12559
14249
|
const pre = readLiveRuntimeState();
|
|
12560
14250
|
if (pre && await verifyRuntime(pre) === "stale") clearRuntimeState();
|