@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/cli.js
CHANGED
|
@@ -541,10 +541,45 @@ var init_config = __esm({
|
|
|
541
541
|
// wraps the runner in an OS sandbox where the host supports one; `'none'`
|
|
542
542
|
// (the default) runs it as a plain child beside this process, a supported
|
|
543
543
|
// configuration in its own right rather than a degraded one.
|
|
544
|
+
// CT1684: `policy` is what the OS sandbox lets a turn reach, read and hold
|
|
545
|
+
// (`runner/sandbox-policy.ts`). It is validated there rather than here, so a
|
|
546
|
+
// bad policy refuses the turns it governs, with the field named, instead of
|
|
547
|
+
// keeping the companion from starting at all.
|
|
544
548
|
runners: z2.object({
|
|
545
549
|
enabled: z2.boolean().optional(),
|
|
546
|
-
provider: z2.
|
|
547
|
-
sandbox: z2.enum(["os", "none"]).optional()
|
|
550
|
+
provider: z2.enum(["local-process", "docker"]).optional(),
|
|
551
|
+
sandbox: z2.enum(["os", "none"]).optional(),
|
|
552
|
+
// CT1681: the `docker` provider — a container per conversation and agent,
|
|
553
|
+
// from one image, on a network whose only way out is an allow-listed proxy.
|
|
554
|
+
docker: z2.object({
|
|
555
|
+
// The base image a container runs. Default: the one this companion
|
|
556
|
+
// builds from its own Dockerfile on first use. One named here must
|
|
557
|
+
// have that one's layout (the `runner` user, `cabane-companion` on
|
|
558
|
+
// PATH reading `CABANE_COMPANION_ROOT`).
|
|
559
|
+
image: z2.string().min(1).optional(),
|
|
560
|
+
// The companion install mounted into every container. Default, on
|
|
561
|
+
// Linux: this companion's own. Needed for one run from a checkout
|
|
562
|
+
// (a `pnpm --filter @cabane/companion deploy --prod <dir>`).
|
|
563
|
+
companionDir: z2.string().min(1).optional(),
|
|
564
|
+
// Hosts a turn may reach besides the Cabane API, the model providers
|
|
565
|
+
// and the git host. `*.example.com` matches any subdomain.
|
|
566
|
+
egressAllow: z2.array(z2.string().min(1)).optional(),
|
|
567
|
+
// Keep a turn's container between turns and start it again for the
|
|
568
|
+
// next one. Off by default: the container goes after every turn and
|
|
569
|
+
// the runtime's session files are restored into the next.
|
|
570
|
+
keepContainers: z2.boolean().optional(),
|
|
571
|
+
// The Cabane URL as a container reaches it, for a device whose own
|
|
572
|
+
// `baseUrl` is a loopback address the container can't see.
|
|
573
|
+
apiUrl: z2.string().url().refine(isAllowedBaseUrl).optional(),
|
|
574
|
+
// Env set on each turn's runner, `${NAME}` resolved from secrets.json
|
|
575
|
+
// at exec: the runtime's own auth, which the image never carries.
|
|
576
|
+
env: z2.record(z2.string(), z2.string()).optional(),
|
|
577
|
+
// A git credential helper run on this device to answer the
|
|
578
|
+
// container's git for an allowed host, e.g. `!gh auth git-credential`.
|
|
579
|
+
// Absent: git's own configured helpers on this device.
|
|
580
|
+
gitCredentialHelper: z2.string().min(1).optional()
|
|
581
|
+
}).strict().optional(),
|
|
582
|
+
policy: z2.unknown().optional()
|
|
548
583
|
}).strict().optional()
|
|
549
584
|
});
|
|
550
585
|
}
|
|
@@ -2543,8 +2578,8 @@ var init_policy = __esm({
|
|
|
2543
2578
|
// packages/agent-runtime/src/claude-code/model.ts
|
|
2544
2579
|
function parseClaudeCodeModel(model) {
|
|
2545
2580
|
if (!model) return null;
|
|
2546
|
-
const
|
|
2547
|
-
const bare =
|
|
2581
|
+
const sep2 = model.indexOf("/");
|
|
2582
|
+
const bare = sep2 === -1 || model.slice(0, sep2) !== "anthropic" ? model : model.slice(sep2 + 1);
|
|
2548
2583
|
return bare === CLAUDE_CODE_DEFAULT_MODEL_BARE ? null : model;
|
|
2549
2584
|
}
|
|
2550
2585
|
var CLAUDE_CODE_DEFAULT_MODEL_BARE;
|
|
@@ -3991,9 +4026,9 @@ var init_policy2 = __esm({
|
|
|
3991
4026
|
|
|
3992
4027
|
// packages/agent-runtime/src/opencode/model.ts
|
|
3993
4028
|
function parseOpencodeModel(model) {
|
|
3994
|
-
const
|
|
3995
|
-
if (
|
|
3996
|
-
return { providerID: model.slice(0,
|
|
4029
|
+
const sep2 = model.indexOf("/");
|
|
4030
|
+
if (sep2 === -1) return { providerID: "", modelID: model };
|
|
4031
|
+
return { providerID: model.slice(0, sep2), modelID: model.slice(sep2 + 1) };
|
|
3997
4032
|
}
|
|
3998
4033
|
var init_model2 = __esm({
|
|
3999
4034
|
"packages/agent-runtime/src/opencode/model.ts"() {
|
|
@@ -4262,10 +4297,10 @@ async function* parseSseStream(body, sessionId, signal) {
|
|
|
4262
4297
|
const { value, done } = await reader.read();
|
|
4263
4298
|
if (done) return;
|
|
4264
4299
|
buf += decoder.decode(value, { stream: true });
|
|
4265
|
-
let
|
|
4266
|
-
while ((
|
|
4267
|
-
const frame = buf.slice(0,
|
|
4268
|
-
buf = buf.slice(
|
|
4300
|
+
let sep2;
|
|
4301
|
+
while ((sep2 = buf.indexOf("\n\n")) !== -1) {
|
|
4302
|
+
const frame = buf.slice(0, sep2);
|
|
4303
|
+
buf = buf.slice(sep2 + 2);
|
|
4269
4304
|
const data = frame.split("\n").filter((l) => l.startsWith("data:")).map((l) => l.slice(5).trim()).join("");
|
|
4270
4305
|
if (!data) continue;
|
|
4271
4306
|
let ev;
|
|
@@ -5291,8 +5326,8 @@ var init_policy3 = __esm({
|
|
|
5291
5326
|
|
|
5292
5327
|
// packages/agent-runtime/src/codex/model.ts
|
|
5293
5328
|
function parseCodexModel(model) {
|
|
5294
|
-
const
|
|
5295
|
-
const bare =
|
|
5329
|
+
const sep2 = model.indexOf("/");
|
|
5330
|
+
const bare = sep2 === -1 || model.slice(0, sep2) !== "openai" ? model : model.slice(sep2 + 1);
|
|
5296
5331
|
return CODEX_DEFAULT_MODEL_BARES.has(bare) ? null : bare;
|
|
5297
5332
|
}
|
|
5298
5333
|
var CODEX_DEFAULT_MODEL_BARES;
|
|
@@ -6632,6 +6667,173 @@ var init_src = __esm({
|
|
|
6632
6667
|
}
|
|
6633
6668
|
});
|
|
6634
6669
|
|
|
6670
|
+
// src/runner/sandbox-policy.ts
|
|
6671
|
+
import { NetworkConfigSchema } from "@anthropic-ai/sandbox-runtime";
|
|
6672
|
+
import { globToRegex } from "@anthropic-ai/sandbox-runtime/dist/sandbox/sandbox-utils.js";
|
|
6673
|
+
import { z as z13 } from "zod";
|
|
6674
|
+
function parseSandboxPolicy(raw) {
|
|
6675
|
+
if (raw === void 0) return { ok: true, policy: {} };
|
|
6676
|
+
const result = sandboxPolicySchema.safeParse(raw);
|
|
6677
|
+
if (!result.success) {
|
|
6678
|
+
const issue = result.error.issues[0];
|
|
6679
|
+
return { ok: false, error: `${fieldPath(issue.path)}: ${issue.message}` };
|
|
6680
|
+
}
|
|
6681
|
+
const refused = unenforceable(result.data);
|
|
6682
|
+
return refused ? { ok: false, error: refused } : { ok: true, policy: result.data };
|
|
6683
|
+
}
|
|
6684
|
+
function fieldPath(path) {
|
|
6685
|
+
return ["runners", "policy", ...path.map(String)].join(".");
|
|
6686
|
+
}
|
|
6687
|
+
function unenforceable(policy) {
|
|
6688
|
+
const hostLists = [
|
|
6689
|
+
["hosts.allow", "allowedDomains", policy.hosts?.allow],
|
|
6690
|
+
["hosts.deny", "deniedDomains", policy.hosts?.deny],
|
|
6691
|
+
...Object.entries(policy.credentials?.env ?? {}).map(
|
|
6692
|
+
([name, entry]) => [
|
|
6693
|
+
`credentials.env.${name}.hosts`,
|
|
6694
|
+
"allowedDomains",
|
|
6695
|
+
entry.hosts
|
|
6696
|
+
]
|
|
6697
|
+
)
|
|
6698
|
+
];
|
|
6699
|
+
for (const [field, as, hosts] of hostLists) {
|
|
6700
|
+
for (const [i, host] of (hosts ?? []).entries()) {
|
|
6701
|
+
const check = NetworkConfigSchema.safeParse({
|
|
6702
|
+
allowedDomains: as === "allowedDomains" ? [host] : [],
|
|
6703
|
+
deniedDomains: as === "deniedDomains" ? [host] : []
|
|
6704
|
+
});
|
|
6705
|
+
if (!check.success) {
|
|
6706
|
+
const message = check.error.issues[0]?.message ?? "not a host pattern the sandbox accepts";
|
|
6707
|
+
return `runners.policy.${field}.${i}: ${message}`;
|
|
6708
|
+
}
|
|
6709
|
+
}
|
|
6710
|
+
}
|
|
6711
|
+
const pathLists = [
|
|
6712
|
+
["filesystem.denyRead", policy.filesystem?.denyRead],
|
|
6713
|
+
["filesystem.allowRead", policy.filesystem?.allowRead],
|
|
6714
|
+
["filesystem.allowWrite", policy.filesystem?.allowWrite]
|
|
6715
|
+
];
|
|
6716
|
+
for (const [field, paths] of pathLists) {
|
|
6717
|
+
for (const [i, path] of (paths ?? []).entries()) {
|
|
6718
|
+
try {
|
|
6719
|
+
new RegExp(globToRegex(path));
|
|
6720
|
+
} catch (err) {
|
|
6721
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
6722
|
+
return `runners.policy.${field}.${i}: not a pattern the sandbox can match (${message})`;
|
|
6723
|
+
}
|
|
6724
|
+
}
|
|
6725
|
+
}
|
|
6726
|
+
return null;
|
|
6727
|
+
}
|
|
6728
|
+
function modelHostsFor(runtime) {
|
|
6729
|
+
if (runtime === "claude-code") return ANTHROPIC_HOSTS;
|
|
6730
|
+
if (runtime === "codex") return OPENAI_HOSTS;
|
|
6731
|
+
return [...ANTHROPIC_HOSTS, ...OPENAI_HOSTS];
|
|
6732
|
+
}
|
|
6733
|
+
function gitAuthVar(index) {
|
|
6734
|
+
return `CABANE_GIT_AUTH_${index}`;
|
|
6735
|
+
}
|
|
6736
|
+
function resolvePolicyCredentials(policy, store) {
|
|
6737
|
+
const missing = /* @__PURE__ */ new Set();
|
|
6738
|
+
const credentials = [];
|
|
6739
|
+
const lookup2 = (ref) => {
|
|
6740
|
+
const name = placeholderName(ref);
|
|
6741
|
+
const value = store.get(name);
|
|
6742
|
+
if (value === void 0) missing.add(name);
|
|
6743
|
+
return value;
|
|
6744
|
+
};
|
|
6745
|
+
for (const [name, entry] of Object.entries(policy.credentials?.env ?? {})) {
|
|
6746
|
+
const value = lookup2(entry.secret);
|
|
6747
|
+
if (value === void 0) continue;
|
|
6748
|
+
credentials.push({ name, value, hosts: entry.hosts });
|
|
6749
|
+
}
|
|
6750
|
+
(policy.credentials?.git ?? []).forEach((entry, i) => {
|
|
6751
|
+
const secret = lookup2(entry.secret);
|
|
6752
|
+
if (secret === void 0) return;
|
|
6753
|
+
const user = entry.username ?? "x-access-token";
|
|
6754
|
+
credentials.push({
|
|
6755
|
+
name: gitAuthVar(i),
|
|
6756
|
+
value: Buffer.from(`${user}:${secret}`).toString("base64"),
|
|
6757
|
+
hosts: [entry.host.replace(/:\d+$/, "")],
|
|
6758
|
+
gitHost: entry.host
|
|
6759
|
+
});
|
|
6760
|
+
});
|
|
6761
|
+
return { credentials, missing: [...missing].sort() };
|
|
6762
|
+
}
|
|
6763
|
+
function policyHasCredentials(policy) {
|
|
6764
|
+
return Object.keys(policy.credentials?.env ?? {}).length > 0 || (policy.credentials?.git ?? []).length > 0;
|
|
6765
|
+
}
|
|
6766
|
+
function postureLines(parsed) {
|
|
6767
|
+
if (!parsed.ok) return [`policy invalid \u2014 turns are refused until it's fixed: ${parsed.error}`];
|
|
6768
|
+
const { policy } = parsed;
|
|
6769
|
+
const mode = policy.filesystem?.mode ?? "blocklist";
|
|
6770
|
+
const defaults = policy.hosts?.defaults !== false;
|
|
6771
|
+
const lines2 = [
|
|
6772
|
+
`filesystem: ${mode}${mode === "strict" ? " (home unreadable except the checkout, harness state and allowRead)" : ""}`,
|
|
6773
|
+
`hosts: Cabane + the turn's model provider${defaults ? " + defaults (GitHub, npm, PyPI, crates.io)" : ""}${policy.hosts?.allow?.length ? ` + ${policy.hosts.allow.join(", ")}` : ""}`
|
|
6774
|
+
];
|
|
6775
|
+
if (policy.hosts?.deny?.length) lines2.push(`hosts denied: ${policy.hosts.deny.join(", ")}`);
|
|
6776
|
+
const extraDeny = policy.filesystem?.denyRead ?? [];
|
|
6777
|
+
if (extraDeny.length) lines2.push(`denied reads (extra): ${extraDeny.join(", ")}`);
|
|
6778
|
+
const allowRead = policy.filesystem?.allowRead ?? [];
|
|
6779
|
+
if (allowRead.length) lines2.push(`reads allowed back: ${allowRead.join(", ")}`);
|
|
6780
|
+
const allowWrite = policy.filesystem?.allowWrite ?? [];
|
|
6781
|
+
if (allowWrite.length) lines2.push(`writes allowed (extra): ${allowWrite.join(", ")}`);
|
|
6782
|
+
const env = Object.keys(policy.credentials?.env ?? {});
|
|
6783
|
+
const git = (policy.credentials?.git ?? []).map((g) => `git@${g.host}`);
|
|
6784
|
+
if (env.length || git.length) {
|
|
6785
|
+
lines2.push(`masked credentials: ${[...env, ...git].join(", ")}`);
|
|
6786
|
+
}
|
|
6787
|
+
return lines2;
|
|
6788
|
+
}
|
|
6789
|
+
var hostPattern, secretRef, envCredentialSchema, gitCredentialSchema, sandboxPolicySchema, DEFAULT_ALLOWED_HOSTS, ANTHROPIC_HOSTS, OPENAI_HOSTS, placeholderName, HarnessSandboxRefused;
|
|
6790
|
+
var init_sandbox_policy = __esm({
|
|
6791
|
+
"src/runner/sandbox-policy.ts"() {
|
|
6792
|
+
"use strict";
|
|
6793
|
+
hostPattern = z13.string().min(1);
|
|
6794
|
+
secretRef = z13.string().regex(/^\$\{[A-Za-z_][A-Za-z0-9_]*\}$/, "must be a ${NAME} reference into secrets.json");
|
|
6795
|
+
envCredentialSchema = z13.object({ secret: secretRef, hosts: z13.array(hostPattern).min(1) }).strict();
|
|
6796
|
+
gitCredentialSchema = z13.object({
|
|
6797
|
+
host: z13.string().regex(/^[A-Za-z0-9.-]+(:\d+)?$/, "must be a host name, like github.com"),
|
|
6798
|
+
username: z13.string().min(1).optional(),
|
|
6799
|
+
secret: secretRef
|
|
6800
|
+
}).strict();
|
|
6801
|
+
sandboxPolicySchema = z13.object({
|
|
6802
|
+
hosts: z13.object({
|
|
6803
|
+
allow: z13.array(hostPattern).optional(),
|
|
6804
|
+
deny: z13.array(hostPattern).optional(),
|
|
6805
|
+
defaults: z13.boolean().optional()
|
|
6806
|
+
}).strict().optional(),
|
|
6807
|
+
filesystem: z13.object({
|
|
6808
|
+
mode: z13.enum(["blocklist", "strict"]).optional(),
|
|
6809
|
+
denyRead: z13.array(z13.string().min(1)).optional(),
|
|
6810
|
+
allowRead: z13.array(z13.string().min(1)).optional(),
|
|
6811
|
+
allowWrite: z13.array(z13.string().min(1)).optional()
|
|
6812
|
+
}).strict().optional(),
|
|
6813
|
+
credentials: z13.object({
|
|
6814
|
+
env: z13.record(z13.string().regex(/^[A-Za-z_][A-Za-z0-9_]*$/), envCredentialSchema).optional(),
|
|
6815
|
+
git: z13.array(gitCredentialSchema).optional()
|
|
6816
|
+
}).strict().optional()
|
|
6817
|
+
}).strict();
|
|
6818
|
+
DEFAULT_ALLOWED_HOSTS = [
|
|
6819
|
+
"github.com",
|
|
6820
|
+
"*.github.com",
|
|
6821
|
+
"*.githubusercontent.com",
|
|
6822
|
+
"registry.npmjs.org",
|
|
6823
|
+
"pypi.org",
|
|
6824
|
+
"files.pythonhosted.org",
|
|
6825
|
+
"crates.io",
|
|
6826
|
+
"static.crates.io",
|
|
6827
|
+
"index.crates.io"
|
|
6828
|
+
];
|
|
6829
|
+
ANTHROPIC_HOSTS = ["api.anthropic.com", "*.anthropic.com", "claude.ai", "*.claude.ai"];
|
|
6830
|
+
OPENAI_HOSTS = ["api.openai.com", "auth.openai.com", "chatgpt.com", "*.chatgpt.com"];
|
|
6831
|
+
placeholderName = (ref) => ref.slice(2, -1);
|
|
6832
|
+
HarnessSandboxRefused = class extends Error {
|
|
6833
|
+
};
|
|
6834
|
+
}
|
|
6835
|
+
});
|
|
6836
|
+
|
|
6635
6837
|
// src/runner/opencode-lock.ts
|
|
6636
6838
|
function opencodeLockKey(serverUrl) {
|
|
6637
6839
|
return `opencode:${serverUrl}`;
|
|
@@ -6990,8 +7192,118 @@ var init_turn_containment = __esm({
|
|
|
6990
7192
|
}
|
|
6991
7193
|
});
|
|
6992
7194
|
|
|
7195
|
+
// src/secrets.ts
|
|
7196
|
+
import { existsSync as existsSync7, readFileSync as readFileSync6 } from "fs";
|
|
7197
|
+
import { join as join10 } from "path";
|
|
7198
|
+
import { z as z14 } from "zod";
|
|
7199
|
+
function secretsPath() {
|
|
7200
|
+
return join10(cabaneDir(), "secrets.json");
|
|
7201
|
+
}
|
|
7202
|
+
function loadSecretStore() {
|
|
7203
|
+
const path = secretsPath();
|
|
7204
|
+
if (!existsSync7(path)) return makeStore({});
|
|
7205
|
+
let raw;
|
|
7206
|
+
try {
|
|
7207
|
+
raw = readFileSync6(path, "utf8");
|
|
7208
|
+
} catch (err) {
|
|
7209
|
+
throw new ConfigError(
|
|
7210
|
+
`couldn't read ${path}: ${err instanceof Error ? err.message : String(err)}`
|
|
7211
|
+
);
|
|
7212
|
+
}
|
|
7213
|
+
if (raw.trim().length === 0) return makeStore({});
|
|
7214
|
+
let parsed;
|
|
7215
|
+
try {
|
|
7216
|
+
parsed = JSON.parse(raw);
|
|
7217
|
+
} catch (err) {
|
|
7218
|
+
throw new ConfigError(
|
|
7219
|
+
`${path} is not valid JSON: ${err instanceof Error ? err.message : String(err)}. It must be a flat object of "NAME": "value" secret pairs.`
|
|
7220
|
+
);
|
|
7221
|
+
}
|
|
7222
|
+
const result = secretStoreSchema.safeParse(parsed);
|
|
7223
|
+
if (!result.success) {
|
|
7224
|
+
throw new ConfigError(
|
|
7225
|
+
`${path} must be a flat object mapping secret names to string values (e.g. { "GITHUB_TOKEN": "ghp_\u2026" }).`
|
|
7226
|
+
);
|
|
7227
|
+
}
|
|
7228
|
+
return makeStore(result.data);
|
|
7229
|
+
}
|
|
7230
|
+
function loadSecretStoreTolerant(onWarn) {
|
|
7231
|
+
try {
|
|
7232
|
+
return loadSecretStore();
|
|
7233
|
+
} catch (err) {
|
|
7234
|
+
onWarn?.(err instanceof Error ? err.message : String(err));
|
|
7235
|
+
return makeStore({});
|
|
7236
|
+
}
|
|
7237
|
+
}
|
|
7238
|
+
function makeStore(map) {
|
|
7239
|
+
return {
|
|
7240
|
+
get: (name) => map[name],
|
|
7241
|
+
names: () => Object.keys(map).sort()
|
|
7242
|
+
};
|
|
7243
|
+
}
|
|
7244
|
+
function requiredSecretNames(mcpServers) {
|
|
7245
|
+
const names = /* @__PURE__ */ new Set();
|
|
7246
|
+
for (const server of Object.values(mcpServers ?? {})) {
|
|
7247
|
+
const bags = [
|
|
7248
|
+
"env" in server ? server.env : void 0,
|
|
7249
|
+
"headers" in server ? server.headers : void 0
|
|
7250
|
+
];
|
|
7251
|
+
for (const bag of bags) {
|
|
7252
|
+
for (const value of Object.values(bag ?? {})) {
|
|
7253
|
+
for (const [, name] of value.matchAll(PLACEHOLDER_RE)) names.add(name);
|
|
7254
|
+
}
|
|
7255
|
+
}
|
|
7256
|
+
}
|
|
7257
|
+
return [...names].sort();
|
|
7258
|
+
}
|
|
7259
|
+
function resolveMcpSecrets(mcpServers, store) {
|
|
7260
|
+
const missing = /* @__PURE__ */ new Set();
|
|
7261
|
+
const interpolate = (value) => value.replace(PLACEHOLDER_RE, (_match, name) => {
|
|
7262
|
+
const resolved = store.get(name);
|
|
7263
|
+
if (resolved === void 0) {
|
|
7264
|
+
missing.add(name);
|
|
7265
|
+
return _match;
|
|
7266
|
+
}
|
|
7267
|
+
return resolved;
|
|
7268
|
+
});
|
|
7269
|
+
const mapBag = (bag) => {
|
|
7270
|
+
const out = {};
|
|
7271
|
+
for (const [k, v] of Object.entries(bag)) out[k] = interpolate(v);
|
|
7272
|
+
return out;
|
|
7273
|
+
};
|
|
7274
|
+
const servers = {};
|
|
7275
|
+
for (const [name, server] of Object.entries(mcpServers)) {
|
|
7276
|
+
const next = { ...server };
|
|
7277
|
+
if ("env" in next && next.env) next.env = mapBag(next.env);
|
|
7278
|
+
if ("headers" in next && next.headers) next.headers = mapBag(next.headers);
|
|
7279
|
+
servers[name] = next;
|
|
7280
|
+
}
|
|
7281
|
+
return { servers, missing: [...missing].sort() };
|
|
7282
|
+
}
|
|
7283
|
+
function missingSecretReason(missing) {
|
|
7284
|
+
const prefix = "missing_secret: ";
|
|
7285
|
+
for (let shown = missing.length; shown >= 0; shown--) {
|
|
7286
|
+
const omitted = missing.length - shown;
|
|
7287
|
+
const parts = [...missing.slice(0, shown), ...omitted > 0 ? [`+${omitted}`] : []];
|
|
7288
|
+
const reason = prefix + parts.join(",");
|
|
7289
|
+
if (reason.length <= ERROR_REASON_MAX) return reason;
|
|
7290
|
+
}
|
|
7291
|
+
return `${prefix}+${missing.length}`;
|
|
7292
|
+
}
|
|
7293
|
+
var PLACEHOLDER_RE, secretStoreSchema, ERROR_REASON_MAX;
|
|
7294
|
+
var init_secrets = __esm({
|
|
7295
|
+
"src/secrets.ts"() {
|
|
7296
|
+
"use strict";
|
|
7297
|
+
init_config();
|
|
7298
|
+
init_errors();
|
|
7299
|
+
PLACEHOLDER_RE = /\$\{([A-Za-z_][A-Za-z0-9_]*)\}/g;
|
|
7300
|
+
secretStoreSchema = z14.record(z14.string(), z14.string());
|
|
7301
|
+
ERROR_REASON_MAX = 200;
|
|
7302
|
+
}
|
|
7303
|
+
});
|
|
7304
|
+
|
|
6993
7305
|
// src/run-config.ts
|
|
6994
|
-
import { z as
|
|
7306
|
+
import { z as z15 } from "zod";
|
|
6995
7307
|
function parseRunConfig(raw, onWarn) {
|
|
6996
7308
|
const result = runConfigSchema.safeParse(raw ?? {});
|
|
6997
7309
|
if (result.success) return result.data;
|
|
@@ -7004,38 +7316,38 @@ var mcpStdioServerSchema, mcpHttpServerSchema, mcpSseServerSchema, mcpServerDefS
|
|
|
7004
7316
|
var init_run_config = __esm({
|
|
7005
7317
|
"src/run-config.ts"() {
|
|
7006
7318
|
"use strict";
|
|
7007
|
-
mcpStdioServerSchema =
|
|
7008
|
-
type:
|
|
7009
|
-
command:
|
|
7010
|
-
args:
|
|
7011
|
-
env:
|
|
7319
|
+
mcpStdioServerSchema = z15.object({
|
|
7320
|
+
type: z15.literal("stdio").optional(),
|
|
7321
|
+
command: z15.string().min(1),
|
|
7322
|
+
args: z15.array(z15.string()).optional(),
|
|
7323
|
+
env: z15.record(z15.string(), z15.string()).optional()
|
|
7012
7324
|
});
|
|
7013
|
-
mcpHttpServerSchema =
|
|
7014
|
-
type:
|
|
7015
|
-
url:
|
|
7016
|
-
headers:
|
|
7325
|
+
mcpHttpServerSchema = z15.object({
|
|
7326
|
+
type: z15.literal("http"),
|
|
7327
|
+
url: z15.string().url(),
|
|
7328
|
+
headers: z15.record(z15.string(), z15.string()).optional()
|
|
7017
7329
|
});
|
|
7018
|
-
mcpSseServerSchema =
|
|
7019
|
-
type:
|
|
7020
|
-
url:
|
|
7021
|
-
headers:
|
|
7330
|
+
mcpSseServerSchema = z15.object({
|
|
7331
|
+
type: z15.literal("sse"),
|
|
7332
|
+
url: z15.string().url(),
|
|
7333
|
+
headers: z15.record(z15.string(), z15.string()).optional()
|
|
7022
7334
|
});
|
|
7023
|
-
mcpServerDefSchema =
|
|
7024
|
-
thinkingConfigSchema =
|
|
7025
|
-
|
|
7026
|
-
|
|
7027
|
-
|
|
7335
|
+
mcpServerDefSchema = z15.union([mcpHttpServerSchema, mcpSseServerSchema, mcpStdioServerSchema]);
|
|
7336
|
+
thinkingConfigSchema = z15.discriminatedUnion("type", [
|
|
7337
|
+
z15.object({ type: z15.literal("adaptive") }),
|
|
7338
|
+
z15.object({ type: z15.literal("enabled"), budgetTokens: z15.number().int().positive().optional() }),
|
|
7339
|
+
z15.object({ type: z15.literal("disabled") })
|
|
7028
7340
|
]);
|
|
7029
|
-
effortSchema =
|
|
7030
|
-
runConfigSchema =
|
|
7341
|
+
effortSchema = z15.enum(["low", "medium", "high", "xhigh", "max"]);
|
|
7342
|
+
runConfigSchema = z15.object({
|
|
7031
7343
|
// CT788: the host-access binary replaced the `assistant`/`coding`/`custom` mode
|
|
7032
7344
|
// trio + its custom tool lists — `true` grants the host filesystem/shell, absent
|
|
7033
7345
|
// is the locked surface. Kept in lockstep with `@cabane/shared`'s
|
|
7034
7346
|
// `agentRunConfigSchema` (independent zod, forward-compatible: unknown keys are
|
|
7035
7347
|
// stripped, so an older companion riding a newer server never rejects the config).
|
|
7036
|
-
hostAccess:
|
|
7037
|
-
mcpServers:
|
|
7038
|
-
model:
|
|
7348
|
+
hostAccess: z15.boolean().optional(),
|
|
7349
|
+
mcpServers: z15.record(z15.string(), mcpServerDefSchema).optional(),
|
|
7350
|
+
model: z15.string().min(1).optional(),
|
|
7039
7351
|
thinking: thinkingConfigSchema.optional(),
|
|
7040
7352
|
effort: effortSchema.optional()
|
|
7041
7353
|
});
|
|
@@ -7044,7 +7356,7 @@ var init_run_config = __esm({
|
|
|
7044
7356
|
});
|
|
7045
7357
|
|
|
7046
7358
|
// src/runner/protocol.ts
|
|
7047
|
-
import { z as
|
|
7359
|
+
import { z as z16 } from "zod";
|
|
7048
7360
|
function required(env, name) {
|
|
7049
7361
|
const value = env[name];
|
|
7050
7362
|
if (!value) throw new Error(`runner: ${name} is not set`);
|
|
@@ -7057,6 +7369,7 @@ function parseRunnerInput(env, stdin) {
|
|
|
7057
7369
|
return {
|
|
7058
7370
|
baseUrl: required(env, RUNNER_ENV.url),
|
|
7059
7371
|
credential: credentials.credential,
|
|
7372
|
+
sandboxCredentials: credentials.sandboxCredentials ?? [],
|
|
7060
7373
|
dispatch,
|
|
7061
7374
|
turnId: required(env, RUNNER_ENV.turnId),
|
|
7062
7375
|
resumed: env[RUNNER_ENV.resumed] === "1",
|
|
@@ -7073,7 +7386,10 @@ function runnerEnv(input) {
|
|
|
7073
7386
|
};
|
|
7074
7387
|
}
|
|
7075
7388
|
function runnerStdin(input) {
|
|
7076
|
-
return `${JSON.stringify({
|
|
7389
|
+
return `${JSON.stringify({
|
|
7390
|
+
credential: input.credential,
|
|
7391
|
+
...input.sandboxCredentials?.length ? { sandboxCredentials: input.sandboxCredentials } : {}
|
|
7392
|
+
})}
|
|
7077
7393
|
`;
|
|
7078
7394
|
}
|
|
7079
7395
|
function encodeLauncherMessage(message) {
|
|
@@ -7109,6 +7425,7 @@ var init_protocol = __esm({
|
|
|
7109
7425
|
"use strict";
|
|
7110
7426
|
init_config();
|
|
7111
7427
|
init_run_config();
|
|
7428
|
+
init_sandbox_policy();
|
|
7112
7429
|
RUNNER_ENV = {
|
|
7113
7430
|
// The wire the runner speaks.
|
|
7114
7431
|
url: "CABANE_URL",
|
|
@@ -7122,56 +7439,75 @@ var init_protocol = __esm({
|
|
|
7122
7439
|
// Everything else the turn is constructed from, as JSON: see `RunnerSpec`.
|
|
7123
7440
|
spec: "CABANE_RUNNER_SPEC"
|
|
7124
7441
|
};
|
|
7125
|
-
runtimeHealthSchema =
|
|
7126
|
-
quotaState:
|
|
7127
|
-
limitedUntil:
|
|
7128
|
-
lastError:
|
|
7442
|
+
runtimeHealthSchema = z16.object({
|
|
7443
|
+
quotaState: z16.enum(["ok", "limited", "auth_expired", "unknown"]),
|
|
7444
|
+
limitedUntil: z16.string().nullable(),
|
|
7445
|
+
lastError: z16.string().nullable()
|
|
7129
7446
|
});
|
|
7130
|
-
runnerSpecSchema =
|
|
7131
|
-
deviceId:
|
|
7447
|
+
runnerSpecSchema = z16.object({
|
|
7448
|
+
deviceId: z16.string().optional(),
|
|
7132
7449
|
// The launcher's boot id: the one its heartbeat carries, so a runner's lease
|
|
7133
7450
|
// matches the device that holds it.
|
|
7134
|
-
bootId:
|
|
7135
|
-
workspaceId:
|
|
7136
|
-
workspaceSlug:
|
|
7137
|
-
agentId:
|
|
7138
|
-
agentUsername:
|
|
7139
|
-
agentName:
|
|
7451
|
+
bootId: z16.string(),
|
|
7452
|
+
workspaceId: z16.string(),
|
|
7453
|
+
workspaceSlug: z16.string(),
|
|
7454
|
+
agentId: z16.string(),
|
|
7455
|
+
agentUsername: z16.string(),
|
|
7456
|
+
agentName: z16.string(),
|
|
7140
7457
|
local: localAgentConfigSchema,
|
|
7141
7458
|
runConfig: runConfigSchema,
|
|
7142
|
-
opencodeServerUrl:
|
|
7143
|
-
claudeCodeAvailable:
|
|
7144
|
-
codexAvailable:
|
|
7145
|
-
idleTimeoutMs:
|
|
7146
|
-
totalTimeoutMs:
|
|
7147
|
-
leaseRenewalMs:
|
|
7148
|
-
preparingRowDelayMs:
|
|
7459
|
+
opencodeServerUrl: z16.string().optional(),
|
|
7460
|
+
claudeCodeAvailable: z16.boolean(),
|
|
7461
|
+
codexAvailable: z16.boolean(),
|
|
7462
|
+
idleTimeoutMs: z16.number().int().positive().optional(),
|
|
7463
|
+
totalTimeoutMs: z16.number().int().positive().optional(),
|
|
7464
|
+
leaseRenewalMs: z16.number().int().positive().optional(),
|
|
7465
|
+
preparingRowDelayMs: z16.number().int().nonnegative().optional(),
|
|
7149
7466
|
// Whether the turn writes a transcript. The directory is the runner's own
|
|
7150
7467
|
// (`transcriptsDir()`), so `CABANE_RUNNER_STATE_DIR` moves it with the outbox.
|
|
7151
|
-
transcripts:
|
|
7468
|
+
transcripts: z16.boolean(),
|
|
7152
7469
|
// Whether the harness starts under the OS sandbox. The launcher sends 'os'
|
|
7153
7470
|
// only when this host can make one.
|
|
7154
|
-
sandbox:
|
|
7155
|
-
|
|
7471
|
+
sandbox: z16.enum(["os", "none"]),
|
|
7472
|
+
// CT1684: the device's sandbox policy, validated by the launcher. Absent
|
|
7473
|
+
// when the sandbox is off.
|
|
7474
|
+
sandboxPolicy: sandboxPolicySchema.optional(),
|
|
7475
|
+
logLevel: z16.string(),
|
|
7156
7476
|
// The launcher's connector-health observations, by runtime, as of the start.
|
|
7157
|
-
connectorHealth:
|
|
7477
|
+
connectorHealth: z16.record(z16.string(), runtimeHealthSchema),
|
|
7158
7478
|
// True when this (conversation, agent) pair was already told its session
|
|
7159
7479
|
// could not be saved: the notice is once per pair, not once per process.
|
|
7160
|
-
sessionWriteNoticeClaimed:
|
|
7480
|
+
sessionWriteNoticeClaimed: z16.boolean(),
|
|
7161
7481
|
// CT1680: the runner's identity, minted with its credential. The start
|
|
7162
7482
|
// PATCH carries it so the lease knows which runner holds the span.
|
|
7163
|
-
runnerId:
|
|
7483
|
+
runnerId: z16.string(),
|
|
7484
|
+
// CT1681: the launcher keeps the runtime's session files beside the attempt
|
|
7485
|
+
// and restores them into this runner's home, so the runner names the attempt
|
|
7486
|
+
// each context was composed for and waits for the launcher to say it's in
|
|
7487
|
+
// place. Absent: nothing to wait for (a runner on the launcher's own disk).
|
|
7488
|
+
attemptState: z16.boolean().optional(),
|
|
7489
|
+
// CT1681: MCP `${NAME}` secrets come from the launcher, by name, instead of
|
|
7490
|
+
// this machine's secrets.json. Absent: the runner reads the device's file.
|
|
7491
|
+
secretsFromLauncher: z16.boolean().optional()
|
|
7164
7492
|
});
|
|
7165
|
-
dispatchSchema =
|
|
7166
|
-
type:
|
|
7167
|
-
agentId:
|
|
7168
|
-
deviceId:
|
|
7169
|
-
conversationId:
|
|
7170
|
-
messageId:
|
|
7171
|
-
episode:
|
|
7493
|
+
dispatchSchema = z16.object({
|
|
7494
|
+
type: z16.literal("device:dispatch_requested"),
|
|
7495
|
+
agentId: z16.string(),
|
|
7496
|
+
deviceId: z16.string(),
|
|
7497
|
+
conversationId: z16.string(),
|
|
7498
|
+
messageId: z16.string(),
|
|
7499
|
+
episode: z16.number().int().optional()
|
|
7172
7500
|
});
|
|
7173
|
-
credentialsSchema =
|
|
7174
|
-
credential:
|
|
7501
|
+
credentialsSchema = z16.object({
|
|
7502
|
+
credential: z16.string().startsWith("cabrn_"),
|
|
7503
|
+
sandboxCredentials: z16.array(
|
|
7504
|
+
z16.object({
|
|
7505
|
+
name: z16.string(),
|
|
7506
|
+
value: z16.string(),
|
|
7507
|
+
hosts: z16.array(z16.string()),
|
|
7508
|
+
gitHost: z16.string().optional()
|
|
7509
|
+
})
|
|
7510
|
+
).optional()
|
|
7175
7511
|
});
|
|
7176
7512
|
}
|
|
7177
7513
|
});
|
|
@@ -7183,6 +7519,11 @@ function selfRunnerCommand() {
|
|
|
7183
7519
|
if (!script) throw new Error("runners: cannot tell which script started this companion");
|
|
7184
7520
|
return [process.execPath, ...process.execArgv, script, "run-turn"];
|
|
7185
7521
|
}
|
|
7522
|
+
function defaultSecretStore(log) {
|
|
7523
|
+
return loadSecretStoreTolerant(
|
|
7524
|
+
(m) => log.warn({ err: m }, "Could not read the secrets file. Check ~/.cabane/secrets.json.")
|
|
7525
|
+
);
|
|
7526
|
+
}
|
|
7186
7527
|
function healthSnapshot(store) {
|
|
7187
7528
|
const out = {};
|
|
7188
7529
|
for (const r of store?.reports() ?? []) {
|
|
@@ -7216,6 +7557,9 @@ function runnerSpec(opts, launch, duties, runnerId) {
|
|
|
7216
7557
|
...opts.preparingRowDelayMs !== void 0 ? { preparingRowDelayMs: opts.preparingRowDelayMs } : {},
|
|
7217
7558
|
transcripts: opts.transcriptDir !== void 0,
|
|
7218
7559
|
sandbox: launch.sandbox ?? "none",
|
|
7560
|
+
...launch.provider.bindAttempt ? { attemptState: true } : {},
|
|
7561
|
+
...launch.provider.inheritsEnvironment === false ? { secretsFromLauncher: true } : {},
|
|
7562
|
+
...launch.sandbox === "os" && launch.policy?.ok ? { sandboxPolicy: launch.policy.policy } : {},
|
|
7219
7563
|
logLevel: opts.log.level,
|
|
7220
7564
|
connectorHealth: healthSnapshot(launch.connectorHealth),
|
|
7221
7565
|
sessionWriteNoticeClaimed: duties.sessionWriteNoticeClaimed()
|
|
@@ -7314,6 +7658,32 @@ async function settleUnstarted(opts, payload, turnId, errorReason, log) {
|
|
|
7314
7658
|
);
|
|
7315
7659
|
}
|
|
7316
7660
|
}
|
|
7661
|
+
function sandboxInputs(launch, log) {
|
|
7662
|
+
const policy = launch.policy ?? { ok: true, policy: {} };
|
|
7663
|
+
if (launch.sandbox !== "os") {
|
|
7664
|
+
if (policy.ok && policyHasCredentials(policy.policy)) {
|
|
7665
|
+
log.debug(
|
|
7666
|
+
"runners: runners.policy names credentials, but turns here run without the OS sandbox; none are given to the harness"
|
|
7667
|
+
);
|
|
7668
|
+
}
|
|
7669
|
+
return { ok: true, credentials: [] };
|
|
7670
|
+
}
|
|
7671
|
+
if (!policy.ok) {
|
|
7672
|
+
log.warn({ error: policy.error }, "runners: the sandbox policy is invalid; refusing the turn");
|
|
7673
|
+
return { ok: false, reason: `setup_failed: ${policy.error}`.slice(0, 200) };
|
|
7674
|
+
}
|
|
7675
|
+
if (!policyHasCredentials(policy.policy)) return { ok: true, credentials: [] };
|
|
7676
|
+
const store = (launch.secretStore ?? defaultSecretStore)(log);
|
|
7677
|
+
const { credentials, missing } = resolvePolicyCredentials(policy.policy, store);
|
|
7678
|
+
if (missing.length) {
|
|
7679
|
+
log.debug(
|
|
7680
|
+
{ missing },
|
|
7681
|
+
"runners: the sandbox policy names secrets this device does not declare"
|
|
7682
|
+
);
|
|
7683
|
+
return { ok: false, reason: missingSecretReason(missing) };
|
|
7684
|
+
}
|
|
7685
|
+
return { ok: true, credentials };
|
|
7686
|
+
}
|
|
7317
7687
|
async function runTurnInRunner(opts, launch, duties, payload, handleOpts) {
|
|
7318
7688
|
const startedAt = Date.now();
|
|
7319
7689
|
const { turnId } = handleOpts;
|
|
@@ -7337,7 +7707,8 @@ async function runTurnInRunner(opts, launch, duties, payload, handleOpts) {
|
|
|
7337
7707
|
agentId: payload.agentId,
|
|
7338
7708
|
turnId
|
|
7339
7709
|
};
|
|
7340
|
-
const
|
|
7710
|
+
const sandboxed = sandboxInputs(launch, log);
|
|
7711
|
+
const minted = sandboxed.ok ? await mintForRunner(opts, payload, handleOpts, controller.signal, log) : { ok: false, reason: sandboxed.reason, errorReason: sandboxed.reason };
|
|
7341
7712
|
if (!minted.ok) {
|
|
7342
7713
|
controller.signal.removeEventListener("abort", forwardStop);
|
|
7343
7714
|
duties.releaseAbort(turnId, controller);
|
|
@@ -7355,15 +7726,42 @@ async function runTurnInRunner(opts, launch, duties, payload, handleOpts) {
|
|
|
7355
7726
|
const remote = [];
|
|
7356
7727
|
const lockOwner = (id) => `${turnId}#${id}`;
|
|
7357
7728
|
const locks = launch.locks;
|
|
7729
|
+
let leasedAttemptId = null;
|
|
7358
7730
|
try {
|
|
7359
7731
|
handle = await launch.provider.acquire(attempt);
|
|
7732
|
+
} catch (err) {
|
|
7733
|
+
const reason2 = `runner_unavailable: ${describeError(err)}`;
|
|
7734
|
+
log.warn(
|
|
7735
|
+
{ turnId, provider: launch.provider.name, err: describeError(err) },
|
|
7736
|
+
"runner: no place to run the turn"
|
|
7737
|
+
);
|
|
7738
|
+
controller.signal.removeEventListener("abort", forwardStop);
|
|
7739
|
+
duties.releaseAbort(turnId, controller);
|
|
7740
|
+
await settleUnstarted(opts, payload, turnId, reason2, log);
|
|
7741
|
+
await launch.reportRunnerGone?.({
|
|
7742
|
+
workspaceId: opts.workspaceId,
|
|
7743
|
+
conversationId: payload.conversationId,
|
|
7744
|
+
agentId: payload.agentId,
|
|
7745
|
+
messageId: payload.messageId,
|
|
7746
|
+
runnerId: minted.runnerId,
|
|
7747
|
+
reason: "provider_failed"
|
|
7748
|
+
});
|
|
7749
|
+
const durationMs2 = Date.now() - startedAt;
|
|
7750
|
+
duties.notifyEnd({ id: payload.messageId, ok: false, durationMs: durationMs2, reason: reason2 });
|
|
7751
|
+
return { ok: false, durationMs: durationMs2, reason: reason2 };
|
|
7752
|
+
}
|
|
7753
|
+
try {
|
|
7754
|
+
const place = handle;
|
|
7360
7755
|
const env = {};
|
|
7361
|
-
|
|
7362
|
-
|
|
7756
|
+
if (launch.provider.inheritsEnvironment !== false) {
|
|
7757
|
+
for (const [k, v] of Object.entries(launch.env ?? process.env)) {
|
|
7758
|
+
if (v !== void 0) env[k] = v;
|
|
7759
|
+
}
|
|
7363
7760
|
}
|
|
7364
7761
|
const input = {
|
|
7365
|
-
baseUrl: opts.baseUrl,
|
|
7762
|
+
baseUrl: launch.runnerBaseUrl ?? opts.baseUrl,
|
|
7366
7763
|
credential: minted.credential,
|
|
7764
|
+
sandboxCredentials: sandboxed.ok ? sandboxed.credentials : [],
|
|
7367
7765
|
dispatch: payload,
|
|
7368
7766
|
turnId,
|
|
7369
7767
|
resumed: handleOpts.resumed,
|
|
@@ -7440,6 +7838,38 @@ async function runTurnInRunner(opts, launch, duties, payload, handleOpts) {
|
|
|
7440
7838
|
case "remote_session":
|
|
7441
7839
|
remote.push({ serverUrl: event.serverUrl, sessionId: event.sessionId });
|
|
7442
7840
|
return;
|
|
7841
|
+
case "attempt": {
|
|
7842
|
+
const bind = launch.provider.bindAttempt?.(place, event.attemptId) ?? Promise.resolve();
|
|
7843
|
+
void bind.then(
|
|
7844
|
+
() => running.stdin.write(encodeLauncherMessage({ cabaneLauncher: "bound", id: event.id })),
|
|
7845
|
+
(err) => {
|
|
7846
|
+
const refused = describeError(err);
|
|
7847
|
+
log.warn(
|
|
7848
|
+
{ attemptId: event.attemptId, err: refused },
|
|
7849
|
+
"runner: couldn't make the place ready for the turn's attempt; not running it there"
|
|
7850
|
+
);
|
|
7851
|
+
running.stdin.write(
|
|
7852
|
+
encodeLauncherMessage({ cabaneLauncher: "bound", id: event.id, refused })
|
|
7853
|
+
);
|
|
7854
|
+
}
|
|
7855
|
+
);
|
|
7856
|
+
return;
|
|
7857
|
+
}
|
|
7858
|
+
case "leased":
|
|
7859
|
+
leasedAttemptId = event.attemptId;
|
|
7860
|
+
return;
|
|
7861
|
+
case "secrets": {
|
|
7862
|
+
const store = (launch.secretStore ?? defaultSecretStore)(log);
|
|
7863
|
+
const values = {};
|
|
7864
|
+
for (const name of event.names) {
|
|
7865
|
+
const value = store.get(name);
|
|
7866
|
+
if (value !== void 0) values[name] = value;
|
|
7867
|
+
}
|
|
7868
|
+
running.stdin.write(
|
|
7869
|
+
encodeLauncherMessage({ cabaneLauncher: "secrets", id: event.id, values })
|
|
7870
|
+
);
|
|
7871
|
+
return;
|
|
7872
|
+
}
|
|
7443
7873
|
}
|
|
7444
7874
|
});
|
|
7445
7875
|
const stderrDone = lines(running.stderr, (line) => relayRunnerLog(line, opts.log));
|
|
@@ -7450,7 +7880,14 @@ async function runTurnInRunner(opts, launch, duties, payload, handleOpts) {
|
|
|
7450
7880
|
child?.stdin.end();
|
|
7451
7881
|
controller.signal.removeEventListener("abort", forwardStop);
|
|
7452
7882
|
duties.releaseAbort(turnId, controller);
|
|
7453
|
-
if (handle)
|
|
7883
|
+
if (handle) {
|
|
7884
|
+
await launch.provider.collect?.(handle, { attemptId: leasedAttemptId }).catch(
|
|
7885
|
+
(err) => log.warn({ err: describeError(err) }, "runner: couldn't collect what the runner left")
|
|
7886
|
+
);
|
|
7887
|
+
await launch.provider.release(handle, { destroy: false }).catch(
|
|
7888
|
+
(err) => log.warn({ err: describeError(err) }, "runner: couldn't give its place back")
|
|
7889
|
+
);
|
|
7890
|
+
}
|
|
7454
7891
|
}
|
|
7455
7892
|
if (result) return result;
|
|
7456
7893
|
const durationMs = Date.now() - startedAt;
|
|
@@ -7496,11 +7933,13 @@ var PINO_LEVELS;
|
|
|
7496
7933
|
var init_launch = __esm({
|
|
7497
7934
|
"src/runner/launch.ts"() {
|
|
7498
7935
|
"use strict";
|
|
7936
|
+
init_sandbox_policy();
|
|
7499
7937
|
init_opencode_lock();
|
|
7500
7938
|
init_api_error_shape();
|
|
7501
7939
|
init_errors();
|
|
7502
7940
|
init_boot_id();
|
|
7503
7941
|
init_turn_containment();
|
|
7942
|
+
init_secrets();
|
|
7504
7943
|
init_protocol();
|
|
7505
7944
|
PINO_LEVELS = {
|
|
7506
7945
|
10: "trace",
|
|
@@ -7515,7 +7954,7 @@ var init_launch = __esm({
|
|
|
7515
7954
|
|
|
7516
7955
|
// src/turn-control-tools.ts
|
|
7517
7956
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
7518
|
-
import { z as
|
|
7957
|
+
import { z as z17 } from "zod";
|
|
7519
7958
|
function createSkipState() {
|
|
7520
7959
|
return { skipped: false, reason: null };
|
|
7521
7960
|
}
|
|
@@ -7566,7 +8005,7 @@ function createTurnControlMcpServer(acts, skipState) {
|
|
|
7566
8005
|
SKIP_TURN_TOOL,
|
|
7567
8006
|
TURN_CONTROL_TOOL_DESCRIPTIONS.skip_turn,
|
|
7568
8007
|
{
|
|
7569
|
-
reason:
|
|
8008
|
+
reason: z17.string().min(1).max(MAX_SKIP_REASON_BYTES).describe(TURN_CONTROL_FIELD_DESCRIPTIONS.skip_turn.reason)
|
|
7570
8009
|
},
|
|
7571
8010
|
async (args) => {
|
|
7572
8011
|
skipState.skipped = true;
|
|
@@ -7582,42 +8021,42 @@ function createTurnControlMcpServer(acts, skipState) {
|
|
|
7582
8021
|
ASK_TOOL,
|
|
7583
8022
|
TURN_CONTROL_TOOL_DESCRIPTIONS.ask,
|
|
7584
8023
|
{
|
|
7585
|
-
targetUserId:
|
|
7586
|
-
question:
|
|
7587
|
-
headline:
|
|
7588
|
-
options:
|
|
7589
|
-
|
|
7590
|
-
|
|
7591
|
-
|
|
7592
|
-
label:
|
|
7593
|
-
move:
|
|
7594
|
-
task:
|
|
7595
|
-
status:
|
|
8024
|
+
targetUserId: z17.string().uuid().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask.targetUserId),
|
|
8025
|
+
question: z17.string().min(1).max(MAX_ASK_QUESTION_BYTES).optional().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask.question),
|
|
8026
|
+
headline: z17.string().min(1).max(MAX_ASK_HEADLINE_BYTES).optional().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask.headline),
|
|
8027
|
+
options: z17.array(
|
|
8028
|
+
z17.union([
|
|
8029
|
+
z17.string().min(1).max(MAX_ASK_OPTION_BYTES),
|
|
8030
|
+
z17.object({
|
|
8031
|
+
label: z17.string().min(1).max(MAX_ASK_OPTION_BYTES).describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask["options.label"]),
|
|
8032
|
+
move: z17.object({
|
|
8033
|
+
task: z17.string().min(1).max(200).describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask["options.move.task"]),
|
|
8034
|
+
status: z17.enum(TASK_STAGES).describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask["options.move.status"])
|
|
7596
8035
|
}).describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask["options.move"])
|
|
7597
8036
|
})
|
|
7598
8037
|
])
|
|
7599
8038
|
).min(MIN_ASK_OPTIONS).max(MAX_ASK_OPTIONS).optional().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask.options),
|
|
7600
|
-
questions:
|
|
7601
|
-
|
|
7602
|
-
headline:
|
|
7603
|
-
body:
|
|
7604
|
-
options:
|
|
7605
|
-
|
|
7606
|
-
|
|
7607
|
-
|
|
7608
|
-
label:
|
|
7609
|
-
move:
|
|
7610
|
-
task:
|
|
7611
|
-
status:
|
|
8039
|
+
questions: z17.array(
|
|
8040
|
+
z17.object({
|
|
8041
|
+
headline: z17.string().min(1).max(MAX_ASK_HEADLINE_BYTES).describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask["questions.headline"]),
|
|
8042
|
+
body: z17.string().min(1).max(MAX_ASK_QUESTION_BYTES).optional().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask["questions.body"]),
|
|
8043
|
+
options: z17.array(
|
|
8044
|
+
z17.union([
|
|
8045
|
+
z17.string().min(1).max(MAX_ASK_OPTION_BYTES),
|
|
8046
|
+
z17.object({
|
|
8047
|
+
label: z17.string().min(1).max(MAX_ASK_OPTION_BYTES).describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask["questions.options.label"]),
|
|
8048
|
+
move: z17.object({
|
|
8049
|
+
task: z17.string().min(1).max(200).describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask["questions.options.move.task"]),
|
|
8050
|
+
status: z17.enum(TASK_STAGES).describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask["questions.options.move.status"])
|
|
7612
8051
|
}).describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask["questions.options.move"])
|
|
7613
8052
|
})
|
|
7614
8053
|
])
|
|
7615
8054
|
).min(MIN_ASK_OPTIONS).max(MAX_ASK_OPTIONS).optional().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask["questions.options"])
|
|
7616
8055
|
})
|
|
7617
8056
|
).min(1).max(MAX_ASK_ITEMS).optional().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask.questions),
|
|
7618
|
-
replaces:
|
|
7619
|
-
askId:
|
|
7620
|
-
reason:
|
|
8057
|
+
replaces: z17.object({
|
|
8058
|
+
askId: z17.string().uuid().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask["replaces.askId"]),
|
|
8059
|
+
reason: z17.string().min(1).max(MAX_WITHDRAW_REASON_BYTES).describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask["replaces.reason"])
|
|
7621
8060
|
}).optional().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask.replaces)
|
|
7622
8061
|
},
|
|
7623
8062
|
async (args) => {
|
|
@@ -7658,9 +8097,9 @@ function createTurnControlMcpServer(acts, skipState) {
|
|
|
7658
8097
|
WAKE_ME_TOOL,
|
|
7659
8098
|
TURN_CONTROL_TOOL_DESCRIPTIONS.wake_me,
|
|
7660
8099
|
{
|
|
7661
|
-
afterSeconds:
|
|
7662
|
-
at:
|
|
7663
|
-
note:
|
|
8100
|
+
afterSeconds: z17.number().int().positive().optional().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.wake_me.afterSeconds),
|
|
8101
|
+
at: z17.string().datetime({ offset: true }).optional().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.wake_me.at),
|
|
8102
|
+
note: z17.string().min(1).max(MAX_WAKE_NOTE_BYTES).describe(TURN_CONTROL_FIELD_DESCRIPTIONS.wake_me.note)
|
|
7664
8103
|
},
|
|
7665
8104
|
async (args) => {
|
|
7666
8105
|
const hasAfter = args.afterSeconds !== void 0;
|
|
@@ -7694,8 +8133,8 @@ function createTurnControlMcpServer(acts, skipState) {
|
|
|
7694
8133
|
WITHDRAW_ASK_TOOL,
|
|
7695
8134
|
TURN_CONTROL_TOOL_DESCRIPTIONS.withdraw_ask,
|
|
7696
8135
|
{
|
|
7697
|
-
askId:
|
|
7698
|
-
reason:
|
|
8136
|
+
askId: z17.string().uuid().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.withdraw_ask.askId),
|
|
8137
|
+
reason: z17.string().min(1).max(MAX_WITHDRAW_REASON_BYTES).describe(TURN_CONTROL_FIELD_DESCRIPTIONS.withdraw_ask.reason)
|
|
7699
8138
|
},
|
|
7700
8139
|
async (args) => performAct(acts, {
|
|
7701
8140
|
kind: "withdraw_ask",
|
|
@@ -7790,10 +8229,10 @@ var init_build_options = __esm({
|
|
|
7790
8229
|
// src/codex-instructions.ts
|
|
7791
8230
|
import { mkdtemp, rm, writeFile } from "fs/promises";
|
|
7792
8231
|
import { tmpdir } from "os";
|
|
7793
|
-
import { join as
|
|
8232
|
+
import { join as join11 } from "path";
|
|
7794
8233
|
async function writeCodexInstructionsFile(contents) {
|
|
7795
|
-
const dir2 = await mkdtemp(
|
|
7796
|
-
const path =
|
|
8234
|
+
const dir2 = await mkdtemp(join11(tmpdir(), PREFIX));
|
|
8235
|
+
const path = join11(dir2, "instructions.md");
|
|
7797
8236
|
await writeFile(path, contents, { encoding: "utf8", mode: 384 });
|
|
7798
8237
|
return {
|
|
7799
8238
|
path,
|
|
@@ -7917,22 +8356,22 @@ var init_prepare_failure = __esm({
|
|
|
7917
8356
|
});
|
|
7918
8357
|
|
|
7919
8358
|
// src/prepared.ts
|
|
7920
|
-
import { mkdirSync as mkdirSync7, readFileSync as
|
|
7921
|
-
import { join as
|
|
8359
|
+
import { mkdirSync as mkdirSync7, readFileSync as readFileSync7, rmSync as rmSync5, writeFileSync as writeFileSync5, existsSync as existsSync8 } from "fs";
|
|
8360
|
+
import { join as join12 } from "path";
|
|
7922
8361
|
function dirFor(workspaceId) {
|
|
7923
|
-
return
|
|
8362
|
+
return join12(cabaneDir(), "prepared", encodeURIComponent(workspaceId));
|
|
7924
8363
|
}
|
|
7925
8364
|
function conversationDir(workspaceId, conversationId) {
|
|
7926
|
-
return
|
|
8365
|
+
return join12(dirFor(workspaceId), encodeURIComponent(conversationId));
|
|
7927
8366
|
}
|
|
7928
8367
|
function pathFor2(workspaceId, conversationId, agentId) {
|
|
7929
|
-
return
|
|
8368
|
+
return join12(conversationDir(workspaceId, conversationId), `${encodeURIComponent(agentId)}.json`);
|
|
7930
8369
|
}
|
|
7931
8370
|
function readPrepared(workspaceId, conversationId, agentId) {
|
|
7932
8371
|
const path = pathFor2(workspaceId, conversationId, agentId);
|
|
7933
|
-
if (!
|
|
8372
|
+
if (!existsSync8(path)) return null;
|
|
7934
8373
|
try {
|
|
7935
|
-
const parsed = JSON.parse(
|
|
8374
|
+
const parsed = JSON.parse(readFileSync7(path, "utf8"));
|
|
7936
8375
|
if (parsed && typeof parsed.cwd === "string" && parsed.cwd.length > 0) {
|
|
7937
8376
|
return {
|
|
7938
8377
|
cwd: parsed.cwd,
|
|
@@ -7962,105 +8401,6 @@ var init_prepared = __esm({
|
|
|
7962
8401
|
}
|
|
7963
8402
|
});
|
|
7964
8403
|
|
|
7965
|
-
// src/secrets.ts
|
|
7966
|
-
import { existsSync as existsSync8, readFileSync as readFileSync7 } from "fs";
|
|
7967
|
-
import { join as join12 } from "path";
|
|
7968
|
-
import { z as z16 } from "zod";
|
|
7969
|
-
function secretsPath() {
|
|
7970
|
-
return join12(cabaneDir(), "secrets.json");
|
|
7971
|
-
}
|
|
7972
|
-
function loadSecretStore() {
|
|
7973
|
-
const path = secretsPath();
|
|
7974
|
-
if (!existsSync8(path)) return makeStore({});
|
|
7975
|
-
let raw;
|
|
7976
|
-
try {
|
|
7977
|
-
raw = readFileSync7(path, "utf8");
|
|
7978
|
-
} catch (err) {
|
|
7979
|
-
throw new ConfigError(
|
|
7980
|
-
`couldn't read ${path}: ${err instanceof Error ? err.message : String(err)}`
|
|
7981
|
-
);
|
|
7982
|
-
}
|
|
7983
|
-
if (raw.trim().length === 0) return makeStore({});
|
|
7984
|
-
let parsed;
|
|
7985
|
-
try {
|
|
7986
|
-
parsed = JSON.parse(raw);
|
|
7987
|
-
} catch (err) {
|
|
7988
|
-
throw new ConfigError(
|
|
7989
|
-
`${path} is not valid JSON: ${err instanceof Error ? err.message : String(err)}. It must be a flat object of "NAME": "value" secret pairs.`
|
|
7990
|
-
);
|
|
7991
|
-
}
|
|
7992
|
-
const result = secretStoreSchema.safeParse(parsed);
|
|
7993
|
-
if (!result.success) {
|
|
7994
|
-
throw new ConfigError(
|
|
7995
|
-
`${path} must be a flat object mapping secret names to string values (e.g. { "GITHUB_TOKEN": "ghp_\u2026" }).`
|
|
7996
|
-
);
|
|
7997
|
-
}
|
|
7998
|
-
return makeStore(result.data);
|
|
7999
|
-
}
|
|
8000
|
-
function loadSecretStoreTolerant(onWarn) {
|
|
8001
|
-
try {
|
|
8002
|
-
return loadSecretStore();
|
|
8003
|
-
} catch (err) {
|
|
8004
|
-
onWarn?.(err instanceof Error ? err.message : String(err));
|
|
8005
|
-
return makeStore({});
|
|
8006
|
-
}
|
|
8007
|
-
}
|
|
8008
|
-
function makeStore(map) {
|
|
8009
|
-
return {
|
|
8010
|
-
get: (name) => map[name],
|
|
8011
|
-
names: () => Object.keys(map).sort()
|
|
8012
|
-
};
|
|
8013
|
-
}
|
|
8014
|
-
function requiredSecretNames(mcpServers) {
|
|
8015
|
-
const names = /* @__PURE__ */ new Set();
|
|
8016
|
-
for (const server of Object.values(mcpServers ?? {})) {
|
|
8017
|
-
const bags = [
|
|
8018
|
-
"env" in server ? server.env : void 0,
|
|
8019
|
-
"headers" in server ? server.headers : void 0
|
|
8020
|
-
];
|
|
8021
|
-
for (const bag of bags) {
|
|
8022
|
-
for (const value of Object.values(bag ?? {})) {
|
|
8023
|
-
for (const [, name] of value.matchAll(PLACEHOLDER_RE)) names.add(name);
|
|
8024
|
-
}
|
|
8025
|
-
}
|
|
8026
|
-
}
|
|
8027
|
-
return [...names].sort();
|
|
8028
|
-
}
|
|
8029
|
-
function resolveMcpSecrets(mcpServers, store) {
|
|
8030
|
-
const missing = /* @__PURE__ */ new Set();
|
|
8031
|
-
const interpolate = (value) => value.replace(PLACEHOLDER_RE, (_match, name) => {
|
|
8032
|
-
const resolved = store.get(name);
|
|
8033
|
-
if (resolved === void 0) {
|
|
8034
|
-
missing.add(name);
|
|
8035
|
-
return _match;
|
|
8036
|
-
}
|
|
8037
|
-
return resolved;
|
|
8038
|
-
});
|
|
8039
|
-
const mapBag = (bag) => {
|
|
8040
|
-
const out = {};
|
|
8041
|
-
for (const [k, v] of Object.entries(bag)) out[k] = interpolate(v);
|
|
8042
|
-
return out;
|
|
8043
|
-
};
|
|
8044
|
-
const servers = {};
|
|
8045
|
-
for (const [name, server] of Object.entries(mcpServers)) {
|
|
8046
|
-
const next = { ...server };
|
|
8047
|
-
if ("env" in next && next.env) next.env = mapBag(next.env);
|
|
8048
|
-
if ("headers" in next && next.headers) next.headers = mapBag(next.headers);
|
|
8049
|
-
servers[name] = next;
|
|
8050
|
-
}
|
|
8051
|
-
return { servers, missing: [...missing].sort() };
|
|
8052
|
-
}
|
|
8053
|
-
var PLACEHOLDER_RE, secretStoreSchema;
|
|
8054
|
-
var init_secrets = __esm({
|
|
8055
|
-
"src/secrets.ts"() {
|
|
8056
|
-
"use strict";
|
|
8057
|
-
init_config();
|
|
8058
|
-
init_errors();
|
|
8059
|
-
PLACEHOLDER_RE = /\$\{([A-Za-z_][A-Za-z0-9_]*)\}/g;
|
|
8060
|
-
secretStoreSchema = z16.record(z16.string(), z16.string());
|
|
8061
|
-
}
|
|
8062
|
-
});
|
|
8063
|
-
|
|
8064
8404
|
// src/transcript-writer.ts
|
|
8065
8405
|
import { appendFileSync, chmodSync as chmodSync4, copyFileSync, mkdirSync as mkdirSync8, readdirSync, rmSync as rmSync6 } from "fs";
|
|
8066
8406
|
import { basename, dirname as dirname6, join as join13 } from "path";
|
|
@@ -8387,16 +8727,6 @@ var init_turn_runtime_integrity = __esm({
|
|
|
8387
8727
|
// src/turn-execution.ts
|
|
8388
8728
|
import { createHash as createHash2, randomUUID as randomUUID3 } from "crypto";
|
|
8389
8729
|
import { existsSync as existsSync9 } from "fs";
|
|
8390
|
-
function missingSecretReason(missing) {
|
|
8391
|
-
const prefix = "missing_secret: ";
|
|
8392
|
-
for (let shown = missing.length; shown >= 0; shown--) {
|
|
8393
|
-
const omitted = missing.length - shown;
|
|
8394
|
-
const parts = [...missing.slice(0, shown), ...omitted > 0 ? [`+${omitted}`] : []];
|
|
8395
|
-
const reason = prefix + parts.join(",");
|
|
8396
|
-
if (reason.length <= ERROR_REASON_MAX) return reason;
|
|
8397
|
-
}
|
|
8398
|
-
return `${prefix}+${missing.length}`;
|
|
8399
|
-
}
|
|
8400
8730
|
function resolveIdleTimeoutMs(override) {
|
|
8401
8731
|
return override ?? DEFAULT_AGENT_IDLE_TIMEOUT_MS;
|
|
8402
8732
|
}
|
|
@@ -8433,7 +8763,7 @@ function fingerprintSessionState(state) {
|
|
|
8433
8763
|
}
|
|
8434
8764
|
return createHash2("sha256").update(opaqueId).digest("hex").slice(0, 16);
|
|
8435
8765
|
}
|
|
8436
|
-
var PREPARING_TOOL_NAME,
|
|
8766
|
+
var PREPARING_TOOL_NAME, RUNTIME_UNAVAILABLE_PREFIX, NO_TURN_TOKEN_REASON, UNEXPECTED_ROLE_REASON, DEFAULT_PREPARING_ROW_DELAY_MS, DEFAULT_AGENT_IDLE_TIMEOUT_MS, DEFAULT_AGENT_TOTAL_TIMEOUT_MS, DEFAULT_LEASE_RENEWAL_MS, TurnConcluded, TurnExecution;
|
|
8437
8767
|
var init_turn_execution = __esm({
|
|
8438
8768
|
"src/turn-execution.ts"() {
|
|
8439
8769
|
"use strict";
|
|
@@ -8457,8 +8787,8 @@ var init_turn_execution = __esm({
|
|
|
8457
8787
|
init_turn_committer();
|
|
8458
8788
|
init_turn_runtime_integrity();
|
|
8459
8789
|
init_dispatcher();
|
|
8790
|
+
init_sandbox_policy();
|
|
8460
8791
|
PREPARING_TOOL_NAME = "preparing";
|
|
8461
|
-
ERROR_REASON_MAX = 200;
|
|
8462
8792
|
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:";
|
|
8463
8793
|
NO_TURN_TOKEN_REASON = "no_turn_token: the server issued no turn credential for this turn";
|
|
8464
8794
|
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`;
|
|
@@ -8727,6 +9057,12 @@ var init_turn_execution = __esm({
|
|
|
8727
9057
|
this.resumedSpoke = turnContext.turnSpoke === true || outboxSpoke;
|
|
8728
9058
|
if (this.resumedSpoke) turnLog.debug({ turnId }, "dispatcher: resumed span already spoke");
|
|
8729
9059
|
}
|
|
9060
|
+
try {
|
|
9061
|
+
await this.opts.inRunner?.onContext?.(turnContext.attemptId ?? null);
|
|
9062
|
+
} catch (err) {
|
|
9063
|
+
const reason = `runner_unavailable: ${err instanceof Error ? err.message : String(err)}`;
|
|
9064
|
+
throw this.concluded(reason, reason);
|
|
9065
|
+
}
|
|
8730
9066
|
}
|
|
8731
9067
|
gateTrigger() {
|
|
8732
9068
|
const { payload, turnLog } = this;
|
|
@@ -8744,7 +9080,8 @@ var init_turn_execution = __esm({
|
|
|
8744
9080
|
}
|
|
8745
9081
|
async resolveSecrets() {
|
|
8746
9082
|
const { payload, workspaceId, turnLog } = this;
|
|
8747
|
-
const
|
|
9083
|
+
const fromLauncher = this.opts.inRunner?.secrets;
|
|
9084
|
+
const secretStore = this.opts.secretStore ?? (fromLauncher ? await fromLauncher(requiredSecretNames(this.turnContext.mcpServers)) : null) ?? loadSecretStoreTolerant(
|
|
8748
9085
|
(m) => turnLog.warn({ err: m }, "Could not read the secrets file. Check ~/.cabane/secrets.json.")
|
|
8749
9086
|
);
|
|
8750
9087
|
const { servers: resolvedMcpServers, missing } = resolveMcpSecrets(
|
|
@@ -8904,42 +9241,48 @@ var init_turn_execution = __esm({
|
|
|
8904
9241
|
const { payload, workspaceId, turnId, turnLog } = this;
|
|
8905
9242
|
const abortController = this.abortController;
|
|
8906
9243
|
this.supervisor.registerAbort(turnId, abortController);
|
|
9244
|
+
let started;
|
|
8907
9245
|
try {
|
|
8908
|
-
await this.opts.api.setActiveRun(
|
|
8909
|
-
|
|
8910
|
-
|
|
8911
|
-
|
|
8912
|
-
|
|
8913
|
-
|
|
8914
|
-
|
|
8915
|
-
|
|
8916
|
-
|
|
8917
|
-
|
|
8918
|
-
|
|
8919
|
-
|
|
8920
|
-
|
|
8921
|
-
|
|
8922
|
-
|
|
8923
|
-
|
|
8924
|
-
|
|
8925
|
-
|
|
8926
|
-
|
|
8927
|
-
|
|
8928
|
-
|
|
8929
|
-
|
|
8930
|
-
|
|
8931
|
-
|
|
8932
|
-
|
|
8933
|
-
|
|
8934
|
-
|
|
8935
|
-
|
|
8936
|
-
|
|
8937
|
-
|
|
8938
|
-
|
|
8939
|
-
|
|
8940
|
-
|
|
8941
|
-
|
|
8942
|
-
|
|
9246
|
+
started = await this.opts.api.setActiveRun(
|
|
9247
|
+
workspaceId,
|
|
9248
|
+
payload.conversationId,
|
|
9249
|
+
payload.agentId,
|
|
9250
|
+
{
|
|
9251
|
+
activeRunStartedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
9252
|
+
// CT1679: a runner leases under its launcher's boot id — the one the
|
|
9253
|
+
// device heartbeats — or the lost-run sweep reads it as a restart.
|
|
9254
|
+
bootId: this.opts.inRunner?.bootId ?? bootId,
|
|
9255
|
+
// CT1680: the runner holding the span, beside the device's boot id.
|
|
9256
|
+
...this.opts.inRunner ? { runnerId: this.opts.inRunner.runnerId } : {},
|
|
9257
|
+
// CT1380: the durable record that this turn resumed, and from where.
|
|
9258
|
+
...this.resumedFromSeq !== void 0 ? { resumedFromSeq: this.resumedFromSeq } : {},
|
|
9259
|
+
// CT1454: the issuance the context THIS attempt runs on came from. Read
|
|
9260
|
+
// off `this.turnContext`, which is the fetch that composed this request —
|
|
9261
|
+
// so a retransmitted start echoes the same id and reuses the same record,
|
|
9262
|
+
// and a resumed attempt echoes its own, newer one. The post-admission
|
|
9263
|
+
// fresh-session recompose (`freshRequest`) deliberately does not touch it:
|
|
9264
|
+
// it adopts only the prompt, never the run-config, so its issuance must
|
|
9265
|
+
// not become this attempt's evidence.
|
|
9266
|
+
...this.turnContext.issuanceId ? { issuanceId: this.turnContext.issuanceId } : {},
|
|
9267
|
+
// CT33: hand the server this turn's id so the new-run chokepoint's
|
|
9268
|
+
// `closeAbandonedTurns` sweep excludes it. The prepare hook may have
|
|
9269
|
+
// already emitted a "preparing" activity row for this turn above (it
|
|
9270
|
+
// runs before this flip), which would otherwise make the sweep mistake
|
|
9271
|
+
// this live turn for an abandoned one and close it with a `stopped`.
|
|
9272
|
+
turnId,
|
|
9273
|
+
// The newest message this turn's read covered: the lease records it,
|
|
9274
|
+
// and settle moves the cursor to it. Omitted when the server sent
|
|
9275
|
+
// none (an older API), so the cursor stays where it was.
|
|
9276
|
+
...this.turnContext.readThrough !== void 0 ? { readThrough: this.turnContext.readThrough } : {},
|
|
9277
|
+
...this.turnContext.readRevision !== void 0 ? { readRevision: this.turnContext.readRevision } : {},
|
|
9278
|
+
// Rooms: the attempt this request's session belongs to (null = fresh).
|
|
9279
|
+
...this.turnContext.attemptId !== void 0 ? { attemptId: this.turnContext.attemptId } : {},
|
|
9280
|
+
// CT1543: the frame this start acts on. The turn-context fetch already
|
|
9281
|
+
// checked it, but that was before an assembly this turn awaited — the
|
|
9282
|
+
// lease checks it again where the stamp is actually taken.
|
|
9283
|
+
...payload.episode !== void 0 ? { dispatchMessageId: payload.messageId, dispatchEpisode: payload.episode } : {}
|
|
9284
|
+
}
|
|
9285
|
+
);
|
|
8943
9286
|
} catch (err) {
|
|
8944
9287
|
if (leaseRefusal(err) === "dispatch_context_stale" && !this.recomposedForLease) {
|
|
8945
9288
|
this.recomposedForLease = true;
|
|
@@ -8964,6 +9307,7 @@ var init_turn_execution = __esm({
|
|
|
8964
9307
|
throw this.concluded(reason, reason);
|
|
8965
9308
|
}
|
|
8966
9309
|
this.admitted = true;
|
|
9310
|
+
if (started?.attemptId) this.opts.inRunner?.onLeased?.(started.attemptId);
|
|
8967
9311
|
}
|
|
8968
9312
|
buildRequest() {
|
|
8969
9313
|
const { payload, workspaceId } = this;
|
|
@@ -9008,7 +9352,18 @@ var init_turn_execution = __esm({
|
|
|
9008
9352
|
const { payload, workspaceId, turnId, turnLog } = this;
|
|
9009
9353
|
await guardBundledBinary(this.integrityContext(), (r, e) => this.concluded(r, e), this.outcome);
|
|
9010
9354
|
const onWarn = (msg, meta) => turnLog.debug(meta ?? {}, msg);
|
|
9011
|
-
|
|
9355
|
+
let sandbox = null;
|
|
9356
|
+
try {
|
|
9357
|
+
sandbox = this.opts.harnessSandbox ? await this.opts.harnessSandbox({
|
|
9358
|
+
cwd: this.effectiveCwd,
|
|
9359
|
+
runtime: this.turnContext.runtime,
|
|
9360
|
+
log: turnLog
|
|
9361
|
+
}) : null;
|
|
9362
|
+
} catch (err) {
|
|
9363
|
+
if (!(err instanceof HarnessSandboxRefused)) throw err;
|
|
9364
|
+
const reason = `setup_failed: ${err.message}`.slice(0, 200);
|
|
9365
|
+
throw this.concluded(reason, reason);
|
|
9366
|
+
}
|
|
9012
9367
|
const adapters = [];
|
|
9013
9368
|
if (this.opts.claudeCodeAvailable?.() ?? true) {
|
|
9014
9369
|
this.containment = createTurnContainment(this.turnId, {
|
|
@@ -9639,12 +9994,265 @@ var init_dispatcher = __esm({
|
|
|
9639
9994
|
}
|
|
9640
9995
|
});
|
|
9641
9996
|
|
|
9997
|
+
// src/runner/egress-proxy.ts
|
|
9998
|
+
var egress_proxy_exports = {};
|
|
9999
|
+
__export(egress_proxy_exports, {
|
|
10000
|
+
connectTarget: () => connectTarget,
|
|
10001
|
+
createEgressProxy: () => createEgressProxy,
|
|
10002
|
+
egressAllowed: () => egressAllowed
|
|
10003
|
+
});
|
|
10004
|
+
import { createServer as createServer2, request as httpRequest } from "http";
|
|
10005
|
+
import { connect as connect3 } from "net";
|
|
10006
|
+
function egressAllowed(host, allow, port, defaultPort = 443) {
|
|
10007
|
+
const h = host.toLowerCase().replace(/\.$/, "");
|
|
10008
|
+
if (!h) return false;
|
|
10009
|
+
const p = port ?? defaultPort;
|
|
10010
|
+
return allow.some((entry) => {
|
|
10011
|
+
const raw = entry.toLowerCase().trim();
|
|
10012
|
+
const m = /^(.*):(\d{1,5})$/.exec(raw);
|
|
10013
|
+
const name = m ? m[1] : raw;
|
|
10014
|
+
const wanted = m ? Number(m[2]) : defaultPort;
|
|
10015
|
+
if (p !== wanted) return false;
|
|
10016
|
+
if (name.startsWith("*.")) return h.endsWith(name.slice(1));
|
|
10017
|
+
return h === name;
|
|
10018
|
+
});
|
|
10019
|
+
}
|
|
10020
|
+
function connectTarget(target) {
|
|
10021
|
+
const m = /^\[?([^\]]+?)\]?:(\d{1,5})$/.exec(target);
|
|
10022
|
+
if (!m) return null;
|
|
10023
|
+
const port = Number(m[2]);
|
|
10024
|
+
if (!m[1] || port < 1 || port > 65535) return null;
|
|
10025
|
+
return { host: m[1], port };
|
|
10026
|
+
}
|
|
10027
|
+
function createEgressProxy(opts) {
|
|
10028
|
+
const { allow, log } = opts;
|
|
10029
|
+
const refuse = (host) => `cabane: ${host} is not on this device's egress allow-list (runners.docker.egressAllow)
|
|
10030
|
+
`;
|
|
10031
|
+
const server = createServer2((req, res) => {
|
|
10032
|
+
req.on("error", () => res.destroy());
|
|
10033
|
+
res.on("error", () => req.destroy());
|
|
10034
|
+
let url;
|
|
10035
|
+
try {
|
|
10036
|
+
url = new URL(req.url ?? "");
|
|
10037
|
+
} catch {
|
|
10038
|
+
res.writeHead(400).end("cabane: this proxy forwards absolute http:// URLs only\n");
|
|
10039
|
+
return;
|
|
10040
|
+
}
|
|
10041
|
+
if (url.protocol !== "http:" || !egressAllowed(url.hostname, allow, url.port ? Number(url.port) : 80, 80)) {
|
|
10042
|
+
log?.warn({ host: url.hostname }, "egress: refused");
|
|
10043
|
+
res.writeHead(403, { "content-type": "text/plain" }).end(refuse(url.hostname));
|
|
10044
|
+
return;
|
|
10045
|
+
}
|
|
10046
|
+
const upstream = httpRequest(
|
|
10047
|
+
{
|
|
10048
|
+
host: url.hostname,
|
|
10049
|
+
port: url.port || 80,
|
|
10050
|
+
method: req.method,
|
|
10051
|
+
path: `${url.pathname}${url.search}`,
|
|
10052
|
+
headers: req.headers
|
|
10053
|
+
},
|
|
10054
|
+
(up) => {
|
|
10055
|
+
res.writeHead(up.statusCode ?? 502, up.headers);
|
|
10056
|
+
up.pipe(res);
|
|
10057
|
+
}
|
|
10058
|
+
);
|
|
10059
|
+
upstream.on("error", () => {
|
|
10060
|
+
if (!res.headersSent) res.writeHead(502);
|
|
10061
|
+
res.end();
|
|
10062
|
+
});
|
|
10063
|
+
req.pipe(upstream);
|
|
10064
|
+
});
|
|
10065
|
+
server.on("clientError", (_err, socket) => socket.destroy());
|
|
10066
|
+
server.on("connect", (req, socket, head) => {
|
|
10067
|
+
socket.on("error", () => socket.destroy());
|
|
10068
|
+
const target = connectTarget(req.url ?? "");
|
|
10069
|
+
if (!target || !egressAllowed(target.host, allow, target.port)) {
|
|
10070
|
+
const host = target?.host ?? String(req.url);
|
|
10071
|
+
log?.warn({ host }, "egress: refused");
|
|
10072
|
+
const body = refuse(host);
|
|
10073
|
+
socket.end(
|
|
10074
|
+
`HTTP/1.1 403 Forbidden\r
|
|
10075
|
+
content-type: text/plain\r
|
|
10076
|
+
content-length: ${Buffer.byteLength(body)}\r
|
|
10077
|
+
\r
|
|
10078
|
+
${body}`
|
|
10079
|
+
);
|
|
10080
|
+
return;
|
|
10081
|
+
}
|
|
10082
|
+
const upstream = connect3(target.port, target.host, () => {
|
|
10083
|
+
socket.write("HTTP/1.1 200 Connection Established\r\n\r\n");
|
|
10084
|
+
if (head.length) upstream.write(head);
|
|
10085
|
+
upstream.pipe(socket);
|
|
10086
|
+
socket.pipe(upstream);
|
|
10087
|
+
});
|
|
10088
|
+
const close = () => {
|
|
10089
|
+
upstream.destroy();
|
|
10090
|
+
socket.destroy();
|
|
10091
|
+
};
|
|
10092
|
+
upstream.on("error", () => {
|
|
10093
|
+
if (!socket.destroyed) socket.end("HTTP/1.1 502 Bad Gateway\r\n\r\n");
|
|
10094
|
+
close();
|
|
10095
|
+
});
|
|
10096
|
+
socket.on("error", close);
|
|
10097
|
+
socket.on("close", () => upstream.destroy());
|
|
10098
|
+
});
|
|
10099
|
+
return server;
|
|
10100
|
+
}
|
|
10101
|
+
var init_egress_proxy = __esm({
|
|
10102
|
+
"src/runner/egress-proxy.ts"() {
|
|
10103
|
+
"use strict";
|
|
10104
|
+
}
|
|
10105
|
+
});
|
|
10106
|
+
|
|
10107
|
+
// src/runner/git-credential.ts
|
|
10108
|
+
var git_credential_exports = {};
|
|
10109
|
+
__export(git_credential_exports, {
|
|
10110
|
+
askGitCredential: () => askGitCredential,
|
|
10111
|
+
deviceGitFill: () => deviceGitFill,
|
|
10112
|
+
parseCredential: () => parseCredential,
|
|
10113
|
+
serveGitCredentials: () => serveGitCredentials
|
|
10114
|
+
});
|
|
10115
|
+
import { spawn as spawn8 } from "child_process";
|
|
10116
|
+
import { chmodSync as chmodSync5, rmSync as rmSync7 } from "fs";
|
|
10117
|
+
import { connect as connect4, createServer as createServer3 } from "net";
|
|
10118
|
+
function parseCredential(text) {
|
|
10119
|
+
const out = {};
|
|
10120
|
+
for (const line of text.split("\n")) {
|
|
10121
|
+
const eq = line.indexOf("=");
|
|
10122
|
+
if (eq <= 0) continue;
|
|
10123
|
+
out[line.slice(0, eq)] = line.slice(eq + 1);
|
|
10124
|
+
}
|
|
10125
|
+
return out;
|
|
10126
|
+
}
|
|
10127
|
+
function formatCredential(fields) {
|
|
10128
|
+
return Object.entries(fields).map(([k, v]) => `${k}=${v}`).join("\n") + "\n\n";
|
|
10129
|
+
}
|
|
10130
|
+
function deviceGitFill(helper, timeoutMs = 15e3) {
|
|
10131
|
+
return (request) => new Promise((resolve2) => {
|
|
10132
|
+
const args = [
|
|
10133
|
+
...helper ? ["-c", "credential.helper=", "-c", `credential.helper=${helper}`] : [],
|
|
10134
|
+
"credential",
|
|
10135
|
+
"fill"
|
|
10136
|
+
];
|
|
10137
|
+
const child = spawn8("git", args, {
|
|
10138
|
+
stdio: ["pipe", "pipe", "ignore"],
|
|
10139
|
+
env: { ...process.env, GIT_TERMINAL_PROMPT: "0", GIT_ASKPASS: "", SSH_ASKPASS: "" }
|
|
10140
|
+
});
|
|
10141
|
+
let out = "";
|
|
10142
|
+
const timer = setTimeout(() => child.kill("SIGKILL"), timeoutMs);
|
|
10143
|
+
child.stdout.on("data", (chunk) => out += String(chunk));
|
|
10144
|
+
child.once("error", () => resolve2(null));
|
|
10145
|
+
child.once("close", (code) => {
|
|
10146
|
+
clearTimeout(timer);
|
|
10147
|
+
resolve2(code === 0 ? out : null);
|
|
10148
|
+
});
|
|
10149
|
+
child.stdin.end(formatCredential(request));
|
|
10150
|
+
});
|
|
10151
|
+
}
|
|
10152
|
+
async function serveGitCredentials(opts) {
|
|
10153
|
+
const { socketPath, hosts, fill, log } = opts;
|
|
10154
|
+
rmSync7(socketPath, { force: true });
|
|
10155
|
+
const server = createServer3({ allowHalfOpen: true }, (socket) => {
|
|
10156
|
+
let input = "";
|
|
10157
|
+
socket.setEncoding("utf8");
|
|
10158
|
+
socket.on("data", (chunk) => input += chunk);
|
|
10159
|
+
socket.on("error", () => socket.destroy());
|
|
10160
|
+
socket.on("end", () => {
|
|
10161
|
+
void (async () => {
|
|
10162
|
+
const [op = "", ...rest] = input.split("\n");
|
|
10163
|
+
const request = parseCredential(rest.join("\n"));
|
|
10164
|
+
if (op !== "get" || request.protocol !== "https" || !request.host) {
|
|
10165
|
+
socket.end();
|
|
10166
|
+
return;
|
|
10167
|
+
}
|
|
10168
|
+
if (!egressAllowed(request.host.replace(/:\d+$/, ""), hosts)) {
|
|
10169
|
+
log?.warn(
|
|
10170
|
+
{ host: request.host },
|
|
10171
|
+
"runner: git asked for a credential for a host it may not use"
|
|
10172
|
+
);
|
|
10173
|
+
socket.end();
|
|
10174
|
+
return;
|
|
10175
|
+
}
|
|
10176
|
+
const answer = await fill({
|
|
10177
|
+
protocol: "https",
|
|
10178
|
+
host: request.host,
|
|
10179
|
+
...request.path ? { path: request.path } : {},
|
|
10180
|
+
...request.username ? { username: request.username } : {}
|
|
10181
|
+
});
|
|
10182
|
+
if (!answer) {
|
|
10183
|
+
log?.warn(
|
|
10184
|
+
{ host: request.host },
|
|
10185
|
+
"runner: this device's git has no credential for the host"
|
|
10186
|
+
);
|
|
10187
|
+
socket.end();
|
|
10188
|
+
return;
|
|
10189
|
+
}
|
|
10190
|
+
const filled = parseCredential(answer);
|
|
10191
|
+
socket.end(
|
|
10192
|
+
formatCredential({
|
|
10193
|
+
protocol: "https",
|
|
10194
|
+
host: request.host,
|
|
10195
|
+
...filled.username ? { username: filled.username } : {},
|
|
10196
|
+
...filled.password ? { password: filled.password } : {}
|
|
10197
|
+
})
|
|
10198
|
+
);
|
|
10199
|
+
})();
|
|
10200
|
+
});
|
|
10201
|
+
});
|
|
10202
|
+
await new Promise((resolve2, reject) => {
|
|
10203
|
+
server.once("error", reject);
|
|
10204
|
+
server.listen(socketPath, () => {
|
|
10205
|
+
server.off("error", reject);
|
|
10206
|
+
resolve2();
|
|
10207
|
+
});
|
|
10208
|
+
});
|
|
10209
|
+
chmodSync5(socketPath, 438);
|
|
10210
|
+
return {
|
|
10211
|
+
close: () => new Promise((resolve2) => {
|
|
10212
|
+
server.close(() => resolve2());
|
|
10213
|
+
rmSync7(socketPath, { force: true });
|
|
10214
|
+
})
|
|
10215
|
+
};
|
|
10216
|
+
}
|
|
10217
|
+
function askGitCredential(socketPath, op, request, timeoutMs = 2e4) {
|
|
10218
|
+
return new Promise((resolve2) => {
|
|
10219
|
+
const socket = connect4(socketPath);
|
|
10220
|
+
let out = "";
|
|
10221
|
+
const timer = setTimeout(() => socket.destroy(), timeoutMs);
|
|
10222
|
+
socket.setEncoding("utf8");
|
|
10223
|
+
socket.on("data", (chunk) => out += chunk);
|
|
10224
|
+
socket.on("error", () => resolve2(""));
|
|
10225
|
+
socket.on("close", () => {
|
|
10226
|
+
clearTimeout(timer);
|
|
10227
|
+
resolve2(out);
|
|
10228
|
+
});
|
|
10229
|
+
socket.end(`${op}
|
|
10230
|
+
${request}`);
|
|
10231
|
+
});
|
|
10232
|
+
}
|
|
10233
|
+
var init_git_credential = __esm({
|
|
10234
|
+
"src/runner/git-credential.ts"() {
|
|
10235
|
+
"use strict";
|
|
10236
|
+
init_egress_proxy();
|
|
10237
|
+
}
|
|
10238
|
+
});
|
|
10239
|
+
|
|
9642
10240
|
// src/runner/harness-sandbox.ts
|
|
9643
10241
|
import { lookup } from "dns/promises";
|
|
9644
|
-
import {
|
|
10242
|
+
import {
|
|
10243
|
+
chmodSync as chmodSync6,
|
|
10244
|
+
mkdirSync as mkdirSync11,
|
|
10245
|
+
mkdtempSync,
|
|
10246
|
+
readFileSync as readFileSync10,
|
|
10247
|
+
realpathSync as realpathSync3,
|
|
10248
|
+
rmSync as rmSync10,
|
|
10249
|
+
writeFileSync as writeFileSync7
|
|
10250
|
+
} from "fs";
|
|
10251
|
+
import { createRequire as createRequire2 } from "module";
|
|
9645
10252
|
import { isIP } from "net";
|
|
9646
|
-
import { homedir as homedir3, networkInterfaces, tmpdir as
|
|
9647
|
-
import { join as
|
|
10253
|
+
import { homedir as homedir3, networkInterfaces, tmpdir as tmpdir3, userInfo as userInfo2 } from "os";
|
|
10254
|
+
import { dirname as dirname8, join as join18 } from "path";
|
|
10255
|
+
import { fileURLToPath as fileURLToPath4 } from "url";
|
|
9648
10256
|
import { SandboxManager } from "@anthropic-ai/sandbox-runtime";
|
|
9649
10257
|
function accountHome() {
|
|
9650
10258
|
try {
|
|
@@ -9674,32 +10282,117 @@ async function resolvedAddresses(baseUrl) {
|
|
|
9674
10282
|
return [];
|
|
9675
10283
|
}
|
|
9676
10284
|
}
|
|
10285
|
+
function companionPackageRoot(modulePath) {
|
|
10286
|
+
for (let dir2 = dirname8(modulePath); ; dir2 = dirname8(dir2)) {
|
|
10287
|
+
try {
|
|
10288
|
+
const manifest = JSON.parse(readFileSync10(join18(dir2, "package.json"), "utf8"));
|
|
10289
|
+
if (manifest.name === "@cabane/companion") return dir2;
|
|
10290
|
+
} catch {
|
|
10291
|
+
}
|
|
10292
|
+
if (dirname8(dir2) === dir2) return null;
|
|
10293
|
+
}
|
|
10294
|
+
}
|
|
10295
|
+
function harnessRuntimePaths() {
|
|
10296
|
+
const paths = [dirname8(dirname8(process.execPath))];
|
|
10297
|
+
const root = companionPackageRoot(fileURLToPath4(import.meta.url));
|
|
10298
|
+
if (root) paths.push(root);
|
|
10299
|
+
try {
|
|
10300
|
+
const require2 = createRequire2(import.meta.url);
|
|
10301
|
+
paths.push(
|
|
10302
|
+
dirname8(realpathSync3(require2.resolve("@anthropic-ai/sandbox-runtime/package.json")))
|
|
10303
|
+
);
|
|
10304
|
+
} catch {
|
|
10305
|
+
}
|
|
10306
|
+
for (const runtime of ["claude-code", "codex"]) {
|
|
10307
|
+
const bundled = resolveBundledBinary(runtime).path;
|
|
10308
|
+
if (bundled) {
|
|
10309
|
+
try {
|
|
10310
|
+
paths.push(dirname8(realpathSync3(bundled)));
|
|
10311
|
+
} catch {
|
|
10312
|
+
}
|
|
10313
|
+
}
|
|
10314
|
+
}
|
|
10315
|
+
return [...new Set(paths)];
|
|
10316
|
+
}
|
|
9677
10317
|
function ownAddresses() {
|
|
9678
10318
|
return Object.values(networkInterfaces()).flat().flatMap((i) => i ? [i.address] : []);
|
|
9679
10319
|
}
|
|
10320
|
+
function expandHome(path, home) {
|
|
10321
|
+
if (path === "~") return home;
|
|
10322
|
+
return path.startsWith("~/") ? join18(home, path.slice(2)) : path;
|
|
10323
|
+
}
|
|
10324
|
+
function stripPort(pattern) {
|
|
10325
|
+
return pattern.startsWith("[") ? pattern.replace(/^\[([^\]]+)\](:\d+)?$/, "$1") : pattern.replace(/:\d+$/, "");
|
|
10326
|
+
}
|
|
10327
|
+
function hostMatches(host, pattern) {
|
|
10328
|
+
return pattern.startsWith("*.") ? host.endsWith(pattern.slice(1)) : host === pattern;
|
|
10329
|
+
}
|
|
10330
|
+
function patternsOverlap(a, b) {
|
|
10331
|
+
if (a === b) return true;
|
|
10332
|
+
const bare = (p) => p.startsWith("*.") ? p.slice(1) : p;
|
|
10333
|
+
if (a.startsWith("*.") && b.startsWith("*.")) {
|
|
10334
|
+
return bare(a).endsWith(bare(b)) || bare(b).endsWith(bare(a));
|
|
10335
|
+
}
|
|
10336
|
+
return hostMatches(a, b) || hostMatches(b, a);
|
|
10337
|
+
}
|
|
9680
10338
|
function harnessSandboxConfig(input) {
|
|
9681
|
-
const
|
|
9682
|
-
const
|
|
10339
|
+
const { policy } = input;
|
|
10340
|
+
const claudeDir = input.claudeConfigDir ?? join18(input.home, ".claude");
|
|
10341
|
+
const codexDir = input.codexHome ?? join18(input.home, ".codex");
|
|
10342
|
+
const homes = [.../* @__PURE__ */ new Set([input.home, input.accountHome ?? input.home])];
|
|
10343
|
+
const expand = (paths) => (paths ?? []).map((p) => expandHome(p, input.home));
|
|
10344
|
+
const cabane = cabaneHostEntries(input.baseUrl, input.resolved, input.ownAddresses);
|
|
10345
|
+
const models = modelHostsFor(input.runtime);
|
|
10346
|
+
const reachable = [
|
|
10347
|
+
...policy.hosts?.defaults === false ? [] : DEFAULT_ALLOWED_HOSTS,
|
|
10348
|
+
...policy.hosts?.allow ?? []
|
|
10349
|
+
];
|
|
10350
|
+
const allowedDomains = [.../* @__PURE__ */ new Set([...cabane, ...models, ...reachable])];
|
|
10351
|
+
const envGlobs = input.cwd ? [join18(input.cwd, "**/.env"), join18(input.cwd, "**/.env.*")] : [];
|
|
10352
|
+
const secrets = [
|
|
10353
|
+
input.cabaneDir,
|
|
10354
|
+
...homes.flatMap((home) => SECRET_PATHS_UNDER_HOME.map((p) => join18(home, p))),
|
|
10355
|
+
...envGlobs,
|
|
10356
|
+
...expand(policy.filesystem?.denyRead)
|
|
10357
|
+
];
|
|
10358
|
+
const strict = policy.filesystem?.mode === "strict";
|
|
10359
|
+
const denyRead = strict ? [...homes, ...secrets] : secrets;
|
|
10360
|
+
const allowRead = strict ? [
|
|
10361
|
+
...input.cwd ? [input.cwd] : [],
|
|
10362
|
+
claudeDir,
|
|
10363
|
+
join18(input.home, ".claude.json"),
|
|
10364
|
+
codexDir,
|
|
10365
|
+
...input.runtimePaths ?? [],
|
|
10366
|
+
...expand(policy.filesystem?.allowRead)
|
|
10367
|
+
] : expand(policy.filesystem?.allowRead);
|
|
10368
|
+
const masked = input.credentials.map((c) => ({
|
|
10369
|
+
name: c.name,
|
|
10370
|
+
mode: "mask",
|
|
10371
|
+
injectHosts: [...new Set(c.hosts.map(stripPort))]
|
|
10372
|
+
}));
|
|
10373
|
+
const injectAt = masked.flatMap((c) => c.injectHosts);
|
|
9683
10374
|
return {
|
|
9684
10375
|
network: {
|
|
9685
|
-
allowedDomains
|
|
9686
|
-
|
|
9687
|
-
|
|
9688
|
-
|
|
9689
|
-
|
|
9690
|
-
|
|
10376
|
+
allowedDomains,
|
|
10377
|
+
deniedDomains: policy.hosts?.deny ?? [],
|
|
10378
|
+
allowLocalBinding: true,
|
|
10379
|
+
// A masked credential is swapped in by a proxy that reads the request,
|
|
10380
|
+
// so TLS is terminated — but only for the hosts a credential goes to.
|
|
10381
|
+
// Cabane, the model provider and every other host keep their own
|
|
10382
|
+
// end-to-end TLS through an opaque tunnel.
|
|
10383
|
+
...masked.length ? {
|
|
10384
|
+
tlsTerminate: {
|
|
10385
|
+
excludeDomains: [
|
|
10386
|
+
...new Set(
|
|
10387
|
+
allowedDomains.map(stripPort).filter((host) => !injectAt.some((p) => patternsOverlap(host, p)))
|
|
10388
|
+
)
|
|
10389
|
+
]
|
|
10390
|
+
}
|
|
10391
|
+
} : {}
|
|
9691
10392
|
},
|
|
9692
10393
|
filesystem: {
|
|
9693
|
-
|
|
9694
|
-
|
|
9695
|
-
denyRead: [
|
|
9696
|
-
input.cabaneDir,
|
|
9697
|
-
...[.../* @__PURE__ */ new Set([input.home, input.accountHome ?? input.home])].flatMap((home) => [
|
|
9698
|
-
join15(home, ".ssh"),
|
|
9699
|
-
join15(home, ".aws"),
|
|
9700
|
-
join15(home, ".gnupg")
|
|
9701
|
-
])
|
|
9702
|
-
],
|
|
10394
|
+
denyRead,
|
|
10395
|
+
...allowRead.length ? { allowRead } : {},
|
|
9703
10396
|
// The checkout, scratch, and the harnesses' own session state — a turn
|
|
9704
10397
|
// resumes from `~/.claude` and `~/.codex`, and without write there every
|
|
9705
10398
|
// resume starts from nothing.
|
|
@@ -9707,13 +10400,28 @@ function harnessSandboxConfig(input) {
|
|
|
9707
10400
|
...input.cwd ? [input.cwd] : [],
|
|
9708
10401
|
input.tmp,
|
|
9709
10402
|
claudeDir,
|
|
9710
|
-
|
|
9711
|
-
codexDir
|
|
10403
|
+
join18(input.home, ".claude.json"),
|
|
10404
|
+
codexDir,
|
|
10405
|
+
...expand(policy.filesystem?.allowWrite)
|
|
9712
10406
|
],
|
|
9713
10407
|
denyWrite: []
|
|
9714
|
-
}
|
|
10408
|
+
},
|
|
10409
|
+
...masked.length ? { credentials: { envVars: masked } } : {}
|
|
9715
10410
|
};
|
|
9716
10411
|
}
|
|
10412
|
+
function gitCredentialExports(credentials) {
|
|
10413
|
+
const git = credentials.filter((c) => c.gitHost);
|
|
10414
|
+
if (!git.length) return "";
|
|
10415
|
+
const lines2 = ["n=${GIT_CONFIG_COUNT:-0}"];
|
|
10416
|
+
for (const c of git) {
|
|
10417
|
+
lines2.push(
|
|
10418
|
+
`export "GIT_CONFIG_KEY_$n=http.https://${c.gitHost}/.extraHeader" "GIT_CONFIG_VALUE_$n=Authorization: Basic $${c.name}"`,
|
|
10419
|
+
"n=$((n+1))"
|
|
10420
|
+
);
|
|
10421
|
+
}
|
|
10422
|
+
lines2.push("export GIT_CONFIG_COUNT=$n");
|
|
10423
|
+
return lines2.join("; ") + "; ";
|
|
10424
|
+
}
|
|
9717
10425
|
function osSandboxUsable(log) {
|
|
9718
10426
|
if (!SandboxManager.isSupportedPlatform()) {
|
|
9719
10427
|
log.warn(
|
|
@@ -9734,7 +10442,9 @@ function osSandboxUsable(log) {
|
|
|
9734
10442
|
}
|
|
9735
10443
|
async function prepareHarnessSandbox(input) {
|
|
9736
10444
|
let wrapped;
|
|
10445
|
+
const held = input.credentials.map((c) => c.name);
|
|
9737
10446
|
try {
|
|
10447
|
+
for (const c of input.credentials) process.env[c.name] = c.value;
|
|
9738
10448
|
const config = harnessSandboxConfig({
|
|
9739
10449
|
cwd: input.cwd,
|
|
9740
10450
|
baseUrl: input.baseUrl,
|
|
@@ -9742,36 +10452,58 @@ async function prepareHarnessSandbox(input) {
|
|
|
9742
10452
|
ownAddresses: ownAddresses(),
|
|
9743
10453
|
home: homedir3(),
|
|
9744
10454
|
accountHome: accountHome(),
|
|
9745
|
-
tmp:
|
|
10455
|
+
tmp: tmpdir3(),
|
|
9746
10456
|
cabaneDir: cabaneDir(),
|
|
9747
10457
|
claudeConfigDir: process.env.CLAUDE_CONFIG_DIR,
|
|
9748
|
-
codexHome: process.env.CODEX_HOME
|
|
10458
|
+
codexHome: process.env.CODEX_HOME,
|
|
10459
|
+
runtime: input.runtime,
|
|
10460
|
+
policy: input.policy,
|
|
10461
|
+
credentials: input.credentials,
|
|
10462
|
+
runtimePaths: harnessRuntimePaths()
|
|
9749
10463
|
});
|
|
9750
10464
|
await SandboxManager.initialize(config);
|
|
10465
|
+
if (input.policy.filesystem?.mode === "strict") {
|
|
10466
|
+
input.log.info(
|
|
10467
|
+
{ readable: config.filesystem.allowRead },
|
|
10468
|
+
"runners: strict sandbox \u2014 the home is unreadable except these paths; a path missing inside a turn and not listed here is hidden by the policy (add it to runners.policy.filesystem.allowRead)"
|
|
10469
|
+
);
|
|
10470
|
+
}
|
|
9751
10471
|
input.log.debug(
|
|
9752
10472
|
{
|
|
9753
10473
|
allowWrite: config.filesystem.allowWrite,
|
|
9754
10474
|
denyRead: config.filesystem.denyRead,
|
|
9755
|
-
|
|
10475
|
+
allowRead: config.filesystem.allowRead,
|
|
10476
|
+
hosts: config.network.allowedDomains,
|
|
10477
|
+
masked: held
|
|
9756
10478
|
},
|
|
9757
10479
|
"runners: harness sandbox ready"
|
|
9758
10480
|
);
|
|
9759
10481
|
wrapped = await SandboxManager.wrapWithSandbox(
|
|
9760
|
-
|
|
10482
|
+
"export NO_PROXY= no_proxy=; " + gitCredentialExports(input.credentials) + 'eval "exec $CABANE_SANDBOXED_ARGV"'
|
|
9761
10483
|
);
|
|
9762
10484
|
} catch (err) {
|
|
10485
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
10486
|
+
await SandboxManager.reset().catch(() => {
|
|
10487
|
+
});
|
|
10488
|
+
if (held.length || Object.keys(input.policy).length) {
|
|
10489
|
+
input.log.warn(
|
|
10490
|
+
{ err: message, masked: held },
|
|
10491
|
+
"runners: the OS sandbox could not start under this device's runners.policy; refusing the turn rather than running it unsandboxed"
|
|
10492
|
+
);
|
|
10493
|
+
throw new HarnessSandboxRefused(`the OS sandbox could not start: ${message}`);
|
|
10494
|
+
}
|
|
9763
10495
|
input.log.warn(
|
|
9764
|
-
{ err:
|
|
10496
|
+
{ err: message },
|
|
9765
10497
|
"runners: the OS sandbox could not start; running the turn unsandboxed"
|
|
9766
10498
|
);
|
|
9767
|
-
await SandboxManager.reset().catch(() => {
|
|
9768
|
-
});
|
|
9769
10499
|
return null;
|
|
10500
|
+
} finally {
|
|
10501
|
+
for (const name of held) delete process.env[name];
|
|
9770
10502
|
}
|
|
9771
|
-
|
|
9772
|
-
const dir2 = mkdtempSync(
|
|
9773
|
-
const script =
|
|
9774
|
-
|
|
10503
|
+
mkdirSync11(join18(cabaneDir(), "sandbox"), { recursive: true });
|
|
10504
|
+
const dir2 = mkdtempSync(join18(cabaneDir(), "sandbox", "turn-"));
|
|
10505
|
+
const script = join18(dir2, "exec-sandboxed");
|
|
10506
|
+
writeFileSync7(
|
|
9775
10507
|
script,
|
|
9776
10508
|
[
|
|
9777
10509
|
"#!/bin/bash",
|
|
@@ -9781,56 +10513,61 @@ async function prepareHarnessSandbox(input) {
|
|
|
9781
10513
|
""
|
|
9782
10514
|
].join("\n")
|
|
9783
10515
|
);
|
|
9784
|
-
|
|
10516
|
+
chmodSync6(script, 448);
|
|
9785
10517
|
let shims = 0;
|
|
9786
10518
|
return {
|
|
9787
10519
|
wrap: (command, args) => ({ command: script, args: [command, ...args] }),
|
|
9788
10520
|
executableFor: (real) => {
|
|
9789
|
-
const shim =
|
|
9790
|
-
|
|
10521
|
+
const shim = join18(dir2, `exec-${++shims}`);
|
|
10522
|
+
writeFileSync7(shim, `#!/bin/bash
|
|
9791
10523
|
exec ${shellQuote(script)} ${shellQuote(real)} "$@"
|
|
9792
10524
|
`);
|
|
9793
|
-
|
|
10525
|
+
chmodSync6(shim, 448);
|
|
9794
10526
|
return shim;
|
|
9795
10527
|
},
|
|
9796
10528
|
dispose: async () => {
|
|
9797
|
-
|
|
10529
|
+
rmSync10(dir2, { recursive: true, force: true });
|
|
9798
10530
|
await SandboxManager.reset();
|
|
9799
10531
|
}
|
|
9800
10532
|
};
|
|
9801
10533
|
}
|
|
9802
|
-
var
|
|
10534
|
+
var SECRET_PATHS_UNDER_HOME;
|
|
9803
10535
|
var init_harness_sandbox = __esm({
|
|
9804
10536
|
"src/runner/harness-sandbox.ts"() {
|
|
9805
10537
|
"use strict";
|
|
10538
|
+
init_bundled_binaries();
|
|
9806
10539
|
init_config();
|
|
9807
|
-
|
|
9808
|
-
|
|
9809
|
-
"
|
|
9810
|
-
"
|
|
9811
|
-
"
|
|
9812
|
-
"
|
|
9813
|
-
"
|
|
9814
|
-
"
|
|
9815
|
-
"
|
|
9816
|
-
"
|
|
9817
|
-
"
|
|
9818
|
-
"
|
|
10540
|
+
init_sandbox_policy();
|
|
10541
|
+
SECRET_PATHS_UNDER_HOME = [
|
|
10542
|
+
".ssh",
|
|
10543
|
+
".aws",
|
|
10544
|
+
".gnupg",
|
|
10545
|
+
".config/gh",
|
|
10546
|
+
".git-credentials",
|
|
10547
|
+
".netrc",
|
|
10548
|
+
".npmrc",
|
|
10549
|
+
".pypirc",
|
|
10550
|
+
".docker/config.json",
|
|
10551
|
+
".kube",
|
|
10552
|
+
".config/gcloud",
|
|
10553
|
+
".azure",
|
|
10554
|
+
".config/op",
|
|
10555
|
+
".password-store"
|
|
9819
10556
|
];
|
|
9820
10557
|
}
|
|
9821
10558
|
});
|
|
9822
10559
|
|
|
9823
10560
|
// src/outbox.ts
|
|
9824
10561
|
import {
|
|
9825
|
-
existsSync as
|
|
9826
|
-
mkdirSync as
|
|
9827
|
-
readdirSync as
|
|
9828
|
-
readFileSync as
|
|
9829
|
-
renameSync as
|
|
9830
|
-
rmSync as
|
|
9831
|
-
writeFileSync as
|
|
10562
|
+
existsSync as existsSync14,
|
|
10563
|
+
mkdirSync as mkdirSync12,
|
|
10564
|
+
readdirSync as readdirSync5,
|
|
10565
|
+
readFileSync as readFileSync11,
|
|
10566
|
+
renameSync as renameSync6,
|
|
10567
|
+
rmSync as rmSync11,
|
|
10568
|
+
writeFileSync as writeFileSync8
|
|
9832
10569
|
} from "fs";
|
|
9833
|
-
import { join as
|
|
10570
|
+
import { join as join19 } from "path";
|
|
9834
10571
|
var MAX_ENTRIES, MAX_AGE_MS, Outbox;
|
|
9835
10572
|
var init_outbox = __esm({
|
|
9836
10573
|
"src/outbox.ts"() {
|
|
@@ -9848,25 +10585,25 @@ var init_outbox = __esm({
|
|
|
9848
10585
|
// Resolved lazily (per call, like cursor.ts) so tests that swap HOME between
|
|
9849
10586
|
// cases route writes at the right tmpdir.
|
|
9850
10587
|
dir() {
|
|
9851
|
-
return
|
|
10588
|
+
return join19(runnerStateDir(), "outbox", encodeURIComponent(this.workspaceId));
|
|
9852
10589
|
}
|
|
9853
10590
|
fileFor(turnId, seq) {
|
|
9854
|
-
return
|
|
10591
|
+
return join19(this.dir(), `${encodeURIComponent(turnId)}__${seq}.json`);
|
|
9855
10592
|
}
|
|
9856
10593
|
// Persist a commit for later draining. Atomic (temp file + rename) so a
|
|
9857
10594
|
// concurrent `list()` never reads a half-written entry, then enforces the
|
|
9858
10595
|
// per-workspace bounds.
|
|
9859
10596
|
persist(entry) {
|
|
9860
10597
|
const dir2 = this.dir();
|
|
9861
|
-
|
|
10598
|
+
mkdirSync12(dir2, { recursive: true });
|
|
9862
10599
|
const target = this.fileFor(entry.turnId, entry.seq);
|
|
9863
10600
|
const tmp = `${target}.${process.pid}.tmp`;
|
|
9864
10601
|
try {
|
|
9865
|
-
|
|
9866
|
-
|
|
10602
|
+
writeFileSync8(tmp, JSON.stringify(entry) + "\n", "utf8");
|
|
10603
|
+
renameSync6(tmp, target);
|
|
9867
10604
|
} catch (err) {
|
|
9868
10605
|
try {
|
|
9869
|
-
|
|
10606
|
+
rmSync11(tmp, { force: true });
|
|
9870
10607
|
} catch {
|
|
9871
10608
|
}
|
|
9872
10609
|
this.log?.warn(
|
|
@@ -9883,19 +10620,19 @@ var init_outbox = __esm({
|
|
|
9883
10620
|
// wedging the drain.
|
|
9884
10621
|
list() {
|
|
9885
10622
|
const dir2 = this.dir();
|
|
9886
|
-
if (!
|
|
10623
|
+
if (!existsSync14(dir2)) return [];
|
|
9887
10624
|
let names;
|
|
9888
10625
|
try {
|
|
9889
|
-
names =
|
|
10626
|
+
names = readdirSync5(dir2);
|
|
9890
10627
|
} catch {
|
|
9891
10628
|
return [];
|
|
9892
10629
|
}
|
|
9893
10630
|
const entries = [];
|
|
9894
10631
|
for (const name of names) {
|
|
9895
10632
|
if (!name.endsWith(".json")) continue;
|
|
9896
|
-
const full =
|
|
10633
|
+
const full = join19(dir2, name);
|
|
9897
10634
|
try {
|
|
9898
|
-
const parsed = JSON.parse(
|
|
10635
|
+
const parsed = JSON.parse(readFileSync11(full, "utf8"));
|
|
9899
10636
|
if (parsed && typeof parsed.turnId === "string" && typeof parsed.seq === "number" && typeof parsed.path === "string") {
|
|
9900
10637
|
entries.push(parsed);
|
|
9901
10638
|
} else {
|
|
@@ -9929,8 +10666,8 @@ var init_outbox = __esm({
|
|
|
9929
10666
|
// different: it is positive evidence that nothing was ever queued here.
|
|
9930
10667
|
maxSeqForTurn(turnId) {
|
|
9931
10668
|
const dir2 = this.dir();
|
|
9932
|
-
if (!
|
|
9933
|
-
const names =
|
|
10669
|
+
if (!existsSync14(dir2)) return 0;
|
|
10670
|
+
const names = readdirSync5(dir2);
|
|
9934
10671
|
const prefix = `${encodeURIComponent(turnId)}__`;
|
|
9935
10672
|
let max = 0;
|
|
9936
10673
|
for (const name of names) {
|
|
@@ -9943,15 +10680,15 @@ var init_outbox = __esm({
|
|
|
9943
10680
|
// Remove a delivered (or terminally-discarded) entry. No-op if already gone.
|
|
9944
10681
|
remove(turnId, seq) {
|
|
9945
10682
|
try {
|
|
9946
|
-
|
|
10683
|
+
rmSync11(this.fileFor(turnId, seq), { force: true });
|
|
9947
10684
|
} catch {
|
|
9948
10685
|
}
|
|
9949
10686
|
}
|
|
9950
10687
|
size() {
|
|
9951
10688
|
const dir2 = this.dir();
|
|
9952
|
-
if (!
|
|
10689
|
+
if (!existsSync14(dir2)) return 0;
|
|
9953
10690
|
try {
|
|
9954
|
-
return
|
|
10691
|
+
return readdirSync5(dir2).filter((n) => n.endsWith(".json")).length;
|
|
9955
10692
|
} catch {
|
|
9956
10693
|
return 0;
|
|
9957
10694
|
}
|
|
@@ -9962,7 +10699,7 @@ var init_outbox = __esm({
|
|
|
9962
10699
|
"A saved update is unreadable and couldn't be delivered. Check the conversation for missing replies."
|
|
9963
10700
|
);
|
|
9964
10701
|
try {
|
|
9965
|
-
|
|
10702
|
+
rmSync11(full, { force: true });
|
|
9966
10703
|
} catch {
|
|
9967
10704
|
}
|
|
9968
10705
|
}
|
|
@@ -9998,9 +10735,42 @@ var init_outbox = __esm({
|
|
|
9998
10735
|
}
|
|
9999
10736
|
});
|
|
10000
10737
|
|
|
10738
|
+
// src/version.ts
|
|
10739
|
+
import { createRequire as createRequire3 } from "module";
|
|
10740
|
+
var pkg, COMPANION_VERSION;
|
|
10741
|
+
var init_version2 = __esm({
|
|
10742
|
+
"src/version.ts"() {
|
|
10743
|
+
"use strict";
|
|
10744
|
+
pkg = createRequire3(import.meta.url)("../package.json");
|
|
10745
|
+
COMPANION_VERSION = pkg.version;
|
|
10746
|
+
}
|
|
10747
|
+
});
|
|
10748
|
+
|
|
10749
|
+
// src/runner/image-manifest.ts
|
|
10750
|
+
var image_manifest_exports = {};
|
|
10751
|
+
__export(image_manifest_exports, {
|
|
10752
|
+
runnerManifest: () => runnerManifest
|
|
10753
|
+
});
|
|
10754
|
+
function runnerManifest() {
|
|
10755
|
+
return {
|
|
10756
|
+
version: COMPANION_VERSION,
|
|
10757
|
+
bundled: {
|
|
10758
|
+
"claude-code": resolveBundledBinary("claude-code").status,
|
|
10759
|
+
codex: resolveBundledBinary("codex").status
|
|
10760
|
+
}
|
|
10761
|
+
};
|
|
10762
|
+
}
|
|
10763
|
+
var init_image_manifest = __esm({
|
|
10764
|
+
"src/runner/image-manifest.ts"() {
|
|
10765
|
+
"use strict";
|
|
10766
|
+
init_bundled_binaries();
|
|
10767
|
+
init_version2();
|
|
10768
|
+
}
|
|
10769
|
+
});
|
|
10770
|
+
|
|
10001
10771
|
// src/runner/exec-shim.ts
|
|
10002
|
-
import { chmodSync as
|
|
10003
|
-
import { join as
|
|
10772
|
+
import { chmodSync as chmodSync7, mkdirSync as mkdirSync15, mkdtempSync as mkdtempSync2, rmSync as rmSync13, writeFileSync as writeFileSync10 } from "fs";
|
|
10773
|
+
import { join as join22 } from "path";
|
|
10004
10774
|
function shellQuote2(value) {
|
|
10005
10775
|
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
10006
10776
|
}
|
|
@@ -10010,18 +10780,18 @@ function createShimDir() {
|
|
|
10010
10780
|
return {
|
|
10011
10781
|
write(argv) {
|
|
10012
10782
|
if (!dir2) {
|
|
10013
|
-
|
|
10014
|
-
dir2 = mkdtempSync2(
|
|
10783
|
+
mkdirSync15(join22(cabaneDir(), "runner"), { recursive: true });
|
|
10784
|
+
dir2 = mkdtempSync2(join22(cabaneDir(), "runner", "turn-"));
|
|
10015
10785
|
}
|
|
10016
|
-
const path =
|
|
10017
|
-
|
|
10786
|
+
const path = join22(dir2, `exec-${++count}`);
|
|
10787
|
+
writeFileSync10(path, `#!/bin/sh
|
|
10018
10788
|
exec ${argv.map(shellQuote2).join(" ")} "$@"
|
|
10019
10789
|
`);
|
|
10020
|
-
|
|
10790
|
+
chmodSync7(path, 448);
|
|
10021
10791
|
return path;
|
|
10022
10792
|
},
|
|
10023
10793
|
dispose() {
|
|
10024
|
-
if (dir2)
|
|
10794
|
+
if (dir2) rmSync13(dir2, { recursive: true, force: true });
|
|
10025
10795
|
dir2 = null;
|
|
10026
10796
|
}
|
|
10027
10797
|
};
|
|
@@ -10037,6 +10807,7 @@ var init_exec_shim = __esm({
|
|
|
10037
10807
|
import { createInterface as createInterface3 } from "readline";
|
|
10038
10808
|
function openLauncherChannel(stdin, emit, onLauncherGone) {
|
|
10039
10809
|
const waiting = /* @__PURE__ */ new Map();
|
|
10810
|
+
const replies = /* @__PURE__ */ new Map();
|
|
10040
10811
|
let next = 0;
|
|
10041
10812
|
let resolveFirst;
|
|
10042
10813
|
let gotFirst = false;
|
|
@@ -10050,11 +10821,26 @@ function openLauncherChannel(stdin, emit, onLauncherGone) {
|
|
|
10050
10821
|
}
|
|
10051
10822
|
const message = decodeLauncherMessage(line);
|
|
10052
10823
|
if (message?.cabaneLauncher === "granted") waiting.get(message.id)?.();
|
|
10824
|
+
else if (message) replies.get(message.id)?.(message);
|
|
10053
10825
|
});
|
|
10826
|
+
let gone = false;
|
|
10054
10827
|
lines2.once("close", () => {
|
|
10828
|
+
gone = true;
|
|
10055
10829
|
if (!gotFirst) resolveFirst("");
|
|
10830
|
+
for (const reply2 of [...replies.values()]) reply2(null);
|
|
10056
10831
|
onLauncherGone();
|
|
10057
10832
|
});
|
|
10833
|
+
const ask = (event) => {
|
|
10834
|
+
if (gone) return Promise.resolve(null);
|
|
10835
|
+
const id = String(++next);
|
|
10836
|
+
return new Promise((resolve2) => {
|
|
10837
|
+
replies.set(id, (message) => {
|
|
10838
|
+
replies.delete(id);
|
|
10839
|
+
resolve2(message);
|
|
10840
|
+
});
|
|
10841
|
+
emit(event(id));
|
|
10842
|
+
});
|
|
10843
|
+
};
|
|
10058
10844
|
return {
|
|
10059
10845
|
first,
|
|
10060
10846
|
acquire(key, signal) {
|
|
@@ -10080,6 +10866,14 @@ function openLauncherChannel(stdin, emit, onLauncherGone) {
|
|
|
10080
10866
|
signal.addEventListener("abort", onAbort, { once: true });
|
|
10081
10867
|
emit({ cabaneRunner: "lock", id, key });
|
|
10082
10868
|
});
|
|
10869
|
+
},
|
|
10870
|
+
async bindAttempt(attemptId) {
|
|
10871
|
+
const reply2 = await ask((id) => ({ cabaneRunner: "attempt", id, attemptId }));
|
|
10872
|
+
if (reply2?.cabaneLauncher === "bound" && reply2.refused) throw new Error(reply2.refused);
|
|
10873
|
+
},
|
|
10874
|
+
async secrets(names) {
|
|
10875
|
+
const reply2 = await ask((id) => ({ cabaneRunner: "secrets", id, names: [...names] }));
|
|
10876
|
+
return reply2?.cabaneLauncher === "secrets" ? reply2.values : {};
|
|
10083
10877
|
}
|
|
10084
10878
|
};
|
|
10085
10879
|
}
|
|
@@ -10093,6 +10887,7 @@ var init_launcher_channel = __esm({
|
|
|
10093
10887
|
// src/runner/run-turn.ts
|
|
10094
10888
|
var run_turn_exports = {};
|
|
10095
10889
|
__export(run_turn_exports, {
|
|
10890
|
+
drainBeforeExit: () => drainBeforeExit,
|
|
10096
10891
|
runTurn: () => runTurn,
|
|
10097
10892
|
runnerDispatcherOptions: () => runnerDispatcherOptions,
|
|
10098
10893
|
runnerLogger: () => runnerLogger,
|
|
@@ -10100,6 +10895,19 @@ __export(run_turn_exports, {
|
|
|
10100
10895
|
});
|
|
10101
10896
|
import { spawnSync } from "child_process";
|
|
10102
10897
|
import pino2 from "pino";
|
|
10898
|
+
async function drainBeforeExit(outbox, api, graceMs = OUTBOX_GRACE_MS, log) {
|
|
10899
|
+
if (outbox.size() === 0) return;
|
|
10900
|
+
const deadline = Date.now() + graceMs;
|
|
10901
|
+
while (Date.now() < deadline) {
|
|
10902
|
+
const { remaining } = await api.drainOutbox().catch(() => ({ remaining: outbox.size() }));
|
|
10903
|
+
if (remaining === 0) return;
|
|
10904
|
+
await new Promise((resolve2) => setTimeout(resolve2, Math.min(OUTBOX_RETRY_MS, graceMs)));
|
|
10905
|
+
}
|
|
10906
|
+
log?.warn(
|
|
10907
|
+
{ remaining: outbox.size() },
|
|
10908
|
+
"runner: exiting with updates Cabane hasn't taken yet; the companion delivers them"
|
|
10909
|
+
);
|
|
10910
|
+
}
|
|
10103
10911
|
function runnerLogger(input, stderr) {
|
|
10104
10912
|
return pino2({ level: input.spec.logLevel }, stderr).child({ agentName: input.spec.agentName });
|
|
10105
10913
|
}
|
|
@@ -10116,6 +10924,8 @@ function runnerDispatcherOptions(input, log, emit, extras = {}) {
|
|
|
10116
10924
|
outbox,
|
|
10117
10925
|
log
|
|
10118
10926
|
});
|
|
10927
|
+
extras.onOutbox?.(outbox, api);
|
|
10928
|
+
const { bindAttempt, secrets } = extras;
|
|
10119
10929
|
const health = new Map(Object.entries(spec.connectorHealth));
|
|
10120
10930
|
const connectorHealth = {
|
|
10121
10931
|
lookup: (runtime) => health.get(runtime),
|
|
@@ -10157,16 +10967,24 @@ function runnerDispatcherOptions(input, log, emit, extras = {}) {
|
|
|
10157
10967
|
(sessionId) => emit({ cabaneRunner: "remote_session", serverUrl, sessionId })
|
|
10158
10968
|
),
|
|
10159
10969
|
...extras.onProcessGroup ? { onProcessGroup: extras.onProcessGroup } : {},
|
|
10970
|
+
...spec.attemptState && bindAttempt ? {
|
|
10971
|
+
onContext: (attemptId) => bindAttempt(attemptId),
|
|
10972
|
+
onLeased: (attemptId) => emit({ cabaneRunner: "leased", attemptId })
|
|
10973
|
+
} : {},
|
|
10974
|
+
...spec.secretsFromLauncher && secrets ? { secrets: async (names) => makeStore(await secrets(names)) } : {},
|
|
10160
10975
|
codexExecutable: (path, containment) => {
|
|
10161
10976
|
const prefix = containment.scopePrefix(process.env);
|
|
10162
10977
|
return prefix ? shims.write([...prefix, path]) : path;
|
|
10163
10978
|
}
|
|
10164
10979
|
},
|
|
10165
10980
|
...spec.sandbox === "os" ? {
|
|
10166
|
-
harnessSandbox: async ({ cwd, log: turnLog }) => {
|
|
10981
|
+
harnessSandbox: async ({ cwd, runtime, log: turnLog }) => {
|
|
10167
10982
|
const sandbox = await prepareHarnessSandbox({
|
|
10168
10983
|
cwd,
|
|
10169
10984
|
baseUrl: input.baseUrl,
|
|
10985
|
+
runtime,
|
|
10986
|
+
policy: spec.sandboxPolicy ?? {},
|
|
10987
|
+
credentials: input.sandboxCredentials ?? [],
|
|
10170
10988
|
log: turnLog
|
|
10171
10989
|
});
|
|
10172
10990
|
if (sandbox) onSandbox?.(sandbox);
|
|
@@ -10259,6 +11077,7 @@ async function runTurn(env, deps) {
|
|
|
10259
11077
|
notifyStart: (info) => emit({ cabaneRunner: "start", ...info }),
|
|
10260
11078
|
notifyEnd: (info) => emit({ cabaneRunner: "end", ...info })
|
|
10261
11079
|
};
|
|
11080
|
+
let outbox;
|
|
10262
11081
|
const opts = runnerDispatcherOptions(input, log, emit, {
|
|
10263
11082
|
...deps.queryFn ? { queryFn: deps.queryFn } : {},
|
|
10264
11083
|
onSandbox: (s) => sandbox = s,
|
|
@@ -10267,7 +11086,10 @@ async function runTurn(env, deps) {
|
|
|
10267
11086
|
onProcessGroup: (pgid) => {
|
|
10268
11087
|
groups.push(pgid);
|
|
10269
11088
|
emit({ cabaneRunner: "process_group", pgid });
|
|
10270
|
-
}
|
|
11089
|
+
},
|
|
11090
|
+
bindAttempt: (attemptId) => channel.bindAttempt(attemptId),
|
|
11091
|
+
secrets: (names) => channel.secrets(names),
|
|
11092
|
+
onOutbox: (o, a) => outbox = { outbox: o, api: a }
|
|
10271
11093
|
});
|
|
10272
11094
|
const result = await new TurnExecution(opts, supervisor, input.dispatch, {
|
|
10273
11095
|
turnId: input.turnId,
|
|
@@ -10276,6 +11098,7 @@ async function runTurn(env, deps) {
|
|
|
10276
11098
|
shims.dispose();
|
|
10277
11099
|
await sandbox?.dispose();
|
|
10278
11100
|
});
|
|
11101
|
+
if (outbox) await drainBeforeExit(outbox.outbox, outbox.api, deps.outboxGraceMs, log);
|
|
10279
11102
|
emit({ cabaneRunner: "result", result });
|
|
10280
11103
|
return { code: result.ok ? 0 : 1, result };
|
|
10281
11104
|
} catch (err) {
|
|
@@ -10285,12 +11108,13 @@ async function runTurn(env, deps) {
|
|
|
10285
11108
|
deps.signals.off("SIGTERM", onTerm);
|
|
10286
11109
|
}
|
|
10287
11110
|
}
|
|
10288
|
-
var LAUNCHER_GONE_GRACE_MS;
|
|
11111
|
+
var OUTBOX_GRACE_MS, OUTBOX_RETRY_MS, LAUNCHER_GONE_GRACE_MS;
|
|
10289
11112
|
var init_run_turn = __esm({
|
|
10290
11113
|
"src/runner/run-turn.ts"() {
|
|
10291
11114
|
"use strict";
|
|
10292
11115
|
init_api();
|
|
10293
11116
|
init_outbox();
|
|
11117
|
+
init_secrets();
|
|
10294
11118
|
init_exec_shim();
|
|
10295
11119
|
init_harness_sandbox();
|
|
10296
11120
|
init_launcher_channel();
|
|
@@ -10298,6 +11122,8 @@ var init_run_turn = __esm({
|
|
|
10298
11122
|
init_transcript_writer();
|
|
10299
11123
|
init_turn_execution();
|
|
10300
11124
|
init_protocol();
|
|
11125
|
+
OUTBOX_GRACE_MS = 3e4;
|
|
11126
|
+
OUTBOX_RETRY_MS = 1e3;
|
|
10301
11127
|
LAUNCHER_GONE_GRACE_MS = 3e3;
|
|
10302
11128
|
}
|
|
10303
11129
|
});
|
|
@@ -10601,7 +11427,8 @@ function buildCompanionManifest(opts) {
|
|
|
10601
11427
|
return {
|
|
10602
11428
|
runtimes,
|
|
10603
11429
|
capabilities: { ...DEVICE_MANIFEST.capabilities },
|
|
10604
|
-
runtimeIssues: opts.runtimeIssues ?? []
|
|
11430
|
+
runtimeIssues: opts.runtimeIssues ?? [],
|
|
11431
|
+
...opts.runnerProvider ? { runner: { provider: opts.runnerProvider } } : {}
|
|
10605
11432
|
};
|
|
10606
11433
|
}
|
|
10607
11434
|
function runtimeIssuesFor(gates) {
|
|
@@ -11431,7 +12258,7 @@ async function pair(opts = {}) {
|
|
|
11431
12258
|
}
|
|
11432
12259
|
|
|
11433
12260
|
// src/cli.ts
|
|
11434
|
-
import { readFileSync as
|
|
12261
|
+
import { readFileSync as readFileSync14 } from "fs";
|
|
11435
12262
|
|
|
11436
12263
|
// src/commands/logs.ts
|
|
11437
12264
|
import { once } from "events";
|
|
@@ -11637,7 +12464,7 @@ import { hostname as hostname4 } from "os";
|
|
|
11637
12464
|
|
|
11638
12465
|
// src/runtime.ts
|
|
11639
12466
|
init_config();
|
|
11640
|
-
import { randomUUID as
|
|
12467
|
+
import { randomUUID as randomUUID7 } from "crypto";
|
|
11641
12468
|
|
|
11642
12469
|
// src/dashboard/server.ts
|
|
11643
12470
|
init_errors();
|
|
@@ -12277,7 +13104,7 @@ function turnFailureCopy(reason, runtime) {
|
|
|
12277
13104
|
init_api_error_shape();
|
|
12278
13105
|
init_boot_id();
|
|
12279
13106
|
init_api();
|
|
12280
|
-
import { randomUUID as
|
|
13107
|
+
import { randomUUID as randomUUID6 } from "crypto";
|
|
12281
13108
|
|
|
12282
13109
|
// src/device-api.ts
|
|
12283
13110
|
init_errors();
|
|
@@ -12549,56 +13376,976 @@ var ConnectorHealthStore = class {
|
|
|
12549
13376
|
lastError: h.lastError
|
|
12550
13377
|
}));
|
|
12551
13378
|
}
|
|
12552
|
-
};
|
|
12553
|
-
|
|
12554
|
-
// src/supervisor.ts
|
|
12555
|
-
init_dispatcher();
|
|
13379
|
+
};
|
|
13380
|
+
|
|
13381
|
+
// src/supervisor.ts
|
|
13382
|
+
init_dispatcher();
|
|
13383
|
+
|
|
13384
|
+
// src/providers/local-process.ts
|
|
13385
|
+
import { spawn as spawn6 } from "child_process";
|
|
13386
|
+
import { constants } from "os";
|
|
13387
|
+
var DEFAULT_KILL_GRACE_MS = 3e4;
|
|
13388
|
+
var DEFAULT_REAP_GRACE_MS = 3e3;
|
|
13389
|
+
function exitCode(code, signal) {
|
|
13390
|
+
if (code !== null) return code;
|
|
13391
|
+
const signum = signal ? constants.signals[signal] : void 0;
|
|
13392
|
+
return signum !== void 0 ? 128 + signum : 1;
|
|
13393
|
+
}
|
|
13394
|
+
function createLocalProcessProvider(opts = {}) {
|
|
13395
|
+
const killGraceMs = opts.killGraceMs ?? DEFAULT_KILL_GRACE_MS;
|
|
13396
|
+
const spawnFn = opts.spawnFn ?? spawn6;
|
|
13397
|
+
const reapGraceMs = opts.reapGraceMs ?? DEFAULT_REAP_GRACE_MS;
|
|
13398
|
+
const killFn = opts.killFn ?? ((pid, signal) => process.kill(pid, signal));
|
|
13399
|
+
const groupAlive = (pgid) => {
|
|
13400
|
+
try {
|
|
13401
|
+
killFn(-pgid, 0);
|
|
13402
|
+
return true;
|
|
13403
|
+
} catch {
|
|
13404
|
+
return false;
|
|
13405
|
+
}
|
|
13406
|
+
};
|
|
13407
|
+
const signalGroup = (pgid, signal) => {
|
|
13408
|
+
try {
|
|
13409
|
+
killFn(-pgid, signal);
|
|
13410
|
+
} catch {
|
|
13411
|
+
}
|
|
13412
|
+
};
|
|
13413
|
+
return {
|
|
13414
|
+
name: "local-process",
|
|
13415
|
+
async acquire(attempt) {
|
|
13416
|
+
return { provider: "local-process", id: attempt.turnId };
|
|
13417
|
+
},
|
|
13418
|
+
exec(_handle, argv, env, { signal, stdin }) {
|
|
13419
|
+
const [command, ...args] = argv;
|
|
13420
|
+
if (!command) throw new Error("local-process: exec needs a command");
|
|
13421
|
+
const child = spawnFn(command, args, {
|
|
13422
|
+
env: { ...env },
|
|
13423
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
13424
|
+
detached: process.platform !== "win32"
|
|
13425
|
+
});
|
|
13426
|
+
child.stdin?.on("error", () => {
|
|
13427
|
+
});
|
|
13428
|
+
if (stdin) child.stdin?.write(stdin);
|
|
13429
|
+
const exit = new Promise((resolve2) => {
|
|
13430
|
+
let settled = false;
|
|
13431
|
+
const done = (code) => {
|
|
13432
|
+
if (settled) return;
|
|
13433
|
+
settled = true;
|
|
13434
|
+
signal.removeEventListener("abort", stop2);
|
|
13435
|
+
if (forceTimer) clearTimeout(forceTimer);
|
|
13436
|
+
resolve2(code);
|
|
13437
|
+
};
|
|
13438
|
+
let forceTimer;
|
|
13439
|
+
const stop2 = () => {
|
|
13440
|
+
child.kill("SIGTERM");
|
|
13441
|
+
forceTimer = setTimeout(() => child.kill("SIGKILL"), killGraceMs);
|
|
13442
|
+
forceTimer.unref?.();
|
|
13443
|
+
};
|
|
13444
|
+
child.once("error", () => done(1));
|
|
13445
|
+
child.once("close", (code, sig) => done(exitCode(code, sig)));
|
|
13446
|
+
if (signal.aborted) stop2();
|
|
13447
|
+
else signal.addEventListener("abort", stop2, { once: true });
|
|
13448
|
+
});
|
|
13449
|
+
if (!child.stdout || !child.stderr || !child.stdin)
|
|
13450
|
+
throw new Error("local-process: runner stdio missing");
|
|
13451
|
+
const pid = child.pid;
|
|
13452
|
+
return {
|
|
13453
|
+
exit,
|
|
13454
|
+
...pid !== void 0 ? { pid } : {},
|
|
13455
|
+
stdout: child.stdout,
|
|
13456
|
+
stderr: child.stderr,
|
|
13457
|
+
stdin: child.stdin,
|
|
13458
|
+
async reap(groups) {
|
|
13459
|
+
if (process.platform === "win32") return;
|
|
13460
|
+
const targets = [.../* @__PURE__ */ new Set([...pid !== void 0 ? [pid] : [], ...groups])].filter(
|
|
13461
|
+
groupAlive
|
|
13462
|
+
);
|
|
13463
|
+
if (!targets.length) return;
|
|
13464
|
+
for (const pgid of targets) signalGroup(pgid, "SIGTERM");
|
|
13465
|
+
await new Promise((resolve2) => setTimeout(resolve2, reapGraceMs));
|
|
13466
|
+
for (const pgid of targets.filter(groupAlive)) signalGroup(pgid, "SIGKILL");
|
|
13467
|
+
}
|
|
13468
|
+
};
|
|
13469
|
+
},
|
|
13470
|
+
async release() {
|
|
13471
|
+
}
|
|
13472
|
+
};
|
|
13473
|
+
}
|
|
13474
|
+
|
|
13475
|
+
// src/providers/docker.ts
|
|
13476
|
+
import { createHash as createHash4, randomUUID as randomUUID5 } from "crypto";
|
|
13477
|
+
import { constants as constants2, tmpdir as tmpdir2 } from "os";
|
|
13478
|
+
import {
|
|
13479
|
+
createReadStream,
|
|
13480
|
+
createWriteStream as createWriteStream2,
|
|
13481
|
+
existsSync as existsSync13,
|
|
13482
|
+
mkdirSync as mkdirSync10,
|
|
13483
|
+
readdirSync as readdirSync4,
|
|
13484
|
+
readFileSync as readFileSync9,
|
|
13485
|
+
renameSync as renameSync5,
|
|
13486
|
+
rmSync as rmSync9,
|
|
13487
|
+
statSync as statSync3,
|
|
13488
|
+
writeFileSync as writeFileSync6
|
|
13489
|
+
} from "fs";
|
|
13490
|
+
import { join as join17 } from "path";
|
|
13491
|
+
import { Writable } from "stream";
|
|
13492
|
+
|
|
13493
|
+
// src/providers/docker-cli.ts
|
|
13494
|
+
import { spawn as spawn7 } from "child_process";
|
|
13495
|
+
function cliEnv(extra) {
|
|
13496
|
+
const keep = [
|
|
13497
|
+
"PATH",
|
|
13498
|
+
"HOME",
|
|
13499
|
+
"DOCKER_HOST",
|
|
13500
|
+
"DOCKER_CONFIG",
|
|
13501
|
+
"DOCKER_CONTEXT",
|
|
13502
|
+
"DOCKER_CERT_PATH"
|
|
13503
|
+
];
|
|
13504
|
+
const env = {};
|
|
13505
|
+
for (const k of keep) if (process.env[k] !== void 0) env[k] = process.env[k];
|
|
13506
|
+
return { ...env, ...extra };
|
|
13507
|
+
}
|
|
13508
|
+
function dockerCli(bin = "docker") {
|
|
13509
|
+
return {
|
|
13510
|
+
run(args, opts = {}) {
|
|
13511
|
+
return new Promise((resolve2) => {
|
|
13512
|
+
let child;
|
|
13513
|
+
try {
|
|
13514
|
+
child = spawn7(bin, [...args], {
|
|
13515
|
+
env: cliEnv(opts.env),
|
|
13516
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
13517
|
+
});
|
|
13518
|
+
} catch (err) {
|
|
13519
|
+
resolve2({ code: 127, stdout: "", stderr: String(err) });
|
|
13520
|
+
return;
|
|
13521
|
+
}
|
|
13522
|
+
let stdout = "";
|
|
13523
|
+
let stderr = "";
|
|
13524
|
+
const timer = opts.timeoutMs ? setTimeout(() => child.kill("SIGKILL"), opts.timeoutMs) : void 0;
|
|
13525
|
+
if (opts.stdout) child.stdout?.pipe(opts.stdout);
|
|
13526
|
+
else child.stdout?.on("data", (chunk) => stdout += String(chunk));
|
|
13527
|
+
child.stderr?.on("data", (chunk) => stderr += String(chunk));
|
|
13528
|
+
child.stdin?.on("error", () => {
|
|
13529
|
+
});
|
|
13530
|
+
child.once("error", (err) => {
|
|
13531
|
+
if (timer) clearTimeout(timer);
|
|
13532
|
+
resolve2({ code: 127, stdout, stderr: stderr || err.message });
|
|
13533
|
+
});
|
|
13534
|
+
child.once("close", (code, signal) => {
|
|
13535
|
+
if (timer) clearTimeout(timer);
|
|
13536
|
+
resolve2({ code: code ?? (signal ? 137 : 1), stdout, stderr });
|
|
13537
|
+
});
|
|
13538
|
+
const input = opts.input;
|
|
13539
|
+
if (input === void 0) child.stdin?.end();
|
|
13540
|
+
else if (typeof input === "string" || Buffer.isBuffer(input)) child.stdin?.end(input);
|
|
13541
|
+
else input.pipe(child.stdin);
|
|
13542
|
+
});
|
|
13543
|
+
},
|
|
13544
|
+
spawn(args, env) {
|
|
13545
|
+
return spawn7(bin, [...args], { env: cliEnv(env), stdio: ["pipe", "pipe", "pipe"] });
|
|
13546
|
+
}
|
|
13547
|
+
};
|
|
13548
|
+
}
|
|
13549
|
+
|
|
13550
|
+
// src/providers/docker.ts
|
|
13551
|
+
init_git_credential();
|
|
13552
|
+
|
|
13553
|
+
// src/runner/collected-outbox.ts
|
|
13554
|
+
import {
|
|
13555
|
+
copyFileSync as copyFileSync2,
|
|
13556
|
+
existsSync as existsSync11,
|
|
13557
|
+
mkdirSync as mkdirSync9,
|
|
13558
|
+
readdirSync as readdirSync3,
|
|
13559
|
+
readFileSync as readFileSync8,
|
|
13560
|
+
renameSync as renameSync4,
|
|
13561
|
+
rmSync as rmSync8
|
|
13562
|
+
} from "fs";
|
|
13563
|
+
import { join as join15 } from "path";
|
|
13564
|
+
function routes(t) {
|
|
13565
|
+
const conv = `/api/workspaces/${t.workspaceId}/conversations/${t.conversationId}`;
|
|
13566
|
+
const agent = `${conv}/participants/agents/${t.agentId}`;
|
|
13567
|
+
return {
|
|
13568
|
+
message: { method: "POST", path: `${conv}/messages` },
|
|
13569
|
+
activity: { method: "POST", path: `${agent}/activity` },
|
|
13570
|
+
thinking: { method: "POST", path: `${agent}/thinking` },
|
|
13571
|
+
text: { method: "POST", path: `${agent}/text` },
|
|
13572
|
+
"active-run": { method: "PATCH", path: `${agent}/active-run` }
|
|
13573
|
+
};
|
|
13574
|
+
}
|
|
13575
|
+
function admitCollectedEntry(raw, turn) {
|
|
13576
|
+
if (!raw || typeof raw !== "object") return null;
|
|
13577
|
+
const entry = raw;
|
|
13578
|
+
const kind = entry.kind;
|
|
13579
|
+
if (!kind || !(kind in routes(turn))) return null;
|
|
13580
|
+
const route = routes(turn)[kind];
|
|
13581
|
+
if (entry.method !== route.method || entry.path !== route.path) return null;
|
|
13582
|
+
if (typeof entry.seq !== "number" || typeof entry.enqueuedAt !== "number") return null;
|
|
13583
|
+
const body = entry.body;
|
|
13584
|
+
if (!body || typeof body !== "object" || typeof body.turnId !== "string") return null;
|
|
13585
|
+
if (!turn.turnIds.includes(body.turnId)) return null;
|
|
13586
|
+
if (kind === "active-run") {
|
|
13587
|
+
if (body.activeRunStartedAt !== null) return null;
|
|
13588
|
+
if (entry.turnId !== `activerun:${turn.conversationId}:${turn.agentId}`) return null;
|
|
13589
|
+
} else if (entry.turnId !== body.turnId) {
|
|
13590
|
+
return null;
|
|
13591
|
+
}
|
|
13592
|
+
return entry;
|
|
13593
|
+
}
|
|
13594
|
+
var atomicMove = (from, to) => {
|
|
13595
|
+
try {
|
|
13596
|
+
renameSync4(from, to);
|
|
13597
|
+
} catch {
|
|
13598
|
+
const partial = `${to}.partial`;
|
|
13599
|
+
copyFileSync2(from, partial);
|
|
13600
|
+
renameSync4(partial, to);
|
|
13601
|
+
rmSync8(from, { force: true });
|
|
13602
|
+
}
|
|
13603
|
+
};
|
|
13604
|
+
function publishStaged(from, to, move = atomicMove) {
|
|
13605
|
+
if (!existsSync11(from)) return { published: 0, complete: true };
|
|
13606
|
+
const staged = readdirSync3(from).filter((n) => n.endsWith(".json")).map((name) => {
|
|
13607
|
+
let entry = {};
|
|
13608
|
+
try {
|
|
13609
|
+
entry = JSON.parse(readFileSync8(join15(from, name), "utf8"));
|
|
13610
|
+
} catch {
|
|
13611
|
+
}
|
|
13612
|
+
return { name, entry };
|
|
13613
|
+
});
|
|
13614
|
+
const settle = (e) => e.kind === "active-run" ? 1 : 0;
|
|
13615
|
+
staged.sort(
|
|
13616
|
+
(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)
|
|
13617
|
+
);
|
|
13618
|
+
if (staged.length) mkdirSync9(to, { recursive: true });
|
|
13619
|
+
let published = 0;
|
|
13620
|
+
for (const { name } of staged) {
|
|
13621
|
+
try {
|
|
13622
|
+
move(join15(from, name), join15(to, name));
|
|
13623
|
+
} catch {
|
|
13624
|
+
return { published, complete: false };
|
|
13625
|
+
}
|
|
13626
|
+
published++;
|
|
13627
|
+
}
|
|
13628
|
+
rmSync8(from, { recursive: true, force: true });
|
|
13629
|
+
return { published, complete: true };
|
|
13630
|
+
}
|
|
13631
|
+
|
|
13632
|
+
// src/providers/runner-base.ts
|
|
13633
|
+
import { createHash as createHash3 } from "crypto";
|
|
13634
|
+
import { existsSync as existsSync12, realpathSync as realpathSync2 } from "fs";
|
|
13635
|
+
import { dirname as dirname7, join as join16, sep } from "path";
|
|
13636
|
+
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
13637
|
+
var COMPANION_MOUNT = "/opt/cabane/companion";
|
|
13638
|
+
var BASE_DOCKERFILE = `FROM node:22-slim
|
|
13639
|
+
RUN apt-get update \\
|
|
13640
|
+
&& apt-get install -y --no-install-recommends git curl ca-certificates procps \\
|
|
13641
|
+
&& rm -rf /var/lib/apt/lists/* \\
|
|
13642
|
+
&& corepack enable
|
|
13643
|
+
RUN usermod -l runner -d /home/runner -m node \\
|
|
13644
|
+
&& groupmod -n runner node \\
|
|
13645
|
+
&& mkdir -p /work /var/lib/cabane-runner /run/cabane ${COMPANION_MOUNT} \\
|
|
13646
|
+
&& chown runner:runner /work /var/lib/cabane-runner
|
|
13647
|
+
RUN mkdir -p /home/runner/.codex /opt/cabane \\
|
|
13648
|
+
&& printf '[features]\\nuse_legacy_landlock = true\\n' > /opt/cabane/codex-config.toml \\
|
|
13649
|
+
&& cp /opt/cabane/codex-config.toml /home/runner/.codex/config.toml \\
|
|
13650
|
+
&& chown -R runner:runner /home/runner/.codex
|
|
13651
|
+
RUN printf '#!/bin/sh\\nexec node "\${CABANE_COMPANION_ROOT:-${COMPANION_MOUNT}}/dist/cli.js" "$@"\\n' > /usr/local/bin/cabane-companion \\
|
|
13652
|
+
&& chmod 755 /usr/local/bin/cabane-companion
|
|
13653
|
+
USER runner
|
|
13654
|
+
WORKDIR /home/runner
|
|
13655
|
+
CMD ["sleep", "infinity"]
|
|
13656
|
+
`;
|
|
13657
|
+
function baseImageTag(dockerfile = BASE_DOCKERFILE) {
|
|
13658
|
+
return `cabane-runner-base:${createHash3("sha256").update(dockerfile).digest("hex").slice(0, 12)}`;
|
|
13659
|
+
}
|
|
13660
|
+
async function ensureBaseImage(docker, image, log) {
|
|
13661
|
+
if ((await docker.run(["image", "inspect", image])).code === 0) return true;
|
|
13662
|
+
if (image !== baseImageTag()) {
|
|
13663
|
+
log.error({ image }, "runners: the configured runner image isn't on this engine");
|
|
13664
|
+
return false;
|
|
13665
|
+
}
|
|
13666
|
+
log.info({ image }, "runners: building the runner base image (once per base)");
|
|
13667
|
+
const built = await docker.run(["build", "-t", image, "-"], {
|
|
13668
|
+
input: BASE_DOCKERFILE,
|
|
13669
|
+
timeoutMs: 20 * 6e4
|
|
13670
|
+
});
|
|
13671
|
+
if (built.code !== 0) {
|
|
13672
|
+
log.error(
|
|
13673
|
+
{ image, err: built.stderr.trim().slice(-2e3) },
|
|
13674
|
+
"runners: couldn't build the runner base image; no turn can run until it is"
|
|
13675
|
+
);
|
|
13676
|
+
return false;
|
|
13677
|
+
}
|
|
13678
|
+
return true;
|
|
13679
|
+
}
|
|
13680
|
+
function ownInstall(moduleFile = fileURLToPath3(import.meta.url)) {
|
|
13681
|
+
let dir2 = dirname7(moduleFile);
|
|
13682
|
+
for (let i = 0; i < 4; i++) {
|
|
13683
|
+
if (existsSync12(join16(dir2, "dist", "cli.js")) && existsSync12(join16(dir2, "package.json"))) break;
|
|
13684
|
+
dir2 = dirname7(dir2);
|
|
13685
|
+
}
|
|
13686
|
+
if (!existsSync12(join16(dir2, "dist", "cli.js"))) return null;
|
|
13687
|
+
const sdk = join16(dir2, "node_modules", "@anthropic-ai", "claude-agent-sdk");
|
|
13688
|
+
if (!existsSync12(sdk)) return null;
|
|
13689
|
+
const root = realpathSync2(dir2);
|
|
13690
|
+
return realpathSync2(sdk).startsWith(root + sep) ? root : null;
|
|
13691
|
+
}
|
|
13692
|
+
function companionSource(input) {
|
|
13693
|
+
if (input.configured) {
|
|
13694
|
+
return { kind: "dir", path: input.configured, key: `dir:${input.configured}` };
|
|
13695
|
+
}
|
|
13696
|
+
if (input.platform === "linux") {
|
|
13697
|
+
if (input.own)
|
|
13698
|
+
return { kind: "dir", path: input.own, key: `dir:${input.own}:${input.version}` };
|
|
13699
|
+
return {
|
|
13700
|
+
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"
|
|
13701
|
+
};
|
|
13702
|
+
}
|
|
13703
|
+
if (input.version === "0.0.0") {
|
|
13704
|
+
return {
|
|
13705
|
+
error: "an unreleased companion can't be installed into a container from npm; set runners.docker.companionDir to a Linux install of it"
|
|
13706
|
+
};
|
|
13707
|
+
}
|
|
13708
|
+
return {
|
|
13709
|
+
kind: "volume",
|
|
13710
|
+
volume: `cabane-companion-${input.version}`,
|
|
13711
|
+
version: input.version,
|
|
13712
|
+
key: `volume:${input.version}`
|
|
13713
|
+
};
|
|
13714
|
+
}
|
|
13715
|
+
function companionRoot(source) {
|
|
13716
|
+
return source.kind === "dir" ? COMPANION_MOUNT : `${COMPANION_MOUNT}/lib/node_modules/@cabane/companion`;
|
|
13717
|
+
}
|
|
13718
|
+
function companionMountArgs(source) {
|
|
13719
|
+
const mount = source.kind === "dir" ? `type=bind,src=${source.path},dst=${COMPANION_MOUNT},readonly` : `type=volume,src=${source.volume},dst=${COMPANION_MOUNT},readonly`;
|
|
13720
|
+
return ["--mount", mount, "-e", `CABANE_COMPANION_ROOT=${companionRoot(source)}`];
|
|
13721
|
+
}
|
|
13722
|
+
var INSTALL_MARKER = `${COMPANION_MOUNT}/.cabane-install-complete`;
|
|
13723
|
+
function volumeInstallScript(version, install = `npm install -g --prefix ${COMPANION_MOUNT} @cabane/companion@${version}`) {
|
|
13724
|
+
const root = `${COMPANION_MOUNT}/lib/node_modules/@cabane/companion`;
|
|
13725
|
+
return [
|
|
13726
|
+
`find ${COMPANION_MOUNT} -mindepth 1 -delete`,
|
|
13727
|
+
install,
|
|
13728
|
+
// Installed is not the same as runnable: the package must start and say
|
|
13729
|
+
// what it runs before the volume counts as done.
|
|
13730
|
+
`node ${root}/dist/cli.js run-turn --manifest > /dev/null`,
|
|
13731
|
+
`printf %s ${version} > ${INSTALL_MARKER}`
|
|
13732
|
+
].join(" && ");
|
|
13733
|
+
}
|
|
13734
|
+
var volumeMount = (source) => `type=volume,src=${source.volume},dst=${COMPANION_MOUNT}`;
|
|
13735
|
+
async function ensureCompanionVolume(docker, image, source, log, script = volumeInstallScript) {
|
|
13736
|
+
if (source.kind !== "volume") return true;
|
|
13737
|
+
const done = await docker.run([
|
|
13738
|
+
"run",
|
|
13739
|
+
"--rm",
|
|
13740
|
+
"--network",
|
|
13741
|
+
"none",
|
|
13742
|
+
"--mount",
|
|
13743
|
+
volumeMount(source),
|
|
13744
|
+
image,
|
|
13745
|
+
"sh",
|
|
13746
|
+
"-c",
|
|
13747
|
+
`[ "$(cat ${INSTALL_MARKER} 2>/dev/null)" = "$1" ]`,
|
|
13748
|
+
"sh",
|
|
13749
|
+
source.version
|
|
13750
|
+
]);
|
|
13751
|
+
if (done.code === 0) return true;
|
|
13752
|
+
log.info(
|
|
13753
|
+
{ version: source.version },
|
|
13754
|
+
"runners: installing this companion version for its containers"
|
|
13755
|
+
);
|
|
13756
|
+
const installed = await docker.run(
|
|
13757
|
+
[
|
|
13758
|
+
"run",
|
|
13759
|
+
"--rm",
|
|
13760
|
+
"-u",
|
|
13761
|
+
"root",
|
|
13762
|
+
"--mount",
|
|
13763
|
+
volumeMount(source),
|
|
13764
|
+
image,
|
|
13765
|
+
"sh",
|
|
13766
|
+
"-c",
|
|
13767
|
+
script(source.version)
|
|
13768
|
+
],
|
|
13769
|
+
{ timeoutMs: 20 * 6e4 }
|
|
13770
|
+
);
|
|
13771
|
+
if (installed.code !== 0) {
|
|
13772
|
+
log.error(
|
|
13773
|
+
{ version: source.version, err: installed.stderr.trim().slice(-2e3) },
|
|
13774
|
+
"runners: couldn't install the companion for its containers; the next start tries again"
|
|
13775
|
+
);
|
|
13776
|
+
return false;
|
|
13777
|
+
}
|
|
13778
|
+
return true;
|
|
13779
|
+
}
|
|
13780
|
+
async function invalidateCompanionVolume(docker, image, source) {
|
|
13781
|
+
if (source.kind !== "volume") return;
|
|
13782
|
+
await docker.run([
|
|
13783
|
+
"run",
|
|
13784
|
+
"--rm",
|
|
13785
|
+
"-u",
|
|
13786
|
+
"root",
|
|
13787
|
+
"--network",
|
|
13788
|
+
"none",
|
|
13789
|
+
"--mount",
|
|
13790
|
+
volumeMount(source),
|
|
13791
|
+
image,
|
|
13792
|
+
"rm",
|
|
13793
|
+
"-f",
|
|
13794
|
+
INSTALL_MARKER
|
|
13795
|
+
]);
|
|
13796
|
+
}
|
|
12556
13797
|
|
|
12557
|
-
// src/providers/
|
|
12558
|
-
|
|
12559
|
-
|
|
12560
|
-
var
|
|
12561
|
-
var
|
|
12562
|
-
|
|
12563
|
-
|
|
12564
|
-
|
|
12565
|
-
|
|
13798
|
+
// src/providers/docker.ts
|
|
13799
|
+
var CONTAINER_HOME = "/home/runner";
|
|
13800
|
+
var CONTAINER_WORK = "/work";
|
|
13801
|
+
var CONTAINER_STATE = "/var/lib/cabane-runner";
|
|
13802
|
+
var CONTAINER_SOCKETS = "/run/cabane";
|
|
13803
|
+
var PROXY_ALIAS = "cabane-egress";
|
|
13804
|
+
var PROXY_PORT = 3128;
|
|
13805
|
+
var DEFAULT_KILL_GRACE_MS2 = 3e4;
|
|
13806
|
+
var REAP_GRACE_S = 3;
|
|
13807
|
+
var DEFAULT_SESSION_CAP_BYTES = 512 * 1024 * 1024;
|
|
13808
|
+
var COLLECT_CAP_BYTES = 128 * 1024 * 1024;
|
|
13809
|
+
var UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
13810
|
+
function containerName(attempt) {
|
|
13811
|
+
return `cabane-runner-${attempt.conversationId}-${attempt.agentId}`;
|
|
13812
|
+
}
|
|
13813
|
+
function sessionArchivePath(deviceDir, attemptId) {
|
|
13814
|
+
if (!UUID_RE.test(attemptId)) throw new Error(`docker: not an attempt id: ${attemptId}`);
|
|
13815
|
+
return join17(deviceDir, "attempts", attemptId, "session.tar");
|
|
13816
|
+
}
|
|
13817
|
+
function pruneOtherAttempts(deviceDir, pair2, keep) {
|
|
13818
|
+
const root = join17(deviceDir, "attempts");
|
|
13819
|
+
if (!existsSync13(root)) return;
|
|
13820
|
+
for (const id of readdirSync4(root)) {
|
|
13821
|
+
if (id === keep || !UUID_RE.test(id)) continue;
|
|
13822
|
+
let pairOf = "";
|
|
13823
|
+
try {
|
|
13824
|
+
pairOf = readFileSync9(join17(root, id, "pair"), "utf8");
|
|
13825
|
+
} catch {
|
|
13826
|
+
continue;
|
|
13827
|
+
}
|
|
13828
|
+
if (pairOf === `${pair2.conversationId}/${pair2.agentId}`) {
|
|
13829
|
+
rmSync9(join17(root, id), { recursive: true, force: true });
|
|
13830
|
+
}
|
|
13831
|
+
}
|
|
12566
13832
|
}
|
|
12567
|
-
|
|
12568
|
-
|
|
12569
|
-
|
|
12570
|
-
|
|
12571
|
-
|
|
12572
|
-
|
|
13833
|
+
var CappedFile = class extends Writable {
|
|
13834
|
+
constructor(path, cap) {
|
|
13835
|
+
super();
|
|
13836
|
+
this.path = path;
|
|
13837
|
+
this.cap = cap;
|
|
13838
|
+
this.out = createWriteStream2(path);
|
|
13839
|
+
}
|
|
13840
|
+
path;
|
|
13841
|
+
cap;
|
|
13842
|
+
bytes = 0;
|
|
13843
|
+
over = false;
|
|
13844
|
+
out;
|
|
13845
|
+
_write(chunk, _enc, done) {
|
|
13846
|
+
this.bytes += chunk.length;
|
|
13847
|
+
if (this.bytes > this.cap) {
|
|
13848
|
+
this.over = true;
|
|
13849
|
+
done();
|
|
13850
|
+
return;
|
|
13851
|
+
}
|
|
13852
|
+
this.out.write(chunk, done);
|
|
13853
|
+
}
|
|
13854
|
+
_final(done) {
|
|
13855
|
+
this.out.end(done);
|
|
13856
|
+
}
|
|
13857
|
+
};
|
|
13858
|
+
var DUMP_SCRIPT = `
|
|
13859
|
+
const fs = require('fs'), path = require('path');
|
|
13860
|
+
const dir = process.argv[1], cap = Number(process.argv[2]);
|
|
13861
|
+
let names;
|
|
13862
|
+
try { names = fs.readdirSync(dir); }
|
|
13863
|
+
catch (e) {
|
|
13864
|
+
if (e.code === 'ENOENT') { process.stdout.write(JSON.stringify({ complete: true, files: [] })); process.exit(0); }
|
|
13865
|
+
process.stderr.write(String(e.message)); process.exit(2);
|
|
13866
|
+
}
|
|
13867
|
+
let total = 0, complete = true; const files = [];
|
|
13868
|
+
for (const name of names) {
|
|
13869
|
+
const p = path.join(dir, name);
|
|
13870
|
+
let st;
|
|
13871
|
+
try { st = fs.lstatSync(p); } catch (e) { process.stderr.write(String(e.message)); process.exit(2); }
|
|
13872
|
+
if (!st.isFile()) continue;
|
|
13873
|
+
// Always at least one file per read, so one over the cap is still collected.
|
|
13874
|
+
if (files.length && total + st.size > cap) { complete = false; continue; }
|
|
13875
|
+
let data;
|
|
13876
|
+
try { data = fs.readFileSync(p); } catch (e) { process.stderr.write(String(e.message)); process.exit(2); }
|
|
13877
|
+
total += st.size;
|
|
13878
|
+
files.push({ name, data: data.toString('base64') });
|
|
13879
|
+
}
|
|
13880
|
+
process.stdout.write(JSON.stringify({ complete, files }));
|
|
13881
|
+
`;
|
|
13882
|
+
var SAFE_NAME_RE = /^[\w.%@+-]+$/;
|
|
13883
|
+
function createDockerProvider(opts) {
|
|
13884
|
+
const docker = opts.docker ?? dockerCli();
|
|
13885
|
+
const log = opts.log;
|
|
13886
|
+
const killGraceMs = opts.killGraceMs ?? DEFAULT_KILL_GRACE_MS2;
|
|
13887
|
+
const sessionCap = opts.sessionCapBytes ?? DEFAULT_SESSION_CAP_BYTES;
|
|
13888
|
+
const collectCap = opts.collectCapBytes ?? COLLECT_CAP_BYTES;
|
|
13889
|
+
const network = `cabane-runners-${opts.deviceKey}`;
|
|
13890
|
+
const companionLabel = createHash4("sha256").update(opts.companion.key).digest("hex").slice(0, 16);
|
|
13891
|
+
const proxy = `cabane-egress-${opts.deviceKey}`;
|
|
13892
|
+
const held = /* @__PURE__ */ new Map();
|
|
13893
|
+
const queues = /* @__PURE__ */ new Map();
|
|
13894
|
+
const must = async (args, what) => {
|
|
13895
|
+
const r = await docker.run(args);
|
|
13896
|
+
if (r.code !== 0)
|
|
13897
|
+
throw new Error(`docker: ${what} failed: ${r.stderr.trim() || `exit ${r.code}`}`);
|
|
13898
|
+
return r.stdout;
|
|
13899
|
+
};
|
|
13900
|
+
const execIn = (name, args, extra = {}) => docker.run(
|
|
13901
|
+
["exec", ...extra.input !== void 0 ? ["-i"] : [], "-u", "runner", name, ...args],
|
|
13902
|
+
extra
|
|
13903
|
+
);
|
|
13904
|
+
const proxyListening = async () => {
|
|
13905
|
+
for (let i = 0; i < 40; i++) {
|
|
13906
|
+
const r = await docker.run([
|
|
13907
|
+
"exec",
|
|
13908
|
+
proxy,
|
|
13909
|
+
"curl",
|
|
13910
|
+
"-s",
|
|
13911
|
+
"-o",
|
|
13912
|
+
"/dev/null",
|
|
13913
|
+
"--max-time",
|
|
13914
|
+
"1",
|
|
13915
|
+
`http://127.0.0.1:${PROXY_PORT}/`
|
|
13916
|
+
]);
|
|
13917
|
+
if (r.code === 0) return;
|
|
13918
|
+
await new Promise((resolve2) => setTimeout(resolve2, 250));
|
|
13919
|
+
}
|
|
13920
|
+
throw new Error("docker: the egress proxy never started listening");
|
|
13921
|
+
};
|
|
13922
|
+
let networkReady = null;
|
|
13923
|
+
const proxyKey = async () => {
|
|
13924
|
+
const id = (await docker.run(["image", "inspect", "-f", "{{.Id}}", opts.image])).stdout.trim();
|
|
13925
|
+
return createHash4("sha256").update(JSON.stringify([id || opts.image, opts.companion.key, [...opts.egressAllow].sort()])).digest("hex").slice(0, 16);
|
|
13926
|
+
};
|
|
13927
|
+
const ensureNetwork = () => {
|
|
13928
|
+
networkReady ??= (async () => {
|
|
13929
|
+
const allowKey = await proxyKey();
|
|
13930
|
+
if ((await docker.run(["network", "inspect", network])).code !== 0) {
|
|
13931
|
+
await must(
|
|
13932
|
+
[
|
|
13933
|
+
"network",
|
|
13934
|
+
"create",
|
|
13935
|
+
// No route off the host, and no address of the host's on the
|
|
13936
|
+
// bridge: the host's own ports are out of reach too. The proxy is
|
|
13937
|
+
// the only other thing on the network.
|
|
13938
|
+
"--internal",
|
|
13939
|
+
"-o",
|
|
13940
|
+
"com.docker.network.bridge.inhibit_ipv4=true",
|
|
13941
|
+
"--label",
|
|
13942
|
+
`cabane.device=${opts.deviceKey}`,
|
|
13943
|
+
network
|
|
13944
|
+
],
|
|
13945
|
+
"creating the runners network"
|
|
13946
|
+
);
|
|
13947
|
+
}
|
|
13948
|
+
const seen = await docker.run([
|
|
13949
|
+
"inspect",
|
|
13950
|
+
"-f",
|
|
13951
|
+
'{{index .Config.Labels "cabane.allow"}} {{.State.Running}}',
|
|
13952
|
+
proxy
|
|
13953
|
+
]);
|
|
13954
|
+
const [key, running] = seen.stdout.trim().split(" ");
|
|
13955
|
+
if (seen.code === 0 && key === allowKey) {
|
|
13956
|
+
if (running !== "true") {
|
|
13957
|
+
await must(["start", proxy], "starting the egress proxy");
|
|
13958
|
+
await proxyListening();
|
|
13959
|
+
}
|
|
13960
|
+
return;
|
|
13961
|
+
}
|
|
13962
|
+
if (seen.code === 0) await docker.run(["rm", "-f", proxy]);
|
|
13963
|
+
await must(
|
|
13964
|
+
[
|
|
13965
|
+
"run",
|
|
13966
|
+
"-d",
|
|
13967
|
+
"--name",
|
|
13968
|
+
proxy,
|
|
13969
|
+
"--label",
|
|
13970
|
+
`cabane.allow=${allowKey}`,
|
|
13971
|
+
"--label",
|
|
13972
|
+
`cabane.device=${opts.deviceKey}`,
|
|
13973
|
+
"--restart",
|
|
13974
|
+
"unless-stopped",
|
|
13975
|
+
"--cap-drop",
|
|
13976
|
+
"ALL",
|
|
13977
|
+
"--security-opt",
|
|
13978
|
+
"no-new-privileges",
|
|
13979
|
+
"-e",
|
|
13980
|
+
`CABANE_EGRESS_ALLOW=${JSON.stringify(opts.egressAllow)}`,
|
|
13981
|
+
...companionMountArgs(opts.companion),
|
|
13982
|
+
opts.image,
|
|
13983
|
+
"cabane-companion",
|
|
13984
|
+
"egress-proxy",
|
|
13985
|
+
"--port",
|
|
13986
|
+
String(PROXY_PORT)
|
|
13987
|
+
],
|
|
13988
|
+
"starting the egress proxy"
|
|
13989
|
+
);
|
|
13990
|
+
await must(
|
|
13991
|
+
["network", "connect", "--alias", PROXY_ALIAS, network, proxy],
|
|
13992
|
+
"connecting the proxy"
|
|
13993
|
+
);
|
|
13994
|
+
await proxyListening();
|
|
13995
|
+
log.info({ network, proxy, allow: opts.egressAllow }, "runners: egress proxy ready");
|
|
13996
|
+
})().catch((err) => {
|
|
13997
|
+
networkReady = null;
|
|
13998
|
+
throw err;
|
|
13999
|
+
});
|
|
14000
|
+
return networkReady;
|
|
14001
|
+
};
|
|
14002
|
+
const socketRoot = opts.socketRoot ?? join17(
|
|
14003
|
+
process.env.XDG_RUNTIME_DIR && existsSync13(process.env.XDG_RUNTIME_DIR) ? process.env.XDG_RUNTIME_DIR : tmpdir2(),
|
|
14004
|
+
`cabane-runners-${opts.deviceKey}`
|
|
14005
|
+
);
|
|
14006
|
+
const socketDir = (name) => join17(socketRoot, createHash4("sha256").update(name).digest("hex").slice(0, 12));
|
|
14007
|
+
const create = async (name, attempt) => {
|
|
14008
|
+
mkdirSync10(socketRoot, { recursive: true, mode: 448 });
|
|
14009
|
+
mkdirSync10(socketDir(name), { recursive: true, mode: 457 });
|
|
14010
|
+
const mounts = (opts.mounts ?? []).filter((m) => m.source.startsWith("/") && existsSync13(m.source)).flatMap((m) => ["--mount", `type=bind,src=${m.source},dst=${m.target},readonly`]);
|
|
14011
|
+
await must(
|
|
14012
|
+
[
|
|
14013
|
+
"create",
|
|
14014
|
+
"--name",
|
|
14015
|
+
name,
|
|
14016
|
+
"--init",
|
|
14017
|
+
"--hostname",
|
|
14018
|
+
"cabane-runner",
|
|
14019
|
+
"--label",
|
|
14020
|
+
"cabane.runner=1",
|
|
14021
|
+
"--label",
|
|
14022
|
+
`cabane.device=${opts.deviceKey}`,
|
|
14023
|
+
"--label",
|
|
14024
|
+
`cabane.workspace=${attempt.workspaceId}`,
|
|
14025
|
+
"--label",
|
|
14026
|
+
`cabane.conversation=${attempt.conversationId}`,
|
|
14027
|
+
"--label",
|
|
14028
|
+
`cabane.agent=${attempt.agentId}`,
|
|
14029
|
+
"--label",
|
|
14030
|
+
`cabane.companion=${companionLabel}`,
|
|
14031
|
+
"--network",
|
|
14032
|
+
network,
|
|
14033
|
+
"--mount",
|
|
14034
|
+
`type=volume,src=${name}-home,dst=${CONTAINER_HOME}`,
|
|
14035
|
+
"--mount",
|
|
14036
|
+
`type=volume,src=${name}-work,dst=${CONTAINER_WORK}`,
|
|
14037
|
+
"--mount",
|
|
14038
|
+
`type=bind,src=${socketDir(name)},dst=${CONTAINER_SOCKETS},readonly`,
|
|
14039
|
+
...mounts,
|
|
14040
|
+
...companionMountArgs(opts.companion),
|
|
14041
|
+
"--cap-drop",
|
|
14042
|
+
"ALL",
|
|
14043
|
+
"--security-opt",
|
|
14044
|
+
"no-new-privileges",
|
|
14045
|
+
"--pids-limit",
|
|
14046
|
+
"4096",
|
|
14047
|
+
opts.image,
|
|
14048
|
+
"sleep",
|
|
14049
|
+
"infinity"
|
|
14050
|
+
],
|
|
14051
|
+
"creating the container"
|
|
14052
|
+
);
|
|
14053
|
+
};
|
|
14054
|
+
const destroy = async (name) => {
|
|
14055
|
+
await docker.run(["rm", "-f", "-v", name]);
|
|
14056
|
+
await docker.run(["volume", "rm", "-f", `${name}-home`, `${name}-work`]);
|
|
14057
|
+
rmSync9(socketDir(name), { recursive: true, force: true });
|
|
14058
|
+
};
|
|
14059
|
+
const containerEnv = () => {
|
|
14060
|
+
const proxyUrl = `http://${PROXY_ALIAS}:${PROXY_PORT}`;
|
|
14061
|
+
const git = [
|
|
14062
|
+
["credential.helper", ""],
|
|
14063
|
+
// The helper speaks a socket, not the network: no proxy, and none of
|
|
14064
|
+
// Node's warning about it in git's output.
|
|
14065
|
+
["credential.helper", "!NODE_USE_ENV_PROXY= cabane-companion git-credential"],
|
|
14066
|
+
...opts.gitHosts.flatMap((h) => [
|
|
14067
|
+
[`url.https://${h}/.insteadOf`, `git@${h}:`],
|
|
14068
|
+
[`url.https://${h}/.insteadOf`, `ssh://git@${h}/`]
|
|
14069
|
+
])
|
|
14070
|
+
];
|
|
14071
|
+
return {
|
|
14072
|
+
HOME: CONTAINER_HOME,
|
|
14073
|
+
CABANE_COMPANION_HOME: CONTAINER_STATE,
|
|
14074
|
+
CABANE_RUNNER_STATE_DIR: CONTAINER_STATE,
|
|
14075
|
+
HTTPS_PROXY: proxyUrl,
|
|
14076
|
+
HTTP_PROXY: proxyUrl,
|
|
14077
|
+
https_proxy: proxyUrl,
|
|
14078
|
+
http_proxy: proxyUrl,
|
|
14079
|
+
NO_PROXY: "localhost,127.0.0.1,::1",
|
|
14080
|
+
no_proxy: "localhost,127.0.0.1,::1",
|
|
14081
|
+
// Node's own fetch — the runner's wire — honours the proxy only when asked.
|
|
14082
|
+
NODE_USE_ENV_PROXY: "1",
|
|
14083
|
+
CABANE_GIT_CREDENTIAL_SOCKET: `${CONTAINER_SOCKETS}/git.sock`,
|
|
14084
|
+
GIT_TERMINAL_PROMPT: "0",
|
|
14085
|
+
GIT_CONFIG_COUNT: String(git.length),
|
|
14086
|
+
...Object.fromEntries(
|
|
14087
|
+
git.flatMap(([k, v], i) => [
|
|
14088
|
+
[`GIT_CONFIG_KEY_${i}`, k],
|
|
14089
|
+
[`GIT_CONFIG_VALUE_${i}`, v]
|
|
14090
|
+
])
|
|
14091
|
+
)
|
|
14092
|
+
};
|
|
14093
|
+
};
|
|
14094
|
+
const heldFor = (handle) => {
|
|
14095
|
+
const h = held.get(handle.id);
|
|
14096
|
+
if (!h) throw new Error(`docker: ${handle.id} is not held`);
|
|
14097
|
+
return h;
|
|
14098
|
+
};
|
|
14099
|
+
const writeMarker = (name, attemptId) => execIn(name, [
|
|
14100
|
+
"sh",
|
|
14101
|
+
"-c",
|
|
14102
|
+
`mkdir -p ${CONTAINER_STATE} && printf %s "$1" > ${CONTAINER_STATE}/attempt`,
|
|
14103
|
+
"sh",
|
|
14104
|
+
attemptId
|
|
14105
|
+
]);
|
|
14106
|
+
const readDir = async (name, from) => {
|
|
14107
|
+
const r = await execIn(name, [
|
|
14108
|
+
"sh",
|
|
14109
|
+
"-c",
|
|
14110
|
+
'[ -e "$1" ] && chmod -R u+rwX "$1"; exec node -e "$2" "$1" "$3"',
|
|
14111
|
+
"sh",
|
|
14112
|
+
from,
|
|
14113
|
+
DUMP_SCRIPT,
|
|
14114
|
+
String(collectCap)
|
|
14115
|
+
]);
|
|
14116
|
+
if (r.code !== 0) {
|
|
14117
|
+
log.warn(
|
|
14118
|
+
{ container: name, dir: from, err: r.stderr.trim() },
|
|
14119
|
+
"runners: couldn't read what the runner left"
|
|
14120
|
+
);
|
|
14121
|
+
return null;
|
|
14122
|
+
}
|
|
12573
14123
|
try {
|
|
12574
|
-
|
|
12575
|
-
return
|
|
14124
|
+
const dump = JSON.parse(r.stdout);
|
|
14125
|
+
if (typeof dump.complete !== "boolean" || !Array.isArray(dump.files)) return null;
|
|
14126
|
+
return {
|
|
14127
|
+
complete: dump.complete,
|
|
14128
|
+
files: dump.files.filter(
|
|
14129
|
+
(f) => typeof f.name === "string" && typeof f.data === "string"
|
|
14130
|
+
).map((f) => ({ name: f.name, data: Buffer.from(f.data, "base64") }))
|
|
14131
|
+
};
|
|
12576
14132
|
} catch {
|
|
12577
|
-
return
|
|
14133
|
+
return null;
|
|
12578
14134
|
}
|
|
12579
14135
|
};
|
|
12580
|
-
const
|
|
14136
|
+
const removeIn = (name, from, names) => names.length ? execIn(name, ["sh", "-c", 'cd "$1" && shift && rm -f -- "$@"', "sh", from, ...names]) : Promise.resolve(null);
|
|
14137
|
+
const pendingPath = (name) => join17(opts.deviceDir, "runners-pending", `${name}.json`);
|
|
14138
|
+
const readPending = (name) => {
|
|
12581
14139
|
try {
|
|
12582
|
-
|
|
14140
|
+
const ids = JSON.parse(readFileSync9(pendingPath(name), "utf8"));
|
|
14141
|
+
return Array.isArray(ids) ? ids.filter((id) => typeof id === "string") : [];
|
|
12583
14142
|
} catch {
|
|
14143
|
+
return [];
|
|
12584
14144
|
}
|
|
12585
14145
|
};
|
|
12586
|
-
|
|
12587
|
-
|
|
14146
|
+
const writePending = (name, ids) => {
|
|
14147
|
+
if (!ids.length) {
|
|
14148
|
+
rmSync9(pendingPath(name), { force: true });
|
|
14149
|
+
return;
|
|
14150
|
+
}
|
|
14151
|
+
mkdirSync10(join17(opts.deviceDir, "runners-pending"), { recursive: true });
|
|
14152
|
+
writeFileSync6(pendingPath(name), JSON.stringify([...new Set(ids)]));
|
|
14153
|
+
};
|
|
14154
|
+
const keepSession = async (name, attempt, attemptId) => {
|
|
14155
|
+
const archive = sessionArchivePath(opts.deviceDir, attemptId);
|
|
14156
|
+
mkdirSync10(join17(archive, ".."), { recursive: true });
|
|
14157
|
+
const partial = `${archive}.${process.pid}.partial`;
|
|
14158
|
+
const file = new CappedFile(partial, sessionCap);
|
|
14159
|
+
const r = await execIn(
|
|
14160
|
+
name,
|
|
14161
|
+
[
|
|
14162
|
+
"sh",
|
|
14163
|
+
"-c",
|
|
14164
|
+
'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 "$@"'
|
|
14165
|
+
],
|
|
14166
|
+
{ stdout: file }
|
|
14167
|
+
);
|
|
14168
|
+
await new Promise(
|
|
14169
|
+
(resolve2) => file.writableFinished ? resolve2() : file.once("finish", resolve2)
|
|
14170
|
+
);
|
|
14171
|
+
if (r.code === 0 && !file.over) {
|
|
14172
|
+
renameSync5(partial, archive);
|
|
14173
|
+
writeFileSync6(join17(archive, "..", "pair"), `${attempt.conversationId}/${attempt.agentId}`);
|
|
14174
|
+
pruneOtherAttempts(opts.deviceDir, attempt, attemptId);
|
|
14175
|
+
await writeMarker(name, attemptId);
|
|
14176
|
+
log.debug({ container: name, attemptId, bytes: file.bytes }, "runners: session files kept");
|
|
14177
|
+
return;
|
|
14178
|
+
}
|
|
14179
|
+
rmSync9(partial, { force: true });
|
|
14180
|
+
if (r.code === 3 && !file.over) return;
|
|
14181
|
+
dropArchive(attemptId);
|
|
14182
|
+
log.warn(
|
|
14183
|
+
file.over ? { container: name, attemptId, capBytes: sessionCap } : { container: name, attemptId, err: r.stderr.trim() },
|
|
14184
|
+
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"
|
|
14185
|
+
);
|
|
14186
|
+
};
|
|
14187
|
+
const stagingDir = (name) => join17(opts.deviceDir, "runners-pending", `${name}.outbox`);
|
|
14188
|
+
const ensureRunning = async (name) => {
|
|
14189
|
+
const seen = await docker.run(["inspect", "-f", "{{.State.Running}}", name]);
|
|
14190
|
+
if (seen.code !== 0) return false;
|
|
14191
|
+
if (seen.stdout.trim() === "true") return true;
|
|
14192
|
+
return (await docker.run(["start", name])).code === 0;
|
|
14193
|
+
};
|
|
14194
|
+
const dropArchive = (attemptId) => {
|
|
14195
|
+
rmSync9(join17(sessionArchivePath(opts.deviceDir, attemptId), ".."), {
|
|
14196
|
+
recursive: true,
|
|
14197
|
+
force: true
|
|
14198
|
+
});
|
|
14199
|
+
};
|
|
14200
|
+
const RETIRE_SCRIPT = [
|
|
14201
|
+
`chmod -R u+rwX /work ${CONTAINER_HOME}`,
|
|
14202
|
+
"find /work -mindepth 1 -delete",
|
|
14203
|
+
`find ${CONTAINER_HOME} -mindepth 1 -delete`,
|
|
14204
|
+
`cp -a /etc/skel/. ${CONTAINER_HOME}/`,
|
|
14205
|
+
`mkdir -p ${CONTAINER_HOME}/.codex`,
|
|
14206
|
+
`cp /opt/cabane/codex-config.toml ${CONTAINER_HOME}/.codex/config.toml`,
|
|
14207
|
+
`rm -rf ${CONTAINER_STATE}/prepared`
|
|
14208
|
+
].join(" && ");
|
|
14209
|
+
const provider = {
|
|
14210
|
+
name: "docker",
|
|
14211
|
+
inheritsEnvironment: false,
|
|
12588
14212
|
async acquire(attempt) {
|
|
12589
|
-
|
|
12590
|
-
|
|
12591
|
-
|
|
12592
|
-
const
|
|
12593
|
-
|
|
12594
|
-
|
|
12595
|
-
|
|
12596
|
-
|
|
12597
|
-
detached: process.platform !== "win32"
|
|
14213
|
+
const name = containerName(attempt);
|
|
14214
|
+
const before = queues.get(name) ?? Promise.resolve();
|
|
14215
|
+
let release;
|
|
14216
|
+
const mine = new Promise((resolve2) => release = resolve2);
|
|
14217
|
+
const tail = before.then(() => mine);
|
|
14218
|
+
queues.set(name, tail);
|
|
14219
|
+
void tail.then(() => {
|
|
14220
|
+
if (queues.get(name) === tail) queues.delete(name);
|
|
12598
14221
|
});
|
|
14222
|
+
await before;
|
|
14223
|
+
try {
|
|
14224
|
+
await ensureNetwork();
|
|
14225
|
+
mkdirSync10(socketRoot, { recursive: true, mode: 448 });
|
|
14226
|
+
mkdirSync10(socketDir(name), { recursive: true, mode: 457 });
|
|
14227
|
+
let seen = await docker.run([
|
|
14228
|
+
"inspect",
|
|
14229
|
+
"-f",
|
|
14230
|
+
'{{.State.Running}} {{index .Config.Labels "cabane.companion"}}',
|
|
14231
|
+
name
|
|
14232
|
+
]);
|
|
14233
|
+
if (seen.code === 0 && seen.stdout.trim().split(" ")[1] !== companionLabel) {
|
|
14234
|
+
held.set(name, { attempt, release: () => {
|
|
14235
|
+
}, collected: false });
|
|
14236
|
+
let collected = false;
|
|
14237
|
+
try {
|
|
14238
|
+
await provider.collect({ provider: "docker", id: name }, { attemptId: null });
|
|
14239
|
+
collected = held.get(name)?.collected === true;
|
|
14240
|
+
} finally {
|
|
14241
|
+
held.delete(name);
|
|
14242
|
+
}
|
|
14243
|
+
if (!collected)
|
|
14244
|
+
throw new Error(
|
|
14245
|
+
"docker: this conversation's container runs another companion and what it holds couldn't be collected"
|
|
14246
|
+
);
|
|
14247
|
+
await destroy(name);
|
|
14248
|
+
log.info(
|
|
14249
|
+
{ container: name },
|
|
14250
|
+
"runners: replaced a container made with another companion"
|
|
14251
|
+
);
|
|
14252
|
+
seen = { code: 1, stdout: "", stderr: "" };
|
|
14253
|
+
}
|
|
14254
|
+
if (seen.code !== 0) {
|
|
14255
|
+
await create(name, attempt);
|
|
14256
|
+
log.debug({ container: name }, "runners: container created");
|
|
14257
|
+
}
|
|
14258
|
+
if (seen.stdout.trim().split(" ")[0] !== "true")
|
|
14259
|
+
await must(["start", name], "starting the container");
|
|
14260
|
+
const git = await serveGitCredentials({
|
|
14261
|
+
socketPath: join17(socketDir(name), "git.sock"),
|
|
14262
|
+
hosts: opts.gitHosts,
|
|
14263
|
+
fill: opts.gitFill,
|
|
14264
|
+
log
|
|
14265
|
+
});
|
|
14266
|
+
held.set(name, { attempt, release, git, collected: false });
|
|
14267
|
+
return { provider: "docker", id: name };
|
|
14268
|
+
} catch (err) {
|
|
14269
|
+
const keep = opts.keepContainers || readPending(name).length > 0;
|
|
14270
|
+
await (keep ? docker.run(["stop", "-t", "1", name]) : destroy(name)).catch(() => {
|
|
14271
|
+
});
|
|
14272
|
+
release();
|
|
14273
|
+
throw err;
|
|
14274
|
+
}
|
|
14275
|
+
},
|
|
14276
|
+
async bindAttempt(handle, attemptId) {
|
|
14277
|
+
const name = handle.id;
|
|
14278
|
+
const marker = await execIn(name, ["cat", `${CONTAINER_STATE}/attempt`]);
|
|
14279
|
+
const bound = marker.code === 0 ? marker.stdout.trim() : "";
|
|
14280
|
+
if (bound === (attemptId ?? "")) return;
|
|
14281
|
+
const { attempt } = heldFor(handle);
|
|
14282
|
+
pruneOtherAttempts(opts.deviceDir, attempt, attemptId ?? "");
|
|
14283
|
+
if (bound) {
|
|
14284
|
+
const wiped = await execIn(name, ["sh", "-c", RETIRE_SCRIPT]);
|
|
14285
|
+
if (wiped.code !== 0)
|
|
14286
|
+
throw new Error(`docker: couldn't clear the retired attempt: ${wiped.stderr.trim()}`);
|
|
14287
|
+
log.info(
|
|
14288
|
+
{ container: name, retired: bound, attemptId },
|
|
14289
|
+
"runners: retired attempt cleared"
|
|
14290
|
+
);
|
|
14291
|
+
}
|
|
14292
|
+
if (attemptId && UUID_RE.test(attemptId)) {
|
|
14293
|
+
const archive = sessionArchivePath(opts.deviceDir, attemptId);
|
|
14294
|
+
if (existsSync13(archive)) {
|
|
14295
|
+
const r = await execIn(name, ["tar", "-x", "-C", CONTAINER_HOME, "-f", "-"], {
|
|
14296
|
+
input: createReadStream(archive)
|
|
14297
|
+
});
|
|
14298
|
+
if (r.code !== 0) {
|
|
14299
|
+
log.warn(
|
|
14300
|
+
{ container: name, attemptId, err: r.stderr.trim() },
|
|
14301
|
+
"runners: couldn't restore the session files; the runtime will start its session over"
|
|
14302
|
+
);
|
|
14303
|
+
} else {
|
|
14304
|
+
log.info(
|
|
14305
|
+
{ container: name, attemptId, bytes: statSync3(archive).size },
|
|
14306
|
+
"runners: session files restored"
|
|
14307
|
+
);
|
|
14308
|
+
}
|
|
14309
|
+
} else {
|
|
14310
|
+
log.info(
|
|
14311
|
+
{ container: name, attemptId },
|
|
14312
|
+
"runners: no session files kept for this attempt on this device"
|
|
14313
|
+
);
|
|
14314
|
+
}
|
|
14315
|
+
}
|
|
14316
|
+
await writeMarker(name, attemptId ?? "");
|
|
14317
|
+
},
|
|
14318
|
+
exec(handle, argv, env, { signal, stdin }) {
|
|
14319
|
+
const name = handle.id;
|
|
14320
|
+
const pidFile = `/tmp/cabane-runner-${randomUUID5()}.pid`;
|
|
14321
|
+
const passed = { ...env, ...opts.runtimeEnv?.() ?? {} };
|
|
14322
|
+
const literal = containerEnv();
|
|
14323
|
+
const args = [
|
|
14324
|
+
"exec",
|
|
14325
|
+
"-i",
|
|
14326
|
+
"-u",
|
|
14327
|
+
"runner",
|
|
14328
|
+
"-w",
|
|
14329
|
+
CONTAINER_HOME,
|
|
14330
|
+
...Object.entries(literal).flatMap(([k, v]) => ["-e", `${k}=${v}`]),
|
|
14331
|
+
...Object.keys(passed).flatMap((k) => ["-e", k]),
|
|
14332
|
+
name,
|
|
14333
|
+
"sh",
|
|
14334
|
+
"-c",
|
|
14335
|
+
'echo $$ > "$0" && exec "$@"',
|
|
14336
|
+
pidFile,
|
|
14337
|
+
...argv
|
|
14338
|
+
];
|
|
14339
|
+
const child = docker.spawn(args, passed);
|
|
12599
14340
|
child.stdin?.on("error", () => {
|
|
12600
14341
|
});
|
|
12601
14342
|
if (stdin) child.stdin?.write(stdin);
|
|
14343
|
+
let forceTimer;
|
|
14344
|
+
const stop2 = () => {
|
|
14345
|
+
void execIn(name, ["sh", "-c", 'kill -TERM "$(cat "$1")"', "sh", pidFile]);
|
|
14346
|
+
forceTimer = setTimeout(() => void docker.run(["kill", name]), killGraceMs);
|
|
14347
|
+
forceTimer.unref?.();
|
|
14348
|
+
};
|
|
12602
14349
|
const exit = new Promise((resolve2) => {
|
|
12603
14350
|
let settled = false;
|
|
12604
14351
|
const done = (code) => {
|
|
@@ -12608,42 +14355,252 @@ function createLocalProcessProvider(opts = {}) {
|
|
|
12608
14355
|
if (forceTimer) clearTimeout(forceTimer);
|
|
12609
14356
|
resolve2(code);
|
|
12610
14357
|
};
|
|
12611
|
-
let forceTimer;
|
|
12612
|
-
const stop2 = () => {
|
|
12613
|
-
child.kill("SIGTERM");
|
|
12614
|
-
forceTimer = setTimeout(() => child.kill("SIGKILL"), killGraceMs);
|
|
12615
|
-
forceTimer.unref?.();
|
|
12616
|
-
};
|
|
12617
14358
|
child.once("error", () => done(1));
|
|
12618
|
-
child.once("close", (code, sig) =>
|
|
14359
|
+
child.once("close", (code, sig) => {
|
|
14360
|
+
const signum = sig ? constants2.signals[sig] : void 0;
|
|
14361
|
+
done(code ?? (signum !== void 0 ? 128 + signum : 1));
|
|
14362
|
+
});
|
|
12619
14363
|
if (signal.aborted) stop2();
|
|
12620
14364
|
else signal.addEventListener("abort", stop2, { once: true });
|
|
12621
14365
|
});
|
|
12622
14366
|
if (!child.stdout || !child.stderr || !child.stdin)
|
|
12623
|
-
throw new Error("
|
|
12624
|
-
const pid = child.pid;
|
|
14367
|
+
throw new Error("docker: runner stdio missing");
|
|
12625
14368
|
return {
|
|
12626
14369
|
exit,
|
|
12627
|
-
...pid !== void 0 ? { pid } : {},
|
|
14370
|
+
...child.pid !== void 0 ? { pid: child.pid } : {},
|
|
12628
14371
|
stdout: child.stdout,
|
|
12629
14372
|
stderr: child.stderr,
|
|
12630
14373
|
stdin: child.stdin,
|
|
12631
|
-
|
|
12632
|
-
|
|
12633
|
-
|
|
12634
|
-
|
|
12635
|
-
|
|
12636
|
-
|
|
12637
|
-
|
|
12638
|
-
|
|
12639
|
-
for (const pgid of targets.filter(groupAlive)) signalGroup(pgid, "SIGKILL");
|
|
14374
|
+
// The runner is the only thing this user runs in the container, so
|
|
14375
|
+
// everything it left is everything the user has left.
|
|
14376
|
+
async reap() {
|
|
14377
|
+
await execIn(name, [
|
|
14378
|
+
"sh",
|
|
14379
|
+
"-c",
|
|
14380
|
+
`kill -TERM -1 2>/dev/null; sleep ${REAP_GRACE_S}; kill -KILL -1 2>/dev/null; true`
|
|
14381
|
+
]);
|
|
12640
14382
|
}
|
|
12641
14383
|
};
|
|
12642
14384
|
},
|
|
12643
|
-
async
|
|
14385
|
+
async collect(handle, { attemptId }) {
|
|
14386
|
+
const name = handle.id;
|
|
14387
|
+
const held2 = heldFor(handle);
|
|
14388
|
+
const { attempt } = held2;
|
|
14389
|
+
const turnIds = [.../* @__PURE__ */ new Set([attempt.turnId, ...readPending(name)])];
|
|
14390
|
+
const running = await ensureRunning(name);
|
|
14391
|
+
if (attemptId && UUID_RE.test(attemptId)) {
|
|
14392
|
+
if (!running) {
|
|
14393
|
+
dropArchive(attemptId);
|
|
14394
|
+
log.warn(
|
|
14395
|
+
{ container: name, attemptId },
|
|
14396
|
+
"runners: couldn't reach the container to keep the session files; the next turn starts the session over"
|
|
14397
|
+
);
|
|
14398
|
+
} else {
|
|
14399
|
+
await keepSession(name, attempt, attemptId);
|
|
14400
|
+
}
|
|
14401
|
+
}
|
|
14402
|
+
if (!running) {
|
|
14403
|
+
writePending(name, turnIds);
|
|
14404
|
+
return;
|
|
14405
|
+
}
|
|
14406
|
+
const transcripts = await readDir(name, `${CONTAINER_STATE}/transcripts`);
|
|
14407
|
+
if (transcripts) {
|
|
14408
|
+
const kept = transcripts.files.filter((f) => SAFE_NAME_RE.test(f.name));
|
|
14409
|
+
if (kept.length) mkdirSync10(join17(opts.hostStateDir, "transcripts"), { recursive: true });
|
|
14410
|
+
for (const f of kept) writeFileSync6(join17(opts.hostStateDir, "transcripts", f.name), f.data);
|
|
14411
|
+
await removeIn(
|
|
14412
|
+
name,
|
|
14413
|
+
`${CONTAINER_STATE}/transcripts`,
|
|
14414
|
+
kept.map((f) => f.name)
|
|
14415
|
+
);
|
|
14416
|
+
}
|
|
14417
|
+
const outboxRel = join17("outbox", encodeURIComponent(attempt.agentId));
|
|
14418
|
+
const outbox = await readDir(name, `${CONTAINER_STATE}/${outboxRel}`);
|
|
14419
|
+
let unpublished = false;
|
|
14420
|
+
if (outbox) {
|
|
14421
|
+
const scope = {
|
|
14422
|
+
workspaceId: attempt.workspaceId,
|
|
14423
|
+
conversationId: attempt.conversationId,
|
|
14424
|
+
agentId: attempt.agentId,
|
|
14425
|
+
turnIds
|
|
14426
|
+
};
|
|
14427
|
+
let queued = 0;
|
|
14428
|
+
let refused = 0;
|
|
14429
|
+
for (const f of outbox.files) {
|
|
14430
|
+
let entry = null;
|
|
14431
|
+
try {
|
|
14432
|
+
entry = admitCollectedEntry(JSON.parse(f.data.toString("utf8")), scope);
|
|
14433
|
+
} catch {
|
|
14434
|
+
entry = null;
|
|
14435
|
+
}
|
|
14436
|
+
if (!entry) {
|
|
14437
|
+
refused++;
|
|
14438
|
+
continue;
|
|
14439
|
+
}
|
|
14440
|
+
const staged = stagingDir(name);
|
|
14441
|
+
mkdirSync10(staged, { recursive: true });
|
|
14442
|
+
writeFileSync6(
|
|
14443
|
+
join17(staged, `${encodeURIComponent(entry.turnId)}__${entry.seq}.json`),
|
|
14444
|
+
`${JSON.stringify(entry)}
|
|
14445
|
+
`
|
|
14446
|
+
);
|
|
14447
|
+
}
|
|
14448
|
+
await removeIn(
|
|
14449
|
+
name,
|
|
14450
|
+
`${CONTAINER_STATE}/${outboxRel}`,
|
|
14451
|
+
outbox.files.map((f) => f.name)
|
|
14452
|
+
);
|
|
14453
|
+
if (outbox.complete) {
|
|
14454
|
+
const published = publishStaged(
|
|
14455
|
+
stagingDir(name),
|
|
14456
|
+
join17(opts.hostStateDir, outboxRel),
|
|
14457
|
+
opts.moveFile
|
|
14458
|
+
);
|
|
14459
|
+
queued = published.published;
|
|
14460
|
+
if (!published.complete) {
|
|
14461
|
+
unpublished = true;
|
|
14462
|
+
log.warn(
|
|
14463
|
+
{ container: name },
|
|
14464
|
+
"runners: couldn't hand all of the runner's outbox to the companion; the rest waits for the next collection"
|
|
14465
|
+
);
|
|
14466
|
+
}
|
|
14467
|
+
}
|
|
14468
|
+
if (refused) {
|
|
14469
|
+
log.warn(
|
|
14470
|
+
{ container: name, refused },
|
|
14471
|
+
"runners: refused outbox entries a runner's own credential couldn't have sent"
|
|
14472
|
+
);
|
|
14473
|
+
}
|
|
14474
|
+
if (queued) {
|
|
14475
|
+
log.info(
|
|
14476
|
+
{ container: name, queued },
|
|
14477
|
+
"runners: delivering what the runner left in its outbox"
|
|
14478
|
+
);
|
|
14479
|
+
opts.onOutboxCollected?.(attempt.agentId);
|
|
14480
|
+
}
|
|
14481
|
+
}
|
|
14482
|
+
held2.collected = !!transcripts?.complete && !!outbox?.complete && !unpublished;
|
|
14483
|
+
writePending(name, held2.collected ? [] : turnIds);
|
|
14484
|
+
},
|
|
14485
|
+
async release(handle, { destroy: over }) {
|
|
14486
|
+
const h = held.get(handle.id);
|
|
14487
|
+
held.delete(handle.id);
|
|
14488
|
+
try {
|
|
14489
|
+
await h?.git?.close();
|
|
14490
|
+
if (h && !h.collected) {
|
|
14491
|
+
log.warn(
|
|
14492
|
+
{ container: handle.id },
|
|
14493
|
+
"runners: kept the container: what its runner left couldn't be collected yet"
|
|
14494
|
+
);
|
|
14495
|
+
await docker.run(["stop", "-t", "10", handle.id]);
|
|
14496
|
+
} else if (over || !opts.keepContainers) await destroy(handle.id);
|
|
14497
|
+
else await docker.run(["stop", "-t", "10", handle.id]);
|
|
14498
|
+
} finally {
|
|
14499
|
+
h?.release();
|
|
14500
|
+
}
|
|
14501
|
+
}
|
|
14502
|
+
};
|
|
14503
|
+
return provider;
|
|
14504
|
+
}
|
|
14505
|
+
|
|
14506
|
+
// src/providers/docker-setup.ts
|
|
14507
|
+
import { createHash as createHash5 } from "crypto";
|
|
14508
|
+
var MODEL_HOSTS = [
|
|
14509
|
+
"api.anthropic.com",
|
|
14510
|
+
"api.openai.com",
|
|
14511
|
+
"chatgpt.com",
|
|
14512
|
+
"auth.openai.com"
|
|
14513
|
+
];
|
|
14514
|
+
var GIT_HOSTS = ["github.com"];
|
|
14515
|
+
function dockerRunnerSettings(config) {
|
|
14516
|
+
const docker = config.runners?.docker ?? {};
|
|
14517
|
+
const apiUrl = docker.apiUrl ?? config.baseUrl;
|
|
14518
|
+
const apiHost = new URL(apiUrl).host;
|
|
14519
|
+
const egressAllow = [
|
|
14520
|
+
.../* @__PURE__ */ new Set([apiHost, ...MODEL_HOSTS, ...GIT_HOSTS, ...docker.egressAllow ?? []])
|
|
14521
|
+
];
|
|
14522
|
+
return {
|
|
14523
|
+
image: docker.image ?? baseImageTag(),
|
|
14524
|
+
keepContainers: docker.keepContainers === true,
|
|
14525
|
+
apiUrl,
|
|
14526
|
+
egressAllow,
|
|
14527
|
+
gitHosts: [...GIT_HOSTS]
|
|
14528
|
+
};
|
|
14529
|
+
}
|
|
14530
|
+
function loopbackApi(apiUrl) {
|
|
14531
|
+
const host = new URL(apiUrl).hostname;
|
|
14532
|
+
return host === "localhost" || host === "::1" || host === "[::1]" || host.startsWith("127.");
|
|
14533
|
+
}
|
|
14534
|
+
function deviceKey(config) {
|
|
14535
|
+
const seed = config.deviceId ?? config.baseUrl;
|
|
14536
|
+
return createHash5("sha256").update(seed).digest("hex").slice(0, 12);
|
|
14537
|
+
}
|
|
14538
|
+
function resolveRuntimeEnv(env, store, log) {
|
|
14539
|
+
const out = {};
|
|
14540
|
+
for (const [name, template] of Object.entries(env ?? {})) {
|
|
14541
|
+
const missing = [];
|
|
14542
|
+
const value = template.replace(/\$\{([A-Za-z_][A-Za-z0-9_]*)\}/g, (_m, key) => {
|
|
14543
|
+
const v = store.get(key);
|
|
14544
|
+
if (v === void 0) missing.push(key);
|
|
14545
|
+
return v ?? "";
|
|
14546
|
+
});
|
|
14547
|
+
if (missing.length) {
|
|
14548
|
+
log?.warn({ name, missing }, "runners: a runner env value names secrets this device lacks");
|
|
14549
|
+
continue;
|
|
12644
14550
|
}
|
|
14551
|
+
out[name] = value;
|
|
14552
|
+
}
|
|
14553
|
+
return out;
|
|
14554
|
+
}
|
|
14555
|
+
async function probeRunner(docker, image, source, log) {
|
|
14556
|
+
const r = await docker.run(
|
|
14557
|
+
[
|
|
14558
|
+
"run",
|
|
14559
|
+
"--rm",
|
|
14560
|
+
"--network",
|
|
14561
|
+
"none",
|
|
14562
|
+
...companionMountArgs(source),
|
|
14563
|
+
image,
|
|
14564
|
+
"cabane-companion",
|
|
14565
|
+
"run-turn",
|
|
14566
|
+
"--manifest"
|
|
14567
|
+
],
|
|
14568
|
+
{ timeoutMs: 6e4 }
|
|
14569
|
+
);
|
|
14570
|
+
try {
|
|
14571
|
+
const manifest = JSON.parse(r.stdout.trim());
|
|
14572
|
+
log.info(
|
|
14573
|
+
{ image, version: manifest.version, bundled: manifest.bundled },
|
|
14574
|
+
"runners: runner ready"
|
|
14575
|
+
);
|
|
14576
|
+
return manifest;
|
|
14577
|
+
} catch {
|
|
14578
|
+
log.error(
|
|
14579
|
+
{ image, err: r.stderr.trim().slice(-2e3) },
|
|
14580
|
+
"runners: the companion didn't start in a runner container"
|
|
14581
|
+
);
|
|
14582
|
+
return null;
|
|
14583
|
+
}
|
|
14584
|
+
}
|
|
14585
|
+
function imageResolution(manifest, image, runtime) {
|
|
14586
|
+
const status2 = manifest?.bundled[runtime] ?? "platform_package_missing";
|
|
14587
|
+
return {
|
|
14588
|
+
runtime,
|
|
14589
|
+
status: status2,
|
|
14590
|
+
path: status2 === "present" ? `${image} (bundled)` : null,
|
|
14591
|
+
packageName: null,
|
|
14592
|
+
platform: "linux",
|
|
14593
|
+
arch: "image"
|
|
12645
14594
|
};
|
|
12646
14595
|
}
|
|
14596
|
+
function containerHookPath(hostPath) {
|
|
14597
|
+
const name = hostPath.split("/").pop() || "hook";
|
|
14598
|
+
const tag = createHash5("sha256").update(hostPath).digest("hex").slice(0, 8);
|
|
14599
|
+
return `/opt/cabane/hooks/${tag}-${name}`;
|
|
14600
|
+
}
|
|
14601
|
+
|
|
14602
|
+
// src/supervisor.ts
|
|
14603
|
+
init_git_credential();
|
|
12647
14604
|
|
|
12648
14605
|
// src/runner/device-locks.ts
|
|
12649
14606
|
var DeviceLocks = class {
|
|
@@ -12702,6 +14659,7 @@ var DeviceLocks = class {
|
|
|
12702
14659
|
|
|
12703
14660
|
// src/supervisor.ts
|
|
12704
14661
|
init_harness_sandbox();
|
|
14662
|
+
init_sandbox_policy();
|
|
12705
14663
|
init_launch();
|
|
12706
14664
|
|
|
12707
14665
|
// src/opencode-models.ts
|
|
@@ -12896,13 +14854,7 @@ function sleep4(ms) {
|
|
|
12896
14854
|
|
|
12897
14855
|
// src/supervisor.ts
|
|
12898
14856
|
init_transcript_writer();
|
|
12899
|
-
|
|
12900
|
-
// src/version.ts
|
|
12901
|
-
import { createRequire as createRequire2 } from "module";
|
|
12902
|
-
var pkg = createRequire2(import.meta.url)("../package.json");
|
|
12903
|
-
var COMPANION_VERSION = pkg.version;
|
|
12904
|
-
|
|
12905
|
-
// src/supervisor.ts
|
|
14857
|
+
init_version2();
|
|
12906
14858
|
init_src();
|
|
12907
14859
|
var HEARTBEAT_INTERVAL_MS = 3e4;
|
|
12908
14860
|
var SHUTDOWN_HEARTBEAT_WAIT_MS = 1e3;
|
|
@@ -13003,8 +14955,27 @@ var CompanionSupervisor = class {
|
|
|
13003
14955
|
if (opts.runnerCommand) this.runnerCommand = opts.runnerCommand;
|
|
13004
14956
|
if (opts.runnerProvider) this.providerForRunners = opts.runnerProvider;
|
|
13005
14957
|
this.unrunnableRetryDelaysMs = opts.unrunnableRetryDelaysMs ?? DEFAULT_UNRUNNABLE_RETRY_DELAYS_MS;
|
|
14958
|
+
if (this.dockerRunners()) {
|
|
14959
|
+
const { image } = dockerRunnerSettings(this.config);
|
|
14960
|
+
this.resolveBundled = (runtime) => imageResolution(this.runnerImage, image, runtime);
|
|
14961
|
+
}
|
|
13006
14962
|
this.latchBootBundledState();
|
|
13007
14963
|
}
|
|
14964
|
+
// CT1681: what a runner container said it can run, probed once at start;
|
|
14965
|
+
// null until then, and when no container could be started.
|
|
14966
|
+
runnerImage = null;
|
|
14967
|
+
// CT1681: the companion every container mounts — this one's own install.
|
|
14968
|
+
runnerCompanion() {
|
|
14969
|
+
return companionSource({
|
|
14970
|
+
configured: this.config.runners?.docker?.companionDir,
|
|
14971
|
+
platform: process.platform,
|
|
14972
|
+
version: COMPANION_VERSION,
|
|
14973
|
+
own: ownInstall()
|
|
14974
|
+
});
|
|
14975
|
+
}
|
|
14976
|
+
dockerRunners() {
|
|
14977
|
+
return runnersEnabled(this.config) && this.config.runners?.provider === "docker";
|
|
14978
|
+
}
|
|
13008
14979
|
// Stand up the data plane: pair check, initial assignments pull, then the
|
|
13009
14980
|
// heartbeat + poll loops. A companion with no device token (logged out) does
|
|
13010
14981
|
// nothing but say so.
|
|
@@ -13013,6 +14984,31 @@ var CompanionSupervisor = class {
|
|
|
13013
14984
|
{ protocolVersion: TURN_PROTOCOL_VERSION, version: COMPANION_VERSION },
|
|
13014
14985
|
`Companion ${COMPANION_VERSION} starting on ${this.config.deviceLabel ?? hostname3()}`
|
|
13015
14986
|
);
|
|
14987
|
+
if (this.dockerRunners() && !this.providerForRunners) {
|
|
14988
|
+
const { image, apiUrl } = dockerRunnerSettings(this.config);
|
|
14989
|
+
if (loopbackApi(apiUrl)) {
|
|
14990
|
+
this.log.error(
|
|
14991
|
+
{ apiUrl },
|
|
14992
|
+
"runners: a container can't reach this machine's loopback; set runners.docker.apiUrl to an address it can"
|
|
14993
|
+
);
|
|
14994
|
+
}
|
|
14995
|
+
const source = this.runnerCompanion();
|
|
14996
|
+
const docker = dockerCli();
|
|
14997
|
+
if ("error" in source) {
|
|
14998
|
+
this.log.error({ reason: source.error }, "runners: no companion to put in a container");
|
|
14999
|
+
} else if (await ensureBaseImage(docker, image, this.log) && await ensureCompanionVolume(docker, image, source, this.log)) {
|
|
15000
|
+
this.runnerImage = await probeRunner(docker, image, source, this.log);
|
|
15001
|
+
if (!this.runnerImage) await invalidateCompanionVolume(docker, image, source);
|
|
15002
|
+
}
|
|
15003
|
+
this.latchBootBundledState();
|
|
15004
|
+
}
|
|
15005
|
+
if (this.runnerSandbox() === "os") {
|
|
15006
|
+
const policy = this.sandboxPolicy();
|
|
15007
|
+
const posture = postureLines(policy);
|
|
15008
|
+
const summary = posture.join("; ");
|
|
15009
|
+
if (policy.ok) this.log.info({ posture }, `runners: OS sandbox policy \u2014 ${summary}`);
|
|
15010
|
+
else this.log.error({ posture }, `runners: ${summary}`);
|
|
15011
|
+
}
|
|
13016
15012
|
const startupHarnessRefresh = this.refreshHarnessStatuses();
|
|
13017
15013
|
this.trackHeartbeat(startupHarnessRefresh);
|
|
13018
15014
|
if (!this.config.deviceToken) {
|
|
@@ -13120,7 +15116,9 @@ var CompanionSupervisor = class {
|
|
|
13120
15116
|
runtimeIssues: this.runtimeIssues(),
|
|
13121
15117
|
// CT571/CT586: each runtime's `version` from the latest harness probe
|
|
13122
15118
|
// (fail-soft to null). Informational only — the server matches on name.
|
|
13123
|
-
versions: this.harnessVersions
|
|
15119
|
+
versions: this.harnessVersions,
|
|
15120
|
+
// CT1681: shown on the device page.
|
|
15121
|
+
...runnersEnabled(this.config) ? { runnerProvider: this.config.runners?.provider ?? "local-process" } : {}
|
|
13124
15122
|
}),
|
|
13125
15123
|
// CT566: echo the last classified credential state per runtime, when the
|
|
13126
15124
|
// companion has seen any. Omitted (undefined) until the first observed
|
|
@@ -13452,22 +15450,79 @@ var CompanionSupervisor = class {
|
|
|
13452
15450
|
// CT1679: `runners.sandbox`, checked against this host once. A host that
|
|
13453
15451
|
// can't make the sandbox says so once and runs its turns unwrapped.
|
|
13454
15452
|
sandboxUsable;
|
|
15453
|
+
// CT1684: `runners.policy`, parsed once. An invalid one is kept as the
|
|
15454
|
+
// error, which every sandboxed turn is refused with.
|
|
15455
|
+
parsedSandboxPolicy;
|
|
15456
|
+
sandboxPolicy() {
|
|
15457
|
+
this.parsedSandboxPolicy ??= parseSandboxPolicy(this.config.runners?.policy);
|
|
15458
|
+
return this.parsedSandboxPolicy;
|
|
15459
|
+
}
|
|
13455
15460
|
runnerSandbox() {
|
|
13456
|
-
if (this.config.runners?.sandbox !== "os") return "none";
|
|
15461
|
+
if (this.config.runners?.sandbox !== "os" || this.dockerRunners()) return "none";
|
|
13457
15462
|
this.sandboxUsable ??= osSandboxUsable(this.log);
|
|
13458
15463
|
return this.sandboxUsable ? "os" : "none";
|
|
13459
15464
|
}
|
|
13460
15465
|
// CT1679: one provider for the device, built on first use.
|
|
13461
15466
|
runnerProvider() {
|
|
13462
|
-
this.providerForRunners ??= createLocalProcessProvider();
|
|
15467
|
+
this.providerForRunners ??= this.dockerRunners() ? this.createDockerRunners() : createLocalProcessProvider();
|
|
13463
15468
|
return this.providerForRunners;
|
|
13464
15469
|
}
|
|
15470
|
+
createDockerRunners() {
|
|
15471
|
+
const settings = dockerRunnerSettings(this.config);
|
|
15472
|
+
const docker = this.config.runners?.docker ?? {};
|
|
15473
|
+
const hooks = [
|
|
15474
|
+
this.config.prepareHook?.command,
|
|
15475
|
+
...Object.values(this.config.agents ?? {}).map((a) => a.prepareHook?.command)
|
|
15476
|
+
].filter((c) => typeof c === "string");
|
|
15477
|
+
const source = this.runnerCompanion();
|
|
15478
|
+
return createDockerProvider({
|
|
15479
|
+
image: settings.image,
|
|
15480
|
+
// Without a companion nothing is advertised, so nothing is sent; a turn
|
|
15481
|
+
// that arrives anyway fails at the mount and says so.
|
|
15482
|
+
companion: "error" in source ? { kind: "dir", path: "/nonexistent", key: "none" } : source,
|
|
15483
|
+
deviceKey: deviceKey(this.config),
|
|
15484
|
+
keepContainers: settings.keepContainers,
|
|
15485
|
+
egressAllow: settings.egressAllow,
|
|
15486
|
+
gitHosts: settings.gitHosts,
|
|
15487
|
+
gitFill: deviceGitFill(docker.gitCredentialHelper),
|
|
15488
|
+
runtimeEnv: () => resolveRuntimeEnv(
|
|
15489
|
+
docker.env,
|
|
15490
|
+
loadSecretStoreTolerant(
|
|
15491
|
+
(m) => this.log.warn(
|
|
15492
|
+
{ err: m },
|
|
15493
|
+
"Could not read the secrets file. Check ~/.cabane/secrets.json."
|
|
15494
|
+
)
|
|
15495
|
+
),
|
|
15496
|
+
this.log
|
|
15497
|
+
),
|
|
15498
|
+
mounts: [...new Set(hooks)].map((source2) => ({ source: source2, target: containerHookPath(source2) })),
|
|
15499
|
+
deviceDir: cabaneDir(),
|
|
15500
|
+
hostStateDir: runnerStateDir(),
|
|
15501
|
+
onOutboxCollected: (agentId) => {
|
|
15502
|
+
for (const wr of this.workspaces.values()) wr.agents.get(agentId)?.kickDrain();
|
|
15503
|
+
},
|
|
15504
|
+
log: this.log
|
|
15505
|
+
});
|
|
15506
|
+
}
|
|
15507
|
+
// CT1681: the runner command for this device's provider: the image's own
|
|
15508
|
+
// companion in a container, this process's otherwise.
|
|
15509
|
+
runnerCommandFor() {
|
|
15510
|
+
if (this.runnerCommand) return this.runnerCommand;
|
|
15511
|
+
return this.dockerRunners() ? ["cabane-companion", "run-turn"] : selfRunnerCommand();
|
|
15512
|
+
}
|
|
13465
15513
|
buildDispatcher(ctx) {
|
|
13466
|
-
const
|
|
15514
|
+
const hostLocal = localAgentConfig(this.config, {
|
|
13467
15515
|
agentId: ctx.agentId,
|
|
13468
15516
|
agentUsername: ctx.agentUsername,
|
|
13469
15517
|
workspaceSlug: ctx.workspaceSlug
|
|
13470
15518
|
});
|
|
15519
|
+
const local = this.dockerRunners() && hostLocal.prepareHook ? {
|
|
15520
|
+
...hostLocal,
|
|
15521
|
+
prepareHook: {
|
|
15522
|
+
...hostLocal.prepareHook,
|
|
15523
|
+
command: containerHookPath(hostLocal.prepareHook.command)
|
|
15524
|
+
}
|
|
15525
|
+
} : hostLocal;
|
|
13471
15526
|
if (this.dispatcherFactory) return this.dispatcherFactory({ ...ctx, local });
|
|
13472
15527
|
return new Dispatcher({
|
|
13473
15528
|
deviceId: this.deviceId ?? void 0,
|
|
@@ -13516,11 +15571,13 @@ var CompanionSupervisor = class {
|
|
|
13516
15571
|
...runnersEnabled(this.config) ? {
|
|
13517
15572
|
runner: {
|
|
13518
15573
|
provider: this.runnerProvider(),
|
|
13519
|
-
command: this.
|
|
15574
|
+
command: this.runnerCommandFor(),
|
|
15575
|
+
...this.dockerRunners() ? { runnerBaseUrl: dockerRunnerSettings(this.config).apiUrl } : {},
|
|
13520
15576
|
reportRunnerGone: (report) => this.reportRunnerGone(report),
|
|
13521
15577
|
connectorHealth: this.connectorHealth,
|
|
13522
15578
|
locks: this.runnerLocks,
|
|
13523
|
-
sandbox: this.runnerSandbox()
|
|
15579
|
+
sandbox: this.runnerSandbox(),
|
|
15580
|
+
policy: this.sandboxPolicy()
|
|
13524
15581
|
}
|
|
13525
15582
|
} : {}
|
|
13526
15583
|
});
|
|
@@ -13840,7 +15897,7 @@ var CompanionSupervisor = class {
|
|
|
13840
15897
|
);
|
|
13841
15898
|
}
|
|
13842
15899
|
const resumedTurnId = recoveredTurnId ?? (dispatchId ? turnIdForEvent(workspaceId, dispatchId) : null);
|
|
13843
|
-
const turnId = resumedTurnId ??
|
|
15900
|
+
const turnId = resumedTurnId ?? randomUUID6();
|
|
13844
15901
|
if (dispatchId) {
|
|
13845
15902
|
const liveTurnIds = rememberTurnId(workspaceId, dispatchId, turnId);
|
|
13846
15903
|
if (liveTurnIds > TURN_ID_OVERFLOW_WARN) {
|
|
@@ -14145,9 +16202,9 @@ async function waitForBoundedHeartbeat(heartbeat) {
|
|
|
14145
16202
|
}
|
|
14146
16203
|
function defaultReexec() {
|
|
14147
16204
|
clearRuntimeState();
|
|
14148
|
-
void import("child_process").then(({ spawn:
|
|
16205
|
+
void import("child_process").then(({ spawn: spawn10 }) => {
|
|
14149
16206
|
try {
|
|
14150
|
-
const child =
|
|
16207
|
+
const child = spawn10(process.execPath, process.argv.slice(1), {
|
|
14151
16208
|
stdio: "inherit",
|
|
14152
16209
|
detached: false
|
|
14153
16210
|
});
|
|
@@ -14159,6 +16216,9 @@ function defaultReexec() {
|
|
|
14159
16216
|
});
|
|
14160
16217
|
}
|
|
14161
16218
|
|
|
16219
|
+
// src/runtime.ts
|
|
16220
|
+
init_version2();
|
|
16221
|
+
|
|
14162
16222
|
// src/safety.ts
|
|
14163
16223
|
var RECOVERABLE_CODES = /* @__PURE__ */ new Set([
|
|
14164
16224
|
"EPIPE",
|
|
@@ -14218,22 +16278,22 @@ function handleUncaught(log, err, origin) {
|
|
|
14218
16278
|
|
|
14219
16279
|
// src/crash-marker.ts
|
|
14220
16280
|
init_config();
|
|
14221
|
-
import { existsSync as
|
|
14222
|
-
import { join as
|
|
16281
|
+
import { existsSync as existsSync15, mkdirSync as mkdirSync13, readFileSync as readFileSync12, rmSync as rmSync12, writeFileSync as writeFileSync9 } from "fs";
|
|
16282
|
+
import { join as join20 } from "path";
|
|
14223
16283
|
function crashMarkerPath() {
|
|
14224
|
-
return
|
|
16284
|
+
return join20(cabaneDir(), "last-error.json");
|
|
14225
16285
|
}
|
|
14226
16286
|
function recordCrash(rec2) {
|
|
14227
16287
|
try {
|
|
14228
|
-
|
|
14229
|
-
|
|
16288
|
+
mkdirSync13(cabaneDir(), { recursive: true });
|
|
16289
|
+
writeFileSync9(crashMarkerPath(), JSON.stringify(rec2, null, 2) + "\n");
|
|
14230
16290
|
} catch {
|
|
14231
16291
|
}
|
|
14232
16292
|
}
|
|
14233
16293
|
function clearCrash() {
|
|
14234
16294
|
try {
|
|
14235
16295
|
const path = crashMarkerPath();
|
|
14236
|
-
if (
|
|
16296
|
+
if (existsSync15(path)) rmSync12(path, { force: true });
|
|
14237
16297
|
} catch {
|
|
14238
16298
|
}
|
|
14239
16299
|
}
|
|
@@ -14276,7 +16336,7 @@ async function createCompanionRuntime(opts = {}) {
|
|
|
14276
16336
|
opencodeServerUrl: cfg.opencode?.serverUrl,
|
|
14277
16337
|
codex: isCodexEnabled(cfg)
|
|
14278
16338
|
});
|
|
14279
|
-
const instanceId =
|
|
16339
|
+
const instanceId = randomUUID7();
|
|
14280
16340
|
const startedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
14281
16341
|
const pre = readLiveRuntimeState();
|
|
14282
16342
|
if (pre && await verifyRuntime(pre) === "stale") clearRuntimeState();
|
|
@@ -14406,16 +16466,16 @@ async function closeSurfaces(control, dashboard) {
|
|
|
14406
16466
|
init_term();
|
|
14407
16467
|
|
|
14408
16468
|
// src/commands/daemon.ts
|
|
14409
|
-
import { spawn as
|
|
14410
|
-
import { closeSync as closeSync3, mkdirSync as
|
|
16469
|
+
import { spawn as spawn9 } from "child_process";
|
|
16470
|
+
import { closeSync as closeSync3, mkdirSync as mkdirSync14, openSync as openSync3 } from "fs";
|
|
14411
16471
|
|
|
14412
16472
|
// src/cli-entry.ts
|
|
14413
|
-
import { existsSync as
|
|
14414
|
-
import { fileURLToPath as
|
|
16473
|
+
import { existsSync as existsSync16 } from "fs";
|
|
16474
|
+
import { fileURLToPath as fileURLToPath5 } from "url";
|
|
14415
16475
|
var RELATIVE_CANDIDATES = ["./cli.js", "../dist/cli.js", "../cli.js"];
|
|
14416
16476
|
function companionCliEntry(deps = {}) {
|
|
14417
|
-
const exists = deps.exists ??
|
|
14418
|
-
const candidates = deps.candidates ?? RELATIVE_CANDIDATES.map((rel) =>
|
|
16477
|
+
const exists = deps.exists ?? existsSync16;
|
|
16478
|
+
const candidates = deps.candidates ?? RELATIVE_CANDIDATES.map((rel) => fileURLToPath5(new URL(rel, import.meta.url)));
|
|
14419
16479
|
for (const candidate of candidates) {
|
|
14420
16480
|
if (exists(candidate)) return candidate;
|
|
14421
16481
|
}
|
|
@@ -14516,10 +16576,10 @@ Stop: cabane-companion stop
|
|
|
14516
16576
|
}
|
|
14517
16577
|
function defaultSpawnDetached(args) {
|
|
14518
16578
|
const cliPath = companionCliEntry();
|
|
14519
|
-
|
|
16579
|
+
mkdirSync14(cabaneDir(), { recursive: true });
|
|
14520
16580
|
const logFd = openSync3(companionLogPath(), "a");
|
|
14521
16581
|
try {
|
|
14522
|
-
return
|
|
16582
|
+
return spawn9(process.execPath, [cliPath, ...args], {
|
|
14523
16583
|
detached: true,
|
|
14524
16584
|
stdio: ["ignore", logFd, logFd],
|
|
14525
16585
|
env: { ...process.env, CABANE_COMPANION_DAEMON: "1" }
|
|
@@ -14808,7 +16868,8 @@ async function runAttached(runtime) {
|
|
|
14808
16868
|
|
|
14809
16869
|
// src/commands/status.ts
|
|
14810
16870
|
init_config();
|
|
14811
|
-
import { readdirSync as
|
|
16871
|
+
import { readdirSync as readdirSync6 } from "fs";
|
|
16872
|
+
init_sandbox_policy();
|
|
14812
16873
|
init_secrets();
|
|
14813
16874
|
init_transcript_writer();
|
|
14814
16875
|
async function status() {
|
|
@@ -14859,6 +16920,13 @@ async function status() {
|
|
|
14859
16920
|
process.stdout.write(`prepare hook: ${cfg.prepareHook.command} (device-level, all agents)
|
|
14860
16921
|
`);
|
|
14861
16922
|
}
|
|
16923
|
+
if (runnersEnabled(cfg) && cfg.runners?.sandbox === "os") {
|
|
16924
|
+
process.stdout.write("sandbox: os\n");
|
|
16925
|
+
for (const line of postureLines(parseSandboxPolicy(cfg.runners.policy))) {
|
|
16926
|
+
process.stdout.write(` ${line}
|
|
16927
|
+
`);
|
|
16928
|
+
}
|
|
16929
|
+
}
|
|
14862
16930
|
const overrides2 = Object.keys(cfg.agents ?? {});
|
|
14863
16931
|
if (overrides2.length > 0) {
|
|
14864
16932
|
process.stdout.write(`local overrides for: ${overrides2.join(", ")}
|
|
@@ -14870,7 +16938,7 @@ function transcriptsLine() {
|
|
|
14870
16938
|
let count = 0;
|
|
14871
16939
|
let newest;
|
|
14872
16940
|
try {
|
|
14873
|
-
const files =
|
|
16941
|
+
const files = readdirSync6(dir2).filter((f) => f.endsWith(".jsonl")).sort();
|
|
14874
16942
|
count = files.length;
|
|
14875
16943
|
newest = files[files.length - 1];
|
|
14876
16944
|
} catch {
|
|
@@ -14951,8 +17019,8 @@ function isAlive(kill, pid) {
|
|
|
14951
17019
|
init_errors();
|
|
14952
17020
|
init_src();
|
|
14953
17021
|
init_transcript_writer();
|
|
14954
|
-
import { existsSync as
|
|
14955
|
-
import { isAbsolute, join as
|
|
17022
|
+
import { existsSync as existsSync17, readFileSync as readFileSync13, readdirSync as readdirSync7 } from "fs";
|
|
17023
|
+
import { isAbsolute, join as join21 } from "path";
|
|
14956
17024
|
async function transcript(opts = {}) {
|
|
14957
17025
|
const dir2 = transcriptsDir();
|
|
14958
17026
|
if (opts.follow) {
|
|
@@ -14969,7 +17037,7 @@ async function transcript(opts = {}) {
|
|
|
14969
17037
|
process.stdout.write(emptyMessage(dir2));
|
|
14970
17038
|
return;
|
|
14971
17039
|
}
|
|
14972
|
-
process.stdout.write(renderFile(
|
|
17040
|
+
process.stdout.write(renderFile(join21(dir2, newest)) + "\n");
|
|
14973
17041
|
return;
|
|
14974
17042
|
}
|
|
14975
17043
|
printList(dir2);
|
|
@@ -15052,7 +17120,7 @@ function isComplete(content) {
|
|
|
15052
17120
|
async function followTranscripts(dir2) {
|
|
15053
17121
|
const follower = new TranscriptFollower({
|
|
15054
17122
|
listFiles: () => listFiles(dir2),
|
|
15055
|
-
read: (f) =>
|
|
17123
|
+
read: (f) => readFileSync13(join21(dir2, f), "utf8"),
|
|
15056
17124
|
write: (s) => process.stdout.write(s),
|
|
15057
17125
|
// CSI: cursor up `n` lines, then erase from cursor to end of screen.
|
|
15058
17126
|
clearLines: (n) => process.stdout.write(`\x1B[${n}A\x1B[0J`),
|
|
@@ -15077,7 +17145,7 @@ async function followTranscripts(dir2) {
|
|
|
15077
17145
|
}
|
|
15078
17146
|
function listFiles(dir2) {
|
|
15079
17147
|
try {
|
|
15080
|
-
return
|
|
17148
|
+
return readdirSync7(dir2).filter((f) => f.endsWith(".jsonl")).sort().reverse();
|
|
15081
17149
|
} catch {
|
|
15082
17150
|
return [];
|
|
15083
17151
|
}
|
|
@@ -15092,7 +17160,7 @@ function printList(dir2) {
|
|
|
15092
17160
|
|
|
15093
17161
|
`);
|
|
15094
17162
|
for (const f of files.slice(0, 20)) {
|
|
15095
|
-
const { meta, outcome } = peek(
|
|
17163
|
+
const { meta, outcome } = peek(join21(dir2, f));
|
|
15096
17164
|
const when = fmtTime(rec(meta)?.ts);
|
|
15097
17165
|
const ws = str2(rec(meta)?.workspaceSlug);
|
|
15098
17166
|
const o = rec(outcome);
|
|
@@ -15113,7 +17181,7 @@ function peek(path) {
|
|
|
15113
17181
|
let meta;
|
|
15114
17182
|
let outcome;
|
|
15115
17183
|
try {
|
|
15116
|
-
for (const line of
|
|
17184
|
+
for (const line of readFileSync13(path, "utf8").split("\n")) {
|
|
15117
17185
|
if (!line.trim()) continue;
|
|
15118
17186
|
const o = safeParse(line);
|
|
15119
17187
|
const t = str2(rec(o)?.type);
|
|
@@ -15126,13 +17194,13 @@ function peek(path) {
|
|
|
15126
17194
|
}
|
|
15127
17195
|
function resolveTarget(dir2, target) {
|
|
15128
17196
|
if (isAbsolute(target) || target.includes("/")) {
|
|
15129
|
-
if (
|
|
17197
|
+
if (existsSync17(target)) return target;
|
|
15130
17198
|
throw new CompanionError(`no transcript at ${target}.`);
|
|
15131
17199
|
}
|
|
15132
|
-
const exact =
|
|
15133
|
-
if (
|
|
17200
|
+
const exact = join21(dir2, target);
|
|
17201
|
+
if (existsSync17(exact)) return exact;
|
|
15134
17202
|
const matches = listFiles(dir2).filter((f) => f.includes(target));
|
|
15135
|
-
if (matches.length === 1) return
|
|
17203
|
+
if (matches.length === 1) return join21(dir2, matches[0]);
|
|
15136
17204
|
if (matches.length === 0) {
|
|
15137
17205
|
throw new CompanionError(
|
|
15138
17206
|
`no transcript matching "${target}" in ${dir2}. Run \`cabane-companion transcript\` to list them.`
|
|
@@ -15146,7 +17214,7 @@ function resolveTarget(dir2, target) {
|
|
|
15146
17214
|
function renderFile(path) {
|
|
15147
17215
|
let content;
|
|
15148
17216
|
try {
|
|
15149
|
-
content =
|
|
17217
|
+
content = readFileSync13(path, "utf8");
|
|
15150
17218
|
} catch (err) {
|
|
15151
17219
|
throw new CompanionError(
|
|
15152
17220
|
`couldn't read ${path}: ${err instanceof Error ? err.message : String(err)}`
|
|
@@ -15295,6 +17363,7 @@ They are written per dispatch while \`cabane-companion start\` is running.
|
|
|
15295
17363
|
|
|
15296
17364
|
// src/cli.ts
|
|
15297
17365
|
init_errors();
|
|
17366
|
+
init_version2();
|
|
15298
17367
|
var program = new Command();
|
|
15299
17368
|
program.name("cabane-companion").description(
|
|
15300
17369
|
"Connect a coding agent on your machine to your Cabane workspace as a responder \u2014 reply to Cabane messages while staying a full local AI client (Claude Code or OpenCode)."
|
|
@@ -15307,7 +17376,7 @@ program.command("pair").description(
|
|
|
15307
17376
|
});
|
|
15308
17377
|
});
|
|
15309
17378
|
program.command("write-paired-config", { hidden: true }).description("persist an already-completed device enrollment payload from stdin.").action(() => {
|
|
15310
|
-
writeCompletedPairing(
|
|
17379
|
+
writeCompletedPairing(readFileSync14(0, "utf8"));
|
|
15311
17380
|
});
|
|
15312
17381
|
program.command("start").description(
|
|
15313
17382
|
"pair this device if needed, connect a coding agent on it, and run in the background."
|
|
@@ -15364,7 +17433,13 @@ program.command("logout").description(
|
|
|
15364
17433
|
...opts.purge ? { purge: true } : {}
|
|
15365
17434
|
});
|
|
15366
17435
|
});
|
|
15367
|
-
program.command("run-turn", { hidden: true }).action(async () => {
|
|
17436
|
+
program.command("run-turn", { hidden: true }).option("--manifest", "print what this install can run, as JSON, and exit").action(async (opts) => {
|
|
17437
|
+
if (opts.manifest) {
|
|
17438
|
+
const { runnerManifest: runnerManifest2 } = await Promise.resolve().then(() => (init_image_manifest(), image_manifest_exports));
|
|
17439
|
+
process.stdout.write(`${JSON.stringify(runnerManifest2())}
|
|
17440
|
+
`);
|
|
17441
|
+
return;
|
|
17442
|
+
}
|
|
15368
17443
|
const { runTurn: runTurn2 } = await Promise.resolve().then(() => (init_run_turn(), run_turn_exports));
|
|
15369
17444
|
const { code } = await runTurn2(process.env, {
|
|
15370
17445
|
stdin: process.stdin,
|
|
@@ -15376,6 +17451,28 @@ program.command("run-turn", { hidden: true }).action(async () => {
|
|
|
15376
17451
|
await new Promise((resolve2) => process.stderr.write("", () => resolve2()));
|
|
15377
17452
|
process.exit(code);
|
|
15378
17453
|
});
|
|
17454
|
+
program.command("egress-proxy", { hidden: true }).requiredOption("--port <port>", "port to listen on").action(async (opts) => {
|
|
17455
|
+
const { createEgressProxy: createEgressProxy2 } = await Promise.resolve().then(() => (init_egress_proxy(), egress_proxy_exports));
|
|
17456
|
+
const allow = JSON.parse(process.env.CABANE_EGRESS_ALLOW ?? "[]");
|
|
17457
|
+
const log = (await import("pino")).default({ level: "info" }, process.stderr);
|
|
17458
|
+
createEgressProxy2({ allow, log }).listen(
|
|
17459
|
+
Number(opts.port),
|
|
17460
|
+
"0.0.0.0",
|
|
17461
|
+
() => log.info({ allow }, "egress proxy listening")
|
|
17462
|
+
);
|
|
17463
|
+
});
|
|
17464
|
+
program.command("git-credential <operation>", { hidden: true }).action(async (operation) => {
|
|
17465
|
+
const socket = process.env.CABANE_GIT_CREDENTIAL_SOCKET;
|
|
17466
|
+
if (!socket) return;
|
|
17467
|
+
const { askGitCredential: askGitCredential2 } = await Promise.resolve().then(() => (init_git_credential(), git_credential_exports));
|
|
17468
|
+
const request = await new Promise((resolve2) => {
|
|
17469
|
+
let input = "";
|
|
17470
|
+
process.stdin.setEncoding("utf8");
|
|
17471
|
+
process.stdin.on("data", (chunk) => input += chunk);
|
|
17472
|
+
process.stdin.on("end", () => resolve2(input));
|
|
17473
|
+
});
|
|
17474
|
+
process.stdout.write(await askGitCredential2(socket, operation, request));
|
|
17475
|
+
});
|
|
15379
17476
|
program.parseAsync(process.argv).catch((err) => {
|
|
15380
17477
|
if (err instanceof CompanionError) {
|
|
15381
17478
|
process.stderr.write(`error: ${err.message}
|