@cabane/companion 0.6.125 → 0.6.127
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +49 -0
- package/dist/cli.js +646 -199
- package/dist/pairing-config.js +6 -1
- package/dist/runtime.js +429 -151
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -541,6 +541,10 @@ 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
550
|
provider: z2.enum(["local-process", "docker"]).optional(),
|
|
@@ -574,7 +578,8 @@ var init_config = __esm({
|
|
|
574
578
|
// container's git for an allowed host, e.g. `!gh auth git-credential`.
|
|
575
579
|
// Absent: git's own configured helpers on this device.
|
|
576
580
|
gitCredentialHelper: z2.string().min(1).optional()
|
|
577
|
-
}).strict().optional()
|
|
581
|
+
}).strict().optional(),
|
|
582
|
+
policy: z2.unknown().optional()
|
|
578
583
|
}).strict().optional()
|
|
579
584
|
});
|
|
580
585
|
}
|
|
@@ -6662,6 +6667,173 @@ var init_src = __esm({
|
|
|
6662
6667
|
}
|
|
6663
6668
|
});
|
|
6664
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
|
+
|
|
6665
6837
|
// src/runner/opencode-lock.ts
|
|
6666
6838
|
function opencodeLockKey(serverUrl) {
|
|
6667
6839
|
return `opencode:${serverUrl}`;
|
|
@@ -7023,7 +7195,7 @@ var init_turn_containment = __esm({
|
|
|
7023
7195
|
// src/secrets.ts
|
|
7024
7196
|
import { existsSync as existsSync7, readFileSync as readFileSync6 } from "fs";
|
|
7025
7197
|
import { join as join10 } from "path";
|
|
7026
|
-
import { z as
|
|
7198
|
+
import { z as z14 } from "zod";
|
|
7027
7199
|
function secretsPath() {
|
|
7028
7200
|
return join10(cabaneDir(), "secrets.json");
|
|
7029
7201
|
}
|
|
@@ -7108,19 +7280,30 @@ function resolveMcpSecrets(mcpServers, store) {
|
|
|
7108
7280
|
}
|
|
7109
7281
|
return { servers, missing: [...missing].sort() };
|
|
7110
7282
|
}
|
|
7111
|
-
|
|
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;
|
|
7112
7294
|
var init_secrets = __esm({
|
|
7113
7295
|
"src/secrets.ts"() {
|
|
7114
7296
|
"use strict";
|
|
7115
7297
|
init_config();
|
|
7116
7298
|
init_errors();
|
|
7117
7299
|
PLACEHOLDER_RE = /\$\{([A-Za-z_][A-Za-z0-9_]*)\}/g;
|
|
7118
|
-
secretStoreSchema =
|
|
7300
|
+
secretStoreSchema = z14.record(z14.string(), z14.string());
|
|
7301
|
+
ERROR_REASON_MAX = 200;
|
|
7119
7302
|
}
|
|
7120
7303
|
});
|
|
7121
7304
|
|
|
7122
7305
|
// src/run-config.ts
|
|
7123
|
-
import { z as
|
|
7306
|
+
import { z as z15 } from "zod";
|
|
7124
7307
|
function parseRunConfig(raw, onWarn) {
|
|
7125
7308
|
const result = runConfigSchema.safeParse(raw ?? {});
|
|
7126
7309
|
if (result.success) return result.data;
|
|
@@ -7133,38 +7316,38 @@ var mcpStdioServerSchema, mcpHttpServerSchema, mcpSseServerSchema, mcpServerDefS
|
|
|
7133
7316
|
var init_run_config = __esm({
|
|
7134
7317
|
"src/run-config.ts"() {
|
|
7135
7318
|
"use strict";
|
|
7136
|
-
mcpStdioServerSchema =
|
|
7137
|
-
type:
|
|
7138
|
-
command:
|
|
7139
|
-
args:
|
|
7140
|
-
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()
|
|
7141
7324
|
});
|
|
7142
|
-
mcpHttpServerSchema =
|
|
7143
|
-
type:
|
|
7144
|
-
url:
|
|
7145
|
-
headers:
|
|
7325
|
+
mcpHttpServerSchema = z15.object({
|
|
7326
|
+
type: z15.literal("http"),
|
|
7327
|
+
url: z15.string().url(),
|
|
7328
|
+
headers: z15.record(z15.string(), z15.string()).optional()
|
|
7146
7329
|
});
|
|
7147
|
-
mcpSseServerSchema =
|
|
7148
|
-
type:
|
|
7149
|
-
url:
|
|
7150
|
-
headers:
|
|
7330
|
+
mcpSseServerSchema = z15.object({
|
|
7331
|
+
type: z15.literal("sse"),
|
|
7332
|
+
url: z15.string().url(),
|
|
7333
|
+
headers: z15.record(z15.string(), z15.string()).optional()
|
|
7151
7334
|
});
|
|
7152
|
-
mcpServerDefSchema =
|
|
7153
|
-
thinkingConfigSchema =
|
|
7154
|
-
|
|
7155
|
-
|
|
7156
|
-
|
|
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") })
|
|
7157
7340
|
]);
|
|
7158
|
-
effortSchema =
|
|
7159
|
-
runConfigSchema =
|
|
7341
|
+
effortSchema = z15.enum(["low", "medium", "high", "xhigh", "max"]);
|
|
7342
|
+
runConfigSchema = z15.object({
|
|
7160
7343
|
// CT788: the host-access binary replaced the `assistant`/`coding`/`custom` mode
|
|
7161
7344
|
// trio + its custom tool lists — `true` grants the host filesystem/shell, absent
|
|
7162
7345
|
// is the locked surface. Kept in lockstep with `@cabane/shared`'s
|
|
7163
7346
|
// `agentRunConfigSchema` (independent zod, forward-compatible: unknown keys are
|
|
7164
7347
|
// stripped, so an older companion riding a newer server never rejects the config).
|
|
7165
|
-
hostAccess:
|
|
7166
|
-
mcpServers:
|
|
7167
|
-
model:
|
|
7348
|
+
hostAccess: z15.boolean().optional(),
|
|
7349
|
+
mcpServers: z15.record(z15.string(), mcpServerDefSchema).optional(),
|
|
7350
|
+
model: z15.string().min(1).optional(),
|
|
7168
7351
|
thinking: thinkingConfigSchema.optional(),
|
|
7169
7352
|
effort: effortSchema.optional()
|
|
7170
7353
|
});
|
|
@@ -7173,7 +7356,7 @@ var init_run_config = __esm({
|
|
|
7173
7356
|
});
|
|
7174
7357
|
|
|
7175
7358
|
// src/runner/protocol.ts
|
|
7176
|
-
import { z as
|
|
7359
|
+
import { z as z16 } from "zod";
|
|
7177
7360
|
function required(env, name) {
|
|
7178
7361
|
const value = env[name];
|
|
7179
7362
|
if (!value) throw new Error(`runner: ${name} is not set`);
|
|
@@ -7186,6 +7369,7 @@ function parseRunnerInput(env, stdin) {
|
|
|
7186
7369
|
return {
|
|
7187
7370
|
baseUrl: required(env, RUNNER_ENV.url),
|
|
7188
7371
|
credential: credentials.credential,
|
|
7372
|
+
sandboxCredentials: credentials.sandboxCredentials ?? [],
|
|
7189
7373
|
dispatch,
|
|
7190
7374
|
turnId: required(env, RUNNER_ENV.turnId),
|
|
7191
7375
|
resumed: env[RUNNER_ENV.resumed] === "1",
|
|
@@ -7202,7 +7386,10 @@ function runnerEnv(input) {
|
|
|
7202
7386
|
};
|
|
7203
7387
|
}
|
|
7204
7388
|
function runnerStdin(input) {
|
|
7205
|
-
return `${JSON.stringify({
|
|
7389
|
+
return `${JSON.stringify({
|
|
7390
|
+
credential: input.credential,
|
|
7391
|
+
...input.sandboxCredentials?.length ? { sandboxCredentials: input.sandboxCredentials } : {}
|
|
7392
|
+
})}
|
|
7206
7393
|
`;
|
|
7207
7394
|
}
|
|
7208
7395
|
function encodeLauncherMessage(message) {
|
|
@@ -7238,6 +7425,7 @@ var init_protocol = __esm({
|
|
|
7238
7425
|
"use strict";
|
|
7239
7426
|
init_config();
|
|
7240
7427
|
init_run_config();
|
|
7428
|
+
init_sandbox_policy();
|
|
7241
7429
|
RUNNER_ENV = {
|
|
7242
7430
|
// The wire the runner speaks.
|
|
7243
7431
|
url: "CABANE_URL",
|
|
@@ -7251,64 +7439,75 @@ var init_protocol = __esm({
|
|
|
7251
7439
|
// Everything else the turn is constructed from, as JSON: see `RunnerSpec`.
|
|
7252
7440
|
spec: "CABANE_RUNNER_SPEC"
|
|
7253
7441
|
};
|
|
7254
|
-
runtimeHealthSchema =
|
|
7255
|
-
quotaState:
|
|
7256
|
-
limitedUntil:
|
|
7257
|
-
lastError:
|
|
7442
|
+
runtimeHealthSchema = z16.object({
|
|
7443
|
+
quotaState: z16.enum(["ok", "limited", "auth_expired", "unknown"]),
|
|
7444
|
+
limitedUntil: z16.string().nullable(),
|
|
7445
|
+
lastError: z16.string().nullable()
|
|
7258
7446
|
});
|
|
7259
|
-
runnerSpecSchema =
|
|
7260
|
-
deviceId:
|
|
7447
|
+
runnerSpecSchema = z16.object({
|
|
7448
|
+
deviceId: z16.string().optional(),
|
|
7261
7449
|
// The launcher's boot id: the one its heartbeat carries, so a runner's lease
|
|
7262
7450
|
// matches the device that holds it.
|
|
7263
|
-
bootId:
|
|
7264
|
-
workspaceId:
|
|
7265
|
-
workspaceSlug:
|
|
7266
|
-
agentId:
|
|
7267
|
-
agentUsername:
|
|
7268
|
-
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(),
|
|
7269
7457
|
local: localAgentConfigSchema,
|
|
7270
7458
|
runConfig: runConfigSchema,
|
|
7271
|
-
opencodeServerUrl:
|
|
7272
|
-
claudeCodeAvailable:
|
|
7273
|
-
codexAvailable:
|
|
7274
|
-
idleTimeoutMs:
|
|
7275
|
-
totalTimeoutMs:
|
|
7276
|
-
leaseRenewalMs:
|
|
7277
|
-
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(),
|
|
7278
7466
|
// Whether the turn writes a transcript. The directory is the runner's own
|
|
7279
7467
|
// (`transcriptsDir()`), so `CABANE_RUNNER_STATE_DIR` moves it with the outbox.
|
|
7280
|
-
transcripts:
|
|
7468
|
+
transcripts: z16.boolean(),
|
|
7281
7469
|
// Whether the harness starts under the OS sandbox. The launcher sends 'os'
|
|
7282
7470
|
// only when this host can make one.
|
|
7283
|
-
sandbox:
|
|
7284
|
-
|
|
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(),
|
|
7285
7476
|
// The launcher's connector-health observations, by runtime, as of the start.
|
|
7286
|
-
connectorHealth:
|
|
7477
|
+
connectorHealth: z16.record(z16.string(), runtimeHealthSchema),
|
|
7287
7478
|
// True when this (conversation, agent) pair was already told its session
|
|
7288
7479
|
// could not be saved: the notice is once per pair, not once per process.
|
|
7289
|
-
sessionWriteNoticeClaimed:
|
|
7480
|
+
sessionWriteNoticeClaimed: z16.boolean(),
|
|
7290
7481
|
// CT1680: the runner's identity, minted with its credential. The start
|
|
7291
7482
|
// PATCH carries it so the lease knows which runner holds the span.
|
|
7292
|
-
runnerId:
|
|
7483
|
+
runnerId: z16.string(),
|
|
7293
7484
|
// CT1681: the launcher keeps the runtime's session files beside the attempt
|
|
7294
7485
|
// and restores them into this runner's home, so the runner names the attempt
|
|
7295
7486
|
// each context was composed for and waits for the launcher to say it's in
|
|
7296
7487
|
// place. Absent: nothing to wait for (a runner on the launcher's own disk).
|
|
7297
|
-
attemptState:
|
|
7488
|
+
attemptState: z16.boolean().optional(),
|
|
7298
7489
|
// CT1681: MCP `${NAME}` secrets come from the launcher, by name, instead of
|
|
7299
7490
|
// this machine's secrets.json. Absent: the runner reads the device's file.
|
|
7300
|
-
secretsFromLauncher:
|
|
7491
|
+
secretsFromLauncher: z16.boolean().optional()
|
|
7301
7492
|
});
|
|
7302
|
-
dispatchSchema =
|
|
7303
|
-
type:
|
|
7304
|
-
agentId:
|
|
7305
|
-
deviceId:
|
|
7306
|
-
conversationId:
|
|
7307
|
-
messageId:
|
|
7308
|
-
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()
|
|
7309
7500
|
});
|
|
7310
|
-
credentialsSchema =
|
|
7311
|
-
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()
|
|
7312
7511
|
});
|
|
7313
7512
|
}
|
|
7314
7513
|
});
|
|
@@ -7360,6 +7559,7 @@ function runnerSpec(opts, launch, duties, runnerId) {
|
|
|
7360
7559
|
sandbox: launch.sandbox ?? "none",
|
|
7361
7560
|
...launch.provider.bindAttempt ? { attemptState: true } : {},
|
|
7362
7561
|
...launch.provider.inheritsEnvironment === false ? { secretsFromLauncher: true } : {},
|
|
7562
|
+
...launch.sandbox === "os" && launch.policy?.ok ? { sandboxPolicy: launch.policy.policy } : {},
|
|
7363
7563
|
logLevel: opts.log.level,
|
|
7364
7564
|
connectorHealth: healthSnapshot(launch.connectorHealth),
|
|
7365
7565
|
sessionWriteNoticeClaimed: duties.sessionWriteNoticeClaimed()
|
|
@@ -7458,6 +7658,32 @@ async function settleUnstarted(opts, payload, turnId, errorReason, log) {
|
|
|
7458
7658
|
);
|
|
7459
7659
|
}
|
|
7460
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
|
+
}
|
|
7461
7687
|
async function runTurnInRunner(opts, launch, duties, payload, handleOpts) {
|
|
7462
7688
|
const startedAt = Date.now();
|
|
7463
7689
|
const { turnId } = handleOpts;
|
|
@@ -7481,7 +7707,8 @@ async function runTurnInRunner(opts, launch, duties, payload, handleOpts) {
|
|
|
7481
7707
|
agentId: payload.agentId,
|
|
7482
7708
|
turnId
|
|
7483
7709
|
};
|
|
7484
|
-
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 };
|
|
7485
7712
|
if (!minted.ok) {
|
|
7486
7713
|
controller.signal.removeEventListener("abort", forwardStop);
|
|
7487
7714
|
duties.releaseAbort(turnId, controller);
|
|
@@ -7534,6 +7761,7 @@ async function runTurnInRunner(opts, launch, duties, payload, handleOpts) {
|
|
|
7534
7761
|
const input = {
|
|
7535
7762
|
baseUrl: launch.runnerBaseUrl ?? opts.baseUrl,
|
|
7536
7763
|
credential: minted.credential,
|
|
7764
|
+
sandboxCredentials: sandboxed.ok ? sandboxed.credentials : [],
|
|
7537
7765
|
dispatch: payload,
|
|
7538
7766
|
turnId,
|
|
7539
7767
|
resumed: handleOpts.resumed,
|
|
@@ -7705,6 +7933,7 @@ var PINO_LEVELS;
|
|
|
7705
7933
|
var init_launch = __esm({
|
|
7706
7934
|
"src/runner/launch.ts"() {
|
|
7707
7935
|
"use strict";
|
|
7936
|
+
init_sandbox_policy();
|
|
7708
7937
|
init_opencode_lock();
|
|
7709
7938
|
init_api_error_shape();
|
|
7710
7939
|
init_errors();
|
|
@@ -7725,7 +7954,7 @@ var init_launch = __esm({
|
|
|
7725
7954
|
|
|
7726
7955
|
// src/turn-control-tools.ts
|
|
7727
7956
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
7728
|
-
import { z as
|
|
7957
|
+
import { z as z17 } from "zod";
|
|
7729
7958
|
function createSkipState() {
|
|
7730
7959
|
return { skipped: false, reason: null };
|
|
7731
7960
|
}
|
|
@@ -7776,7 +8005,7 @@ function createTurnControlMcpServer(acts, skipState) {
|
|
|
7776
8005
|
SKIP_TURN_TOOL,
|
|
7777
8006
|
TURN_CONTROL_TOOL_DESCRIPTIONS.skip_turn,
|
|
7778
8007
|
{
|
|
7779
|
-
reason:
|
|
8008
|
+
reason: z17.string().min(1).max(MAX_SKIP_REASON_BYTES).describe(TURN_CONTROL_FIELD_DESCRIPTIONS.skip_turn.reason)
|
|
7780
8009
|
},
|
|
7781
8010
|
async (args) => {
|
|
7782
8011
|
skipState.skipped = true;
|
|
@@ -7792,42 +8021,42 @@ function createTurnControlMcpServer(acts, skipState) {
|
|
|
7792
8021
|
ASK_TOOL,
|
|
7793
8022
|
TURN_CONTROL_TOOL_DESCRIPTIONS.ask,
|
|
7794
8023
|
{
|
|
7795
|
-
targetUserId:
|
|
7796
|
-
question:
|
|
7797
|
-
headline:
|
|
7798
|
-
options:
|
|
7799
|
-
|
|
7800
|
-
|
|
7801
|
-
|
|
7802
|
-
label:
|
|
7803
|
-
move:
|
|
7804
|
-
task:
|
|
7805
|
-
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"])
|
|
7806
8035
|
}).describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask["options.move"])
|
|
7807
8036
|
})
|
|
7808
8037
|
])
|
|
7809
8038
|
).min(MIN_ASK_OPTIONS).max(MAX_ASK_OPTIONS).optional().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask.options),
|
|
7810
|
-
questions:
|
|
7811
|
-
|
|
7812
|
-
headline:
|
|
7813
|
-
body:
|
|
7814
|
-
options:
|
|
7815
|
-
|
|
7816
|
-
|
|
7817
|
-
|
|
7818
|
-
label:
|
|
7819
|
-
move:
|
|
7820
|
-
task:
|
|
7821
|
-
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"])
|
|
7822
8051
|
}).describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask["questions.options.move"])
|
|
7823
8052
|
})
|
|
7824
8053
|
])
|
|
7825
8054
|
).min(MIN_ASK_OPTIONS).max(MAX_ASK_OPTIONS).optional().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask["questions.options"])
|
|
7826
8055
|
})
|
|
7827
8056
|
).min(1).max(MAX_ASK_ITEMS).optional().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask.questions),
|
|
7828
|
-
replaces:
|
|
7829
|
-
askId:
|
|
7830
|
-
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"])
|
|
7831
8060
|
}).optional().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask.replaces)
|
|
7832
8061
|
},
|
|
7833
8062
|
async (args) => {
|
|
@@ -7868,9 +8097,9 @@ function createTurnControlMcpServer(acts, skipState) {
|
|
|
7868
8097
|
WAKE_ME_TOOL,
|
|
7869
8098
|
TURN_CONTROL_TOOL_DESCRIPTIONS.wake_me,
|
|
7870
8099
|
{
|
|
7871
|
-
afterSeconds:
|
|
7872
|
-
at:
|
|
7873
|
-
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)
|
|
7874
8103
|
},
|
|
7875
8104
|
async (args) => {
|
|
7876
8105
|
const hasAfter = args.afterSeconds !== void 0;
|
|
@@ -7904,8 +8133,8 @@ function createTurnControlMcpServer(acts, skipState) {
|
|
|
7904
8133
|
WITHDRAW_ASK_TOOL,
|
|
7905
8134
|
TURN_CONTROL_TOOL_DESCRIPTIONS.withdraw_ask,
|
|
7906
8135
|
{
|
|
7907
|
-
askId:
|
|
7908
|
-
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)
|
|
7909
8138
|
},
|
|
7910
8139
|
async (args) => performAct(acts, {
|
|
7911
8140
|
kind: "withdraw_ask",
|
|
@@ -8498,16 +8727,6 @@ var init_turn_runtime_integrity = __esm({
|
|
|
8498
8727
|
// src/turn-execution.ts
|
|
8499
8728
|
import { createHash as createHash2, randomUUID as randomUUID3 } from "crypto";
|
|
8500
8729
|
import { existsSync as existsSync9 } from "fs";
|
|
8501
|
-
function missingSecretReason(missing) {
|
|
8502
|
-
const prefix = "missing_secret: ";
|
|
8503
|
-
for (let shown = missing.length; shown >= 0; shown--) {
|
|
8504
|
-
const omitted = missing.length - shown;
|
|
8505
|
-
const parts = [...missing.slice(0, shown), ...omitted > 0 ? [`+${omitted}`] : []];
|
|
8506
|
-
const reason = prefix + parts.join(",");
|
|
8507
|
-
if (reason.length <= ERROR_REASON_MAX) return reason;
|
|
8508
|
-
}
|
|
8509
|
-
return `${prefix}+${missing.length}`;
|
|
8510
|
-
}
|
|
8511
8730
|
function resolveIdleTimeoutMs(override) {
|
|
8512
8731
|
return override ?? DEFAULT_AGENT_IDLE_TIMEOUT_MS;
|
|
8513
8732
|
}
|
|
@@ -8544,7 +8763,7 @@ function fingerprintSessionState(state) {
|
|
|
8544
8763
|
}
|
|
8545
8764
|
return createHash2("sha256").update(opaqueId).digest("hex").slice(0, 16);
|
|
8546
8765
|
}
|
|
8547
|
-
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;
|
|
8548
8767
|
var init_turn_execution = __esm({
|
|
8549
8768
|
"src/turn-execution.ts"() {
|
|
8550
8769
|
"use strict";
|
|
@@ -8568,8 +8787,8 @@ var init_turn_execution = __esm({
|
|
|
8568
8787
|
init_turn_committer();
|
|
8569
8788
|
init_turn_runtime_integrity();
|
|
8570
8789
|
init_dispatcher();
|
|
8790
|
+
init_sandbox_policy();
|
|
8571
8791
|
PREPARING_TOOL_NAME = "preparing";
|
|
8572
|
-
ERROR_REASON_MAX = 200;
|
|
8573
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:";
|
|
8574
8793
|
NO_TURN_TOKEN_REASON = "no_turn_token: the server issued no turn credential for this turn";
|
|
8575
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`;
|
|
@@ -9133,7 +9352,18 @@ var init_turn_execution = __esm({
|
|
|
9133
9352
|
const { payload, workspaceId, turnId, turnLog } = this;
|
|
9134
9353
|
await guardBundledBinary(this.integrityContext(), (r, e) => this.concluded(r, e), this.outcome);
|
|
9135
9354
|
const onWarn = (msg, meta) => turnLog.debug(meta ?? {}, msg);
|
|
9136
|
-
|
|
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
|
+
}
|
|
9137
9367
|
const adapters = [];
|
|
9138
9368
|
if (this.opts.claudeCodeAvailable?.() ?? true) {
|
|
9139
9369
|
this.containment = createTurnContainment(this.turnId, {
|
|
@@ -10009,10 +10239,20 @@ var init_git_credential = __esm({
|
|
|
10009
10239
|
|
|
10010
10240
|
// src/runner/harness-sandbox.ts
|
|
10011
10241
|
import { lookup } from "dns/promises";
|
|
10012
|
-
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";
|
|
10013
10252
|
import { isIP } from "net";
|
|
10014
10253
|
import { homedir as homedir3, networkInterfaces, tmpdir as tmpdir3, userInfo as userInfo2 } from "os";
|
|
10015
|
-
import { join as join18 } from "path";
|
|
10254
|
+
import { dirname as dirname8, join as join18 } from "path";
|
|
10255
|
+
import { fileURLToPath as fileURLToPath4 } from "url";
|
|
10016
10256
|
import { SandboxManager } from "@anthropic-ai/sandbox-runtime";
|
|
10017
10257
|
function accountHome() {
|
|
10018
10258
|
try {
|
|
@@ -10042,32 +10282,117 @@ async function resolvedAddresses(baseUrl) {
|
|
|
10042
10282
|
return [];
|
|
10043
10283
|
}
|
|
10044
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
|
+
}
|
|
10045
10317
|
function ownAddresses() {
|
|
10046
10318
|
return Object.values(networkInterfaces()).flat().flatMap((i) => i ? [i.address] : []);
|
|
10047
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
|
+
}
|
|
10048
10338
|
function harnessSandboxConfig(input) {
|
|
10339
|
+
const { policy } = input;
|
|
10049
10340
|
const claudeDir = input.claudeConfigDir ?? join18(input.home, ".claude");
|
|
10050
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);
|
|
10051
10374
|
return {
|
|
10052
10375
|
network: {
|
|
10053
|
-
allowedDomains
|
|
10054
|
-
|
|
10055
|
-
|
|
10056
|
-
|
|
10057
|
-
|
|
10058
|
-
|
|
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
|
+
} : {}
|
|
10059
10392
|
},
|
|
10060
10393
|
filesystem: {
|
|
10061
|
-
|
|
10062
|
-
|
|
10063
|
-
denyRead: [
|
|
10064
|
-
input.cabaneDir,
|
|
10065
|
-
...[.../* @__PURE__ */ new Set([input.home, input.accountHome ?? input.home])].flatMap((home) => [
|
|
10066
|
-
join18(home, ".ssh"),
|
|
10067
|
-
join18(home, ".aws"),
|
|
10068
|
-
join18(home, ".gnupg")
|
|
10069
|
-
])
|
|
10070
|
-
],
|
|
10394
|
+
denyRead,
|
|
10395
|
+
...allowRead.length ? { allowRead } : {},
|
|
10071
10396
|
// The checkout, scratch, and the harnesses' own session state — a turn
|
|
10072
10397
|
// resumes from `~/.claude` and `~/.codex`, and without write there every
|
|
10073
10398
|
// resume starts from nothing.
|
|
@@ -10076,12 +10401,27 @@ function harnessSandboxConfig(input) {
|
|
|
10076
10401
|
input.tmp,
|
|
10077
10402
|
claudeDir,
|
|
10078
10403
|
join18(input.home, ".claude.json"),
|
|
10079
|
-
codexDir
|
|
10404
|
+
codexDir,
|
|
10405
|
+
...expand(policy.filesystem?.allowWrite)
|
|
10080
10406
|
],
|
|
10081
10407
|
denyWrite: []
|
|
10082
|
-
}
|
|
10408
|
+
},
|
|
10409
|
+
...masked.length ? { credentials: { envVars: masked } } : {}
|
|
10083
10410
|
};
|
|
10084
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
|
+
}
|
|
10085
10425
|
function osSandboxUsable(log) {
|
|
10086
10426
|
if (!SandboxManager.isSupportedPlatform()) {
|
|
10087
10427
|
log.warn(
|
|
@@ -10102,7 +10442,9 @@ function osSandboxUsable(log) {
|
|
|
10102
10442
|
}
|
|
10103
10443
|
async function prepareHarnessSandbox(input) {
|
|
10104
10444
|
let wrapped;
|
|
10445
|
+
const held = input.credentials.map((c) => c.name);
|
|
10105
10446
|
try {
|
|
10447
|
+
for (const c of input.credentials) process.env[c.name] = c.value;
|
|
10106
10448
|
const config = harnessSandboxConfig({
|
|
10107
10449
|
cwd: input.cwd,
|
|
10108
10450
|
baseUrl: input.baseUrl,
|
|
@@ -10113,28 +10455,50 @@ async function prepareHarnessSandbox(input) {
|
|
|
10113
10455
|
tmp: tmpdir3(),
|
|
10114
10456
|
cabaneDir: cabaneDir(),
|
|
10115
10457
|
claudeConfigDir: process.env.CLAUDE_CONFIG_DIR,
|
|
10116
|
-
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()
|
|
10117
10463
|
});
|
|
10118
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
|
+
}
|
|
10119
10471
|
input.log.debug(
|
|
10120
10472
|
{
|
|
10121
10473
|
allowWrite: config.filesystem.allowWrite,
|
|
10122
10474
|
denyRead: config.filesystem.denyRead,
|
|
10123
|
-
|
|
10475
|
+
allowRead: config.filesystem.allowRead,
|
|
10476
|
+
hosts: config.network.allowedDomains,
|
|
10477
|
+
masked: held
|
|
10124
10478
|
},
|
|
10125
10479
|
"runners: harness sandbox ready"
|
|
10126
10480
|
);
|
|
10127
10481
|
wrapped = await SandboxManager.wrapWithSandbox(
|
|
10128
|
-
|
|
10482
|
+
"export NO_PROXY= no_proxy=; " + gitCredentialExports(input.credentials) + 'eval "exec $CABANE_SANDBOXED_ARGV"'
|
|
10129
10483
|
);
|
|
10130
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
|
+
}
|
|
10131
10495
|
input.log.warn(
|
|
10132
|
-
{ err:
|
|
10496
|
+
{ err: message },
|
|
10133
10497
|
"runners: the OS sandbox could not start; running the turn unsandboxed"
|
|
10134
10498
|
);
|
|
10135
|
-
await SandboxManager.reset().catch(() => {
|
|
10136
|
-
});
|
|
10137
10499
|
return null;
|
|
10500
|
+
} finally {
|
|
10501
|
+
for (const name of held) delete process.env[name];
|
|
10138
10502
|
}
|
|
10139
10503
|
mkdirSync11(join18(cabaneDir(), "sandbox"), { recursive: true });
|
|
10140
10504
|
const dir2 = mkdtempSync(join18(cabaneDir(), "sandbox", "turn-"));
|
|
@@ -10167,23 +10531,28 @@ exec ${shellQuote(script)} ${shellQuote(real)} "$@"
|
|
|
10167
10531
|
}
|
|
10168
10532
|
};
|
|
10169
10533
|
}
|
|
10170
|
-
var
|
|
10534
|
+
var SECRET_PATHS_UNDER_HOME;
|
|
10171
10535
|
var init_harness_sandbox = __esm({
|
|
10172
10536
|
"src/runner/harness-sandbox.ts"() {
|
|
10173
10537
|
"use strict";
|
|
10538
|
+
init_bundled_binaries();
|
|
10174
10539
|
init_config();
|
|
10175
|
-
|
|
10176
|
-
|
|
10177
|
-
"
|
|
10178
|
-
"
|
|
10179
|
-
"
|
|
10180
|
-
"
|
|
10181
|
-
"
|
|
10182
|
-
"
|
|
10183
|
-
"
|
|
10184
|
-
"
|
|
10185
|
-
"
|
|
10186
|
-
"
|
|
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"
|
|
10187
10556
|
];
|
|
10188
10557
|
}
|
|
10189
10558
|
});
|
|
@@ -10193,7 +10562,7 @@ import {
|
|
|
10193
10562
|
existsSync as existsSync14,
|
|
10194
10563
|
mkdirSync as mkdirSync12,
|
|
10195
10564
|
readdirSync as readdirSync5,
|
|
10196
|
-
readFileSync as
|
|
10565
|
+
readFileSync as readFileSync11,
|
|
10197
10566
|
renameSync as renameSync6,
|
|
10198
10567
|
rmSync as rmSync11,
|
|
10199
10568
|
writeFileSync as writeFileSync8
|
|
@@ -10263,7 +10632,7 @@ var init_outbox = __esm({
|
|
|
10263
10632
|
if (!name.endsWith(".json")) continue;
|
|
10264
10633
|
const full = join19(dir2, name);
|
|
10265
10634
|
try {
|
|
10266
|
-
const parsed = JSON.parse(
|
|
10635
|
+
const parsed = JSON.parse(readFileSync11(full, "utf8"));
|
|
10267
10636
|
if (parsed && typeof parsed.turnId === "string" && typeof parsed.seq === "number" && typeof parsed.path === "string") {
|
|
10268
10637
|
entries.push(parsed);
|
|
10269
10638
|
} else {
|
|
@@ -10367,12 +10736,12 @@ var init_outbox = __esm({
|
|
|
10367
10736
|
});
|
|
10368
10737
|
|
|
10369
10738
|
// src/version.ts
|
|
10370
|
-
import { createRequire as
|
|
10739
|
+
import { createRequire as createRequire3 } from "module";
|
|
10371
10740
|
var pkg, COMPANION_VERSION;
|
|
10372
10741
|
var init_version2 = __esm({
|
|
10373
10742
|
"src/version.ts"() {
|
|
10374
10743
|
"use strict";
|
|
10375
|
-
pkg =
|
|
10744
|
+
pkg = createRequire3(import.meta.url)("../package.json");
|
|
10376
10745
|
COMPANION_VERSION = pkg.version;
|
|
10377
10746
|
}
|
|
10378
10747
|
});
|
|
@@ -10609,10 +10978,13 @@ function runnerDispatcherOptions(input, log, emit, extras = {}) {
|
|
|
10609
10978
|
}
|
|
10610
10979
|
},
|
|
10611
10980
|
...spec.sandbox === "os" ? {
|
|
10612
|
-
harnessSandbox: async ({ cwd, log: turnLog }) => {
|
|
10981
|
+
harnessSandbox: async ({ cwd, runtime, log: turnLog }) => {
|
|
10613
10982
|
const sandbox = await prepareHarnessSandbox({
|
|
10614
10983
|
cwd,
|
|
10615
10984
|
baseUrl: input.baseUrl,
|
|
10985
|
+
runtime,
|
|
10986
|
+
policy: spec.sandboxPolicy ?? {},
|
|
10987
|
+
credentials: input.sandboxCredentials ?? [],
|
|
10616
10988
|
log: turnLog
|
|
10617
10989
|
});
|
|
10618
10990
|
if (sandbox) onSandbox?.(sandbox);
|
|
@@ -11886,7 +12258,7 @@ async function pair(opts = {}) {
|
|
|
11886
12258
|
}
|
|
11887
12259
|
|
|
11888
12260
|
// src/cli.ts
|
|
11889
|
-
import { readFileSync as
|
|
12261
|
+
import { readFileSync as readFileSync14 } from "fs";
|
|
11890
12262
|
|
|
11891
12263
|
// src/commands/logs.ts
|
|
11892
12264
|
import { once } from "events";
|
|
@@ -13438,6 +13810,15 @@ var UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
|
13438
13810
|
function containerName(attempt) {
|
|
13439
13811
|
return `cabane-runner-${attempt.conversationId}-${attempt.agentId}`;
|
|
13440
13812
|
}
|
|
13813
|
+
var networkName = containerName;
|
|
13814
|
+
function networkCreateError(stderr) {
|
|
13815
|
+
const said = stderr.trim();
|
|
13816
|
+
if (/address pool|fully subnetted/i.test(said))
|
|
13817
|
+
return new Error(
|
|
13818
|
+
`docker: couldn't create this conversation's runner network: the engine has no address pool left for another network. Each conversation and agent with a runner container holds one; remove kept runner containers, or widen \`default-address-pools\` in the daemon.json. Docker said: ${said}`
|
|
13819
|
+
);
|
|
13820
|
+
return new Error(`docker: creating the runner network failed: ${said || "no reason given"}`);
|
|
13821
|
+
}
|
|
13441
13822
|
function sessionArchivePath(deviceDir, attemptId) {
|
|
13442
13823
|
if (!UUID_RE.test(attemptId)) throw new Error(`docker: not an attempt id: ${attemptId}`);
|
|
13443
13824
|
return join17(deviceDir, "attempts", attemptId, "session.tar");
|
|
@@ -13514,7 +13895,7 @@ function createDockerProvider(opts) {
|
|
|
13514
13895
|
const killGraceMs = opts.killGraceMs ?? DEFAULT_KILL_GRACE_MS2;
|
|
13515
13896
|
const sessionCap = opts.sessionCapBytes ?? DEFAULT_SESSION_CAP_BYTES;
|
|
13516
13897
|
const collectCap = opts.collectCapBytes ?? COLLECT_CAP_BYTES;
|
|
13517
|
-
const
|
|
13898
|
+
const sharedNetwork = `cabane-runners-${opts.deviceKey}`;
|
|
13518
13899
|
const companionLabel = createHash4("sha256").update(opts.companion.key).digest("hex").slice(0, 16);
|
|
13519
13900
|
const proxy = `cabane-egress-${opts.deviceKey}`;
|
|
13520
13901
|
const held = /* @__PURE__ */ new Map();
|
|
@@ -13547,32 +13928,14 @@ function createDockerProvider(opts) {
|
|
|
13547
13928
|
}
|
|
13548
13929
|
throw new Error("docker: the egress proxy never started listening");
|
|
13549
13930
|
};
|
|
13550
|
-
let
|
|
13931
|
+
let proxyReady = null;
|
|
13551
13932
|
const proxyKey = async () => {
|
|
13552
13933
|
const id = (await docker.run(["image", "inspect", "-f", "{{.Id}}", opts.image])).stdout.trim();
|
|
13553
13934
|
return createHash4("sha256").update(JSON.stringify([id || opts.image, opts.companion.key, [...opts.egressAllow].sort()])).digest("hex").slice(0, 16);
|
|
13554
13935
|
};
|
|
13555
|
-
const
|
|
13556
|
-
|
|
13936
|
+
const ensureProxy = () => {
|
|
13937
|
+
proxyReady ??= (async () => {
|
|
13557
13938
|
const allowKey = await proxyKey();
|
|
13558
|
-
if ((await docker.run(["network", "inspect", network])).code !== 0) {
|
|
13559
|
-
await must(
|
|
13560
|
-
[
|
|
13561
|
-
"network",
|
|
13562
|
-
"create",
|
|
13563
|
-
// No route off the host, and no address of the host's on the
|
|
13564
|
-
// bridge: the host's own ports are out of reach too. The proxy is
|
|
13565
|
-
// the only other thing on the network.
|
|
13566
|
-
"--internal",
|
|
13567
|
-
"-o",
|
|
13568
|
-
"com.docker.network.bridge.inhibit_ipv4=true",
|
|
13569
|
-
"--label",
|
|
13570
|
-
`cabane.device=${opts.deviceKey}`,
|
|
13571
|
-
network
|
|
13572
|
-
],
|
|
13573
|
-
"creating the runners network"
|
|
13574
|
-
);
|
|
13575
|
-
}
|
|
13576
13939
|
const seen = await docker.run([
|
|
13577
13940
|
"inspect",
|
|
13578
13941
|
"-f",
|
|
@@ -13581,6 +13944,7 @@ function createDockerProvider(opts) {
|
|
|
13581
13944
|
]);
|
|
13582
13945
|
const [key, running] = seen.stdout.trim().split(" ");
|
|
13583
13946
|
if (seen.code === 0 && key === allowKey) {
|
|
13947
|
+
await retireSharedNetwork();
|
|
13584
13948
|
if (running !== "true") {
|
|
13585
13949
|
await must(["start", proxy], "starting the egress proxy");
|
|
13586
13950
|
await proxyListening();
|
|
@@ -13615,17 +13979,61 @@ function createDockerProvider(opts) {
|
|
|
13615
13979
|
],
|
|
13616
13980
|
"starting the egress proxy"
|
|
13617
13981
|
);
|
|
13618
|
-
await
|
|
13619
|
-
["network", "connect", "--alias", PROXY_ALIAS, network, proxy],
|
|
13620
|
-
"connecting the proxy"
|
|
13621
|
-
);
|
|
13982
|
+
await retireSharedNetwork();
|
|
13622
13983
|
await proxyListening();
|
|
13623
|
-
log.info({
|
|
13984
|
+
log.info({ proxy, allow: opts.egressAllow }, "runners: egress proxy ready");
|
|
13624
13985
|
})().catch((err) => {
|
|
13625
|
-
|
|
13986
|
+
proxyReady = null;
|
|
13626
13987
|
throw err;
|
|
13627
13988
|
});
|
|
13628
|
-
return
|
|
13989
|
+
return proxyReady;
|
|
13990
|
+
};
|
|
13991
|
+
const retireSharedNetwork = async () => {
|
|
13992
|
+
if ((await docker.run(["network", "inspect", sharedNetwork])).code !== 0) return;
|
|
13993
|
+
await docker.run(["network", "disconnect", "-f", sharedNetwork, proxy]);
|
|
13994
|
+
const on = await docker.run(["ps", "-aq", "--filter", `network=${sharedNetwork}`]);
|
|
13995
|
+
if (on.code === 0 && !on.stdout.trim()) await docker.run(["network", "rm", sharedNetwork]);
|
|
13996
|
+
};
|
|
13997
|
+
const networksOf = async (container) => (await docker.run([
|
|
13998
|
+
"inspect",
|
|
13999
|
+
"-f",
|
|
14000
|
+
"{{range $k, $_ := .NetworkSettings.Networks}}{{$k}} {{end}}",
|
|
14001
|
+
container
|
|
14002
|
+
])).stdout.split(/\s+/).filter(Boolean);
|
|
14003
|
+
const ensurePairNetwork = async (net, attempt) => {
|
|
14004
|
+
if ((await docker.run(["network", "inspect", net])).code !== 0) {
|
|
14005
|
+
const made = await docker.run([
|
|
14006
|
+
"network",
|
|
14007
|
+
"create",
|
|
14008
|
+
// No route off the host, and no address of the host's on the bridge:
|
|
14009
|
+
// the host's own ports are out of reach too. The proxy is the only
|
|
14010
|
+
// other thing on the network.
|
|
14011
|
+
"--internal",
|
|
14012
|
+
"-o",
|
|
14013
|
+
"com.docker.network.bridge.inhibit_ipv4=true",
|
|
14014
|
+
"--label",
|
|
14015
|
+
`cabane.device=${opts.deviceKey}`,
|
|
14016
|
+
"--label",
|
|
14017
|
+
`cabane.conversation=${attempt.conversationId}`,
|
|
14018
|
+
"--label",
|
|
14019
|
+
`cabane.agent=${attempt.agentId}`,
|
|
14020
|
+
net
|
|
14021
|
+
]);
|
|
14022
|
+
if (made.code !== 0) throw networkCreateError(made.stderr);
|
|
14023
|
+
}
|
|
14024
|
+
if (!(await networksOf(proxy)).includes(net))
|
|
14025
|
+
await must(
|
|
14026
|
+
["network", "connect", "--alias", PROXY_ALIAS, net, proxy],
|
|
14027
|
+
"connecting the proxy"
|
|
14028
|
+
);
|
|
14029
|
+
};
|
|
14030
|
+
const settleNetworks = async (name, net) => {
|
|
14031
|
+
const on = await networksOf(name);
|
|
14032
|
+
if (!on.includes(net)) await must(["network", "connect", net, name], "moving the container");
|
|
14033
|
+
const others = on.filter((n) => n !== net);
|
|
14034
|
+
for (const other of others)
|
|
14035
|
+
await must(["network", "disconnect", "-f", other, name], "moving the container");
|
|
14036
|
+
if (others.includes(sharedNetwork)) await retireSharedNetwork();
|
|
13629
14037
|
};
|
|
13630
14038
|
const socketRoot = opts.socketRoot ?? join17(
|
|
13631
14039
|
process.env.XDG_RUNTIME_DIR && existsSync13(process.env.XDG_RUNTIME_DIR) ? process.env.XDG_RUNTIME_DIR : tmpdir2(),
|
|
@@ -13657,7 +14065,7 @@ function createDockerProvider(opts) {
|
|
|
13657
14065
|
"--label",
|
|
13658
14066
|
`cabane.companion=${companionLabel}`,
|
|
13659
14067
|
"--network",
|
|
13660
|
-
|
|
14068
|
+
networkName(attempt),
|
|
13661
14069
|
"--mount",
|
|
13662
14070
|
`type=volume,src=${name}-home,dst=${CONTAINER_HOME}`,
|
|
13663
14071
|
"--mount",
|
|
@@ -13683,6 +14091,17 @@ function createDockerProvider(opts) {
|
|
|
13683
14091
|
await docker.run(["rm", "-f", "-v", name]);
|
|
13684
14092
|
await docker.run(["volume", "rm", "-f", `${name}-home`, `${name}-work`]);
|
|
13685
14093
|
rmSync9(socketDir(name), { recursive: true, force: true });
|
|
14094
|
+
await dropNetwork(name);
|
|
14095
|
+
};
|
|
14096
|
+
const dropNetwork = async (name) => {
|
|
14097
|
+
if ((await docker.run(["network", "inspect", name])).code !== 0) return;
|
|
14098
|
+
await docker.run(["network", "disconnect", "-f", name, proxy]);
|
|
14099
|
+
const gone = await docker.run(["network", "rm", name]);
|
|
14100
|
+
if (gone.code !== 0)
|
|
14101
|
+
log.warn(
|
|
14102
|
+
{ network: name, err: gone.stderr.trim() },
|
|
14103
|
+
"runners: couldn't remove a pair's network"
|
|
14104
|
+
);
|
|
13686
14105
|
};
|
|
13687
14106
|
const containerEnv = () => {
|
|
13688
14107
|
const proxyUrl = `http://${PROXY_ALIAS}:${PROXY_PORT}`;
|
|
@@ -13849,7 +14268,7 @@ function createDockerProvider(opts) {
|
|
|
13849
14268
|
});
|
|
13850
14269
|
await before;
|
|
13851
14270
|
try {
|
|
13852
|
-
await
|
|
14271
|
+
await ensureProxy();
|
|
13853
14272
|
mkdirSync10(socketRoot, { recursive: true, mode: 448 });
|
|
13854
14273
|
mkdirSync10(socketDir(name), { recursive: true, mode: 457 });
|
|
13855
14274
|
let seen = await docker.run([
|
|
@@ -13879,9 +14298,12 @@ function createDockerProvider(opts) {
|
|
|
13879
14298
|
);
|
|
13880
14299
|
seen = { code: 1, stdout: "", stderr: "" };
|
|
13881
14300
|
}
|
|
14301
|
+
await ensurePairNetwork(networkName(attempt), attempt);
|
|
13882
14302
|
if (seen.code !== 0) {
|
|
13883
14303
|
await create(name, attempt);
|
|
13884
14304
|
log.debug({ container: name }, "runners: container created");
|
|
14305
|
+
} else {
|
|
14306
|
+
await settleNetworks(name, networkName(attempt));
|
|
13885
14307
|
}
|
|
13886
14308
|
if (seen.stdout.trim().split(" ")[0] !== "true")
|
|
13887
14309
|
await must(["start", name], "starting the container");
|
|
@@ -13895,7 +14317,8 @@ function createDockerProvider(opts) {
|
|
|
13895
14317
|
return { provider: "docker", id: name };
|
|
13896
14318
|
} catch (err) {
|
|
13897
14319
|
const keep = opts.keepContainers || readPending(name).length > 0;
|
|
13898
|
-
|
|
14320
|
+
const kept = keep && (await docker.run(["inspect", "-f", "{{.Id}}", name])).code === 0;
|
|
14321
|
+
await (kept ? docker.run(["stop", "-t", "1", name]) : keep ? dropNetwork(networkName(attempt)) : destroy(name)).catch(() => {
|
|
13899
14322
|
});
|
|
13900
14323
|
release();
|
|
13901
14324
|
throw err;
|
|
@@ -14287,6 +14710,7 @@ var DeviceLocks = class {
|
|
|
14287
14710
|
|
|
14288
14711
|
// src/supervisor.ts
|
|
14289
14712
|
init_harness_sandbox();
|
|
14713
|
+
init_sandbox_policy();
|
|
14290
14714
|
init_launch();
|
|
14291
14715
|
|
|
14292
14716
|
// src/opencode-models.ts
|
|
@@ -14629,6 +15053,13 @@ var CompanionSupervisor = class {
|
|
|
14629
15053
|
}
|
|
14630
15054
|
this.latchBootBundledState();
|
|
14631
15055
|
}
|
|
15056
|
+
if (this.runnerSandbox() === "os") {
|
|
15057
|
+
const policy = this.sandboxPolicy();
|
|
15058
|
+
const posture = postureLines(policy);
|
|
15059
|
+
const summary = posture.join("; ");
|
|
15060
|
+
if (policy.ok) this.log.info({ posture }, `runners: OS sandbox policy \u2014 ${summary}`);
|
|
15061
|
+
else this.log.error({ posture }, `runners: ${summary}`);
|
|
15062
|
+
}
|
|
14632
15063
|
const startupHarnessRefresh = this.refreshHarnessStatuses();
|
|
14633
15064
|
this.trackHeartbeat(startupHarnessRefresh);
|
|
14634
15065
|
if (!this.config.deviceToken) {
|
|
@@ -15070,6 +15501,13 @@ var CompanionSupervisor = class {
|
|
|
15070
15501
|
// CT1679: `runners.sandbox`, checked against this host once. A host that
|
|
15071
15502
|
// can't make the sandbox says so once and runs its turns unwrapped.
|
|
15072
15503
|
sandboxUsable;
|
|
15504
|
+
// CT1684: `runners.policy`, parsed once. An invalid one is kept as the
|
|
15505
|
+
// error, which every sandboxed turn is refused with.
|
|
15506
|
+
parsedSandboxPolicy;
|
|
15507
|
+
sandboxPolicy() {
|
|
15508
|
+
this.parsedSandboxPolicy ??= parseSandboxPolicy(this.config.runners?.policy);
|
|
15509
|
+
return this.parsedSandboxPolicy;
|
|
15510
|
+
}
|
|
15073
15511
|
runnerSandbox() {
|
|
15074
15512
|
if (this.config.runners?.sandbox !== "os" || this.dockerRunners()) return "none";
|
|
15075
15513
|
this.sandboxUsable ??= osSandboxUsable(this.log);
|
|
@@ -15189,7 +15627,8 @@ var CompanionSupervisor = class {
|
|
|
15189
15627
|
reportRunnerGone: (report) => this.reportRunnerGone(report),
|
|
15190
15628
|
connectorHealth: this.connectorHealth,
|
|
15191
15629
|
locks: this.runnerLocks,
|
|
15192
|
-
sandbox: this.runnerSandbox()
|
|
15630
|
+
sandbox: this.runnerSandbox(),
|
|
15631
|
+
policy: this.sandboxPolicy()
|
|
15193
15632
|
}
|
|
15194
15633
|
} : {}
|
|
15195
15634
|
});
|
|
@@ -15890,7 +16329,7 @@ function handleUncaught(log, err, origin) {
|
|
|
15890
16329
|
|
|
15891
16330
|
// src/crash-marker.ts
|
|
15892
16331
|
init_config();
|
|
15893
|
-
import { existsSync as existsSync15, mkdirSync as mkdirSync13, readFileSync as
|
|
16332
|
+
import { existsSync as existsSync15, mkdirSync as mkdirSync13, readFileSync as readFileSync12, rmSync as rmSync12, writeFileSync as writeFileSync9 } from "fs";
|
|
15894
16333
|
import { join as join20 } from "path";
|
|
15895
16334
|
function crashMarkerPath() {
|
|
15896
16335
|
return join20(cabaneDir(), "last-error.json");
|
|
@@ -16083,11 +16522,11 @@ import { closeSync as closeSync3, mkdirSync as mkdirSync14, openSync as openSync
|
|
|
16083
16522
|
|
|
16084
16523
|
// src/cli-entry.ts
|
|
16085
16524
|
import { existsSync as existsSync16 } from "fs";
|
|
16086
|
-
import { fileURLToPath as
|
|
16525
|
+
import { fileURLToPath as fileURLToPath5 } from "url";
|
|
16087
16526
|
var RELATIVE_CANDIDATES = ["./cli.js", "../dist/cli.js", "../cli.js"];
|
|
16088
16527
|
function companionCliEntry(deps = {}) {
|
|
16089
16528
|
const exists = deps.exists ?? existsSync16;
|
|
16090
|
-
const candidates = deps.candidates ?? RELATIVE_CANDIDATES.map((rel) =>
|
|
16529
|
+
const candidates = deps.candidates ?? RELATIVE_CANDIDATES.map((rel) => fileURLToPath5(new URL(rel, import.meta.url)));
|
|
16091
16530
|
for (const candidate of candidates) {
|
|
16092
16531
|
if (exists(candidate)) return candidate;
|
|
16093
16532
|
}
|
|
@@ -16481,6 +16920,7 @@ async function runAttached(runtime) {
|
|
|
16481
16920
|
// src/commands/status.ts
|
|
16482
16921
|
init_config();
|
|
16483
16922
|
import { readdirSync as readdirSync6 } from "fs";
|
|
16923
|
+
init_sandbox_policy();
|
|
16484
16924
|
init_secrets();
|
|
16485
16925
|
init_transcript_writer();
|
|
16486
16926
|
async function status() {
|
|
@@ -16531,6 +16971,13 @@ async function status() {
|
|
|
16531
16971
|
process.stdout.write(`prepare hook: ${cfg.prepareHook.command} (device-level, all agents)
|
|
16532
16972
|
`);
|
|
16533
16973
|
}
|
|
16974
|
+
if (runnersEnabled(cfg) && cfg.runners?.sandbox === "os") {
|
|
16975
|
+
process.stdout.write("sandbox: os\n");
|
|
16976
|
+
for (const line of postureLines(parseSandboxPolicy(cfg.runners.policy))) {
|
|
16977
|
+
process.stdout.write(` ${line}
|
|
16978
|
+
`);
|
|
16979
|
+
}
|
|
16980
|
+
}
|
|
16534
16981
|
const overrides2 = Object.keys(cfg.agents ?? {});
|
|
16535
16982
|
if (overrides2.length > 0) {
|
|
16536
16983
|
process.stdout.write(`local overrides for: ${overrides2.join(", ")}
|
|
@@ -16623,7 +17070,7 @@ function isAlive(kill, pid) {
|
|
|
16623
17070
|
init_errors();
|
|
16624
17071
|
init_src();
|
|
16625
17072
|
init_transcript_writer();
|
|
16626
|
-
import { existsSync as existsSync17, readFileSync as
|
|
17073
|
+
import { existsSync as existsSync17, readFileSync as readFileSync13, readdirSync as readdirSync7 } from "fs";
|
|
16627
17074
|
import { isAbsolute, join as join21 } from "path";
|
|
16628
17075
|
async function transcript(opts = {}) {
|
|
16629
17076
|
const dir2 = transcriptsDir();
|
|
@@ -16724,7 +17171,7 @@ function isComplete(content) {
|
|
|
16724
17171
|
async function followTranscripts(dir2) {
|
|
16725
17172
|
const follower = new TranscriptFollower({
|
|
16726
17173
|
listFiles: () => listFiles(dir2),
|
|
16727
|
-
read: (f) =>
|
|
17174
|
+
read: (f) => readFileSync13(join21(dir2, f), "utf8"),
|
|
16728
17175
|
write: (s) => process.stdout.write(s),
|
|
16729
17176
|
// CSI: cursor up `n` lines, then erase from cursor to end of screen.
|
|
16730
17177
|
clearLines: (n) => process.stdout.write(`\x1B[${n}A\x1B[0J`),
|
|
@@ -16785,7 +17232,7 @@ function peek(path) {
|
|
|
16785
17232
|
let meta;
|
|
16786
17233
|
let outcome;
|
|
16787
17234
|
try {
|
|
16788
|
-
for (const line of
|
|
17235
|
+
for (const line of readFileSync13(path, "utf8").split("\n")) {
|
|
16789
17236
|
if (!line.trim()) continue;
|
|
16790
17237
|
const o = safeParse(line);
|
|
16791
17238
|
const t = str2(rec(o)?.type);
|
|
@@ -16818,7 +17265,7 @@ function resolveTarget(dir2, target) {
|
|
|
16818
17265
|
function renderFile(path) {
|
|
16819
17266
|
let content;
|
|
16820
17267
|
try {
|
|
16821
|
-
content =
|
|
17268
|
+
content = readFileSync13(path, "utf8");
|
|
16822
17269
|
} catch (err) {
|
|
16823
17270
|
throw new CompanionError(
|
|
16824
17271
|
`couldn't read ${path}: ${err instanceof Error ? err.message : String(err)}`
|
|
@@ -16980,7 +17427,7 @@ program.command("pair").description(
|
|
|
16980
17427
|
});
|
|
16981
17428
|
});
|
|
16982
17429
|
program.command("write-paired-config", { hidden: true }).description("persist an already-completed device enrollment payload from stdin.").action(() => {
|
|
16983
|
-
writeCompletedPairing(
|
|
17430
|
+
writeCompletedPairing(readFileSync14(0, "utf8"));
|
|
16984
17431
|
});
|
|
16985
17432
|
program.command("start").description(
|
|
16986
17433
|
"pair this device if needed, connect a coding agent on it, and run in the background."
|