@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/runtime.js
CHANGED
|
@@ -358,6 +358,10 @@ var companionConfigSchema = z2.object({
|
|
|
358
358
|
// wraps the runner in an OS sandbox where the host supports one; `'none'`
|
|
359
359
|
// (the default) runs it as a plain child beside this process, a supported
|
|
360
360
|
// configuration in its own right rather than a degraded one.
|
|
361
|
+
// CT1684: `policy` is what the OS sandbox lets a turn reach, read and hold
|
|
362
|
+
// (`runner/sandbox-policy.ts`). It is validated there rather than here, so a
|
|
363
|
+
// bad policy refuses the turns it governs, with the field named, instead of
|
|
364
|
+
// keeping the companion from starting at all.
|
|
361
365
|
runners: z2.object({
|
|
362
366
|
enabled: z2.boolean().optional(),
|
|
363
367
|
provider: z2.enum(["local-process", "docker"]).optional(),
|
|
@@ -391,7 +395,8 @@ var companionConfigSchema = z2.object({
|
|
|
391
395
|
// container's git for an allowed host, e.g. `!gh auth git-credential`.
|
|
392
396
|
// Absent: git's own configured helpers on this device.
|
|
393
397
|
gitCredentialHelper: z2.string().min(1).optional()
|
|
394
|
-
}).strict().optional()
|
|
398
|
+
}).strict().optional(),
|
|
399
|
+
policy: z2.unknown().optional()
|
|
395
400
|
}).strict().optional()
|
|
396
401
|
});
|
|
397
402
|
function runnersEnabled(cfg) {
|
|
@@ -7872,6 +7877,149 @@ import { join as join14 } from "path";
|
|
|
7872
7877
|
// src/runner/launch.ts
|
|
7873
7878
|
import { createInterface } from "readline";
|
|
7874
7879
|
|
|
7880
|
+
// src/runner/sandbox-policy.ts
|
|
7881
|
+
import { NetworkConfigSchema } from "@anthropic-ai/sandbox-runtime";
|
|
7882
|
+
import { globToRegex } from "@anthropic-ai/sandbox-runtime/dist/sandbox/sandbox-utils.js";
|
|
7883
|
+
import { z as z13 } from "zod";
|
|
7884
|
+
var hostPattern = z13.string().min(1);
|
|
7885
|
+
var secretRef = z13.string().regex(/^\$\{[A-Za-z_][A-Za-z0-9_]*\}$/, "must be a ${NAME} reference into secrets.json");
|
|
7886
|
+
var envCredentialSchema = z13.object({ secret: secretRef, hosts: z13.array(hostPattern).min(1) }).strict();
|
|
7887
|
+
var gitCredentialSchema = z13.object({
|
|
7888
|
+
host: z13.string().regex(/^[A-Za-z0-9.-]+(:\d+)?$/, "must be a host name, like github.com"),
|
|
7889
|
+
username: z13.string().min(1).optional(),
|
|
7890
|
+
secret: secretRef
|
|
7891
|
+
}).strict();
|
|
7892
|
+
var sandboxPolicySchema = z13.object({
|
|
7893
|
+
hosts: z13.object({
|
|
7894
|
+
allow: z13.array(hostPattern).optional(),
|
|
7895
|
+
deny: z13.array(hostPattern).optional(),
|
|
7896
|
+
defaults: z13.boolean().optional()
|
|
7897
|
+
}).strict().optional(),
|
|
7898
|
+
filesystem: z13.object({
|
|
7899
|
+
mode: z13.enum(["blocklist", "strict"]).optional(),
|
|
7900
|
+
denyRead: z13.array(z13.string().min(1)).optional(),
|
|
7901
|
+
allowRead: z13.array(z13.string().min(1)).optional(),
|
|
7902
|
+
allowWrite: z13.array(z13.string().min(1)).optional()
|
|
7903
|
+
}).strict().optional(),
|
|
7904
|
+
credentials: z13.object({
|
|
7905
|
+
env: z13.record(z13.string().regex(/^[A-Za-z_][A-Za-z0-9_]*$/), envCredentialSchema).optional(),
|
|
7906
|
+
git: z13.array(gitCredentialSchema).optional()
|
|
7907
|
+
}).strict().optional()
|
|
7908
|
+
}).strict();
|
|
7909
|
+
function parseSandboxPolicy(raw) {
|
|
7910
|
+
if (raw === void 0) return { ok: true, policy: {} };
|
|
7911
|
+
const result = sandboxPolicySchema.safeParse(raw);
|
|
7912
|
+
if (!result.success) {
|
|
7913
|
+
const issue = result.error.issues[0];
|
|
7914
|
+
return { ok: false, error: `${fieldPath(issue.path)}: ${issue.message}` };
|
|
7915
|
+
}
|
|
7916
|
+
const refused = unenforceable(result.data);
|
|
7917
|
+
return refused ? { ok: false, error: refused } : { ok: true, policy: result.data };
|
|
7918
|
+
}
|
|
7919
|
+
function fieldPath(path) {
|
|
7920
|
+
return ["runners", "policy", ...path.map(String)].join(".");
|
|
7921
|
+
}
|
|
7922
|
+
function unenforceable(policy) {
|
|
7923
|
+
const hostLists = [
|
|
7924
|
+
["hosts.allow", "allowedDomains", policy.hosts?.allow],
|
|
7925
|
+
["hosts.deny", "deniedDomains", policy.hosts?.deny],
|
|
7926
|
+
...Object.entries(policy.credentials?.env ?? {}).map(
|
|
7927
|
+
([name, entry]) => [
|
|
7928
|
+
`credentials.env.${name}.hosts`,
|
|
7929
|
+
"allowedDomains",
|
|
7930
|
+
entry.hosts
|
|
7931
|
+
]
|
|
7932
|
+
)
|
|
7933
|
+
];
|
|
7934
|
+
for (const [field, as, hosts] of hostLists) {
|
|
7935
|
+
for (const [i, host] of (hosts ?? []).entries()) {
|
|
7936
|
+
const check = NetworkConfigSchema.safeParse({
|
|
7937
|
+
allowedDomains: as === "allowedDomains" ? [host] : [],
|
|
7938
|
+
deniedDomains: as === "deniedDomains" ? [host] : []
|
|
7939
|
+
});
|
|
7940
|
+
if (!check.success) {
|
|
7941
|
+
const message = check.error.issues[0]?.message ?? "not a host pattern the sandbox accepts";
|
|
7942
|
+
return `runners.policy.${field}.${i}: ${message}`;
|
|
7943
|
+
}
|
|
7944
|
+
}
|
|
7945
|
+
}
|
|
7946
|
+
const pathLists = [
|
|
7947
|
+
["filesystem.denyRead", policy.filesystem?.denyRead],
|
|
7948
|
+
["filesystem.allowRead", policy.filesystem?.allowRead],
|
|
7949
|
+
["filesystem.allowWrite", policy.filesystem?.allowWrite]
|
|
7950
|
+
];
|
|
7951
|
+
for (const [field, paths] of pathLists) {
|
|
7952
|
+
for (const [i, path] of (paths ?? []).entries()) {
|
|
7953
|
+
try {
|
|
7954
|
+
new RegExp(globToRegex(path));
|
|
7955
|
+
} catch (err) {
|
|
7956
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
7957
|
+
return `runners.policy.${field}.${i}: not a pattern the sandbox can match (${message})`;
|
|
7958
|
+
}
|
|
7959
|
+
}
|
|
7960
|
+
}
|
|
7961
|
+
return null;
|
|
7962
|
+
}
|
|
7963
|
+
var placeholderName = (ref) => ref.slice(2, -1);
|
|
7964
|
+
function gitAuthVar(index) {
|
|
7965
|
+
return `CABANE_GIT_AUTH_${index}`;
|
|
7966
|
+
}
|
|
7967
|
+
function resolvePolicyCredentials(policy, store) {
|
|
7968
|
+
const missing = /* @__PURE__ */ new Set();
|
|
7969
|
+
const credentials = [];
|
|
7970
|
+
const lookup2 = (ref) => {
|
|
7971
|
+
const name = placeholderName(ref);
|
|
7972
|
+
const value = store.get(name);
|
|
7973
|
+
if (value === void 0) missing.add(name);
|
|
7974
|
+
return value;
|
|
7975
|
+
};
|
|
7976
|
+
for (const [name, entry] of Object.entries(policy.credentials?.env ?? {})) {
|
|
7977
|
+
const value = lookup2(entry.secret);
|
|
7978
|
+
if (value === void 0) continue;
|
|
7979
|
+
credentials.push({ name, value, hosts: entry.hosts });
|
|
7980
|
+
}
|
|
7981
|
+
(policy.credentials?.git ?? []).forEach((entry, i) => {
|
|
7982
|
+
const secret = lookup2(entry.secret);
|
|
7983
|
+
if (secret === void 0) return;
|
|
7984
|
+
const user = entry.username ?? "x-access-token";
|
|
7985
|
+
credentials.push({
|
|
7986
|
+
name: gitAuthVar(i),
|
|
7987
|
+
value: Buffer.from(`${user}:${secret}`).toString("base64"),
|
|
7988
|
+
hosts: [entry.host.replace(/:\d+$/, "")],
|
|
7989
|
+
gitHost: entry.host
|
|
7990
|
+
});
|
|
7991
|
+
});
|
|
7992
|
+
return { credentials, missing: [...missing].sort() };
|
|
7993
|
+
}
|
|
7994
|
+
var HarnessSandboxRefused = class extends Error {
|
|
7995
|
+
};
|
|
7996
|
+
function policyHasCredentials(policy) {
|
|
7997
|
+
return Object.keys(policy.credentials?.env ?? {}).length > 0 || (policy.credentials?.git ?? []).length > 0;
|
|
7998
|
+
}
|
|
7999
|
+
function postureLines(parsed) {
|
|
8000
|
+
if (!parsed.ok) return [`policy invalid \u2014 turns are refused until it's fixed: ${parsed.error}`];
|
|
8001
|
+
const { policy } = parsed;
|
|
8002
|
+
const mode = policy.filesystem?.mode ?? "blocklist";
|
|
8003
|
+
const defaults = policy.hosts?.defaults !== false;
|
|
8004
|
+
const lines2 = [
|
|
8005
|
+
`filesystem: ${mode}${mode === "strict" ? " (home unreadable except the checkout, harness state and allowRead)" : ""}`,
|
|
8006
|
+
`hosts: Cabane + the turn's model provider${defaults ? " + defaults (GitHub, npm, PyPI, crates.io)" : ""}${policy.hosts?.allow?.length ? ` + ${policy.hosts.allow.join(", ")}` : ""}`
|
|
8007
|
+
];
|
|
8008
|
+
if (policy.hosts?.deny?.length) lines2.push(`hosts denied: ${policy.hosts.deny.join(", ")}`);
|
|
8009
|
+
const extraDeny = policy.filesystem?.denyRead ?? [];
|
|
8010
|
+
if (extraDeny.length) lines2.push(`denied reads (extra): ${extraDeny.join(", ")}`);
|
|
8011
|
+
const allowRead = policy.filesystem?.allowRead ?? [];
|
|
8012
|
+
if (allowRead.length) lines2.push(`reads allowed back: ${allowRead.join(", ")}`);
|
|
8013
|
+
const allowWrite = policy.filesystem?.allowWrite ?? [];
|
|
8014
|
+
if (allowWrite.length) lines2.push(`writes allowed (extra): ${allowWrite.join(", ")}`);
|
|
8015
|
+
const env = Object.keys(policy.credentials?.env ?? {});
|
|
8016
|
+
const git = (policy.credentials?.git ?? []).map((g) => `git@${g.host}`);
|
|
8017
|
+
if (env.length || git.length) {
|
|
8018
|
+
lines2.push(`masked credentials: ${[...env, ...git].join(", ")}`);
|
|
8019
|
+
}
|
|
8020
|
+
return lines2;
|
|
8021
|
+
}
|
|
8022
|
+
|
|
7875
8023
|
// src/runner/opencode-lock.ts
|
|
7876
8024
|
function opencodeLockKey(serverUrl) {
|
|
7877
8025
|
return `opencode:${serverUrl}`;
|
|
@@ -8185,12 +8333,12 @@ function createTurnContainment(turnId, deps = {}) {
|
|
|
8185
8333
|
// src/secrets.ts
|
|
8186
8334
|
import { existsSync as existsSync7, readFileSync as readFileSync6 } from "fs";
|
|
8187
8335
|
import { join as join10 } from "path";
|
|
8188
|
-
import { z as
|
|
8336
|
+
import { z as z14 } from "zod";
|
|
8189
8337
|
var PLACEHOLDER_RE = /\$\{([A-Za-z_][A-Za-z0-9_]*)\}/g;
|
|
8190
8338
|
function secretsPath() {
|
|
8191
8339
|
return join10(cabaneDir(), "secrets.json");
|
|
8192
8340
|
}
|
|
8193
|
-
var secretStoreSchema =
|
|
8341
|
+
var secretStoreSchema = z14.record(z14.string(), z14.string());
|
|
8194
8342
|
function loadSecretStore() {
|
|
8195
8343
|
const path = secretsPath();
|
|
8196
8344
|
if (!existsSync7(path)) return makeStore({});
|
|
@@ -8272,44 +8420,55 @@ function resolveMcpSecrets(mcpServers, store) {
|
|
|
8272
8420
|
}
|
|
8273
8421
|
return { servers, missing: [...missing].sort() };
|
|
8274
8422
|
}
|
|
8423
|
+
var ERROR_REASON_MAX = 200;
|
|
8424
|
+
function missingSecretReason(missing) {
|
|
8425
|
+
const prefix = "missing_secret: ";
|
|
8426
|
+
for (let shown = missing.length; shown >= 0; shown--) {
|
|
8427
|
+
const omitted = missing.length - shown;
|
|
8428
|
+
const parts = [...missing.slice(0, shown), ...omitted > 0 ? [`+${omitted}`] : []];
|
|
8429
|
+
const reason = prefix + parts.join(",");
|
|
8430
|
+
if (reason.length <= ERROR_REASON_MAX) return reason;
|
|
8431
|
+
}
|
|
8432
|
+
return `${prefix}+${missing.length}`;
|
|
8433
|
+
}
|
|
8275
8434
|
|
|
8276
8435
|
// src/runner/protocol.ts
|
|
8277
|
-
import { z as
|
|
8436
|
+
import { z as z16 } from "zod";
|
|
8278
8437
|
|
|
8279
8438
|
// src/run-config.ts
|
|
8280
|
-
import { z as
|
|
8281
|
-
var mcpStdioServerSchema =
|
|
8282
|
-
type:
|
|
8283
|
-
command:
|
|
8284
|
-
args:
|
|
8285
|
-
env:
|
|
8439
|
+
import { z as z15 } from "zod";
|
|
8440
|
+
var mcpStdioServerSchema = z15.object({
|
|
8441
|
+
type: z15.literal("stdio").optional(),
|
|
8442
|
+
command: z15.string().min(1),
|
|
8443
|
+
args: z15.array(z15.string()).optional(),
|
|
8444
|
+
env: z15.record(z15.string(), z15.string()).optional()
|
|
8286
8445
|
});
|
|
8287
|
-
var mcpHttpServerSchema =
|
|
8288
|
-
type:
|
|
8289
|
-
url:
|
|
8290
|
-
headers:
|
|
8446
|
+
var mcpHttpServerSchema = z15.object({
|
|
8447
|
+
type: z15.literal("http"),
|
|
8448
|
+
url: z15.string().url(),
|
|
8449
|
+
headers: z15.record(z15.string(), z15.string()).optional()
|
|
8291
8450
|
});
|
|
8292
|
-
var mcpSseServerSchema =
|
|
8293
|
-
type:
|
|
8294
|
-
url:
|
|
8295
|
-
headers:
|
|
8451
|
+
var mcpSseServerSchema = z15.object({
|
|
8452
|
+
type: z15.literal("sse"),
|
|
8453
|
+
url: z15.string().url(),
|
|
8454
|
+
headers: z15.record(z15.string(), z15.string()).optional()
|
|
8296
8455
|
});
|
|
8297
|
-
var mcpServerDefSchema =
|
|
8298
|
-
var thinkingConfigSchema =
|
|
8299
|
-
|
|
8300
|
-
|
|
8301
|
-
|
|
8456
|
+
var mcpServerDefSchema = z15.union([mcpHttpServerSchema, mcpSseServerSchema, mcpStdioServerSchema]);
|
|
8457
|
+
var thinkingConfigSchema = z15.discriminatedUnion("type", [
|
|
8458
|
+
z15.object({ type: z15.literal("adaptive") }),
|
|
8459
|
+
z15.object({ type: z15.literal("enabled"), budgetTokens: z15.number().int().positive().optional() }),
|
|
8460
|
+
z15.object({ type: z15.literal("disabled") })
|
|
8302
8461
|
]);
|
|
8303
|
-
var effortSchema =
|
|
8304
|
-
var runConfigSchema =
|
|
8462
|
+
var effortSchema = z15.enum(["low", "medium", "high", "xhigh", "max"]);
|
|
8463
|
+
var runConfigSchema = z15.object({
|
|
8305
8464
|
// CT788: the host-access binary replaced the `assistant`/`coding`/`custom` mode
|
|
8306
8465
|
// trio + its custom tool lists — `true` grants the host filesystem/shell, absent
|
|
8307
8466
|
// is the locked surface. Kept in lockstep with `@cabane/shared`'s
|
|
8308
8467
|
// `agentRunConfigSchema` (independent zod, forward-compatible: unknown keys are
|
|
8309
8468
|
// stripped, so an older companion riding a newer server never rejects the config).
|
|
8310
|
-
hostAccess:
|
|
8311
|
-
mcpServers:
|
|
8312
|
-
model:
|
|
8469
|
+
hostAccess: z15.boolean().optional(),
|
|
8470
|
+
mcpServers: z15.record(z15.string(), mcpServerDefSchema).optional(),
|
|
8471
|
+
model: z15.string().min(1).optional(),
|
|
8313
8472
|
thinking: thinkingConfigSchema.optional(),
|
|
8314
8473
|
effort: effortSchema.optional()
|
|
8315
8474
|
});
|
|
@@ -8337,64 +8496,75 @@ var RUNNER_ENV = {
|
|
|
8337
8496
|
// Everything else the turn is constructed from, as JSON: see `RunnerSpec`.
|
|
8338
8497
|
spec: "CABANE_RUNNER_SPEC"
|
|
8339
8498
|
};
|
|
8340
|
-
var runtimeHealthSchema =
|
|
8341
|
-
quotaState:
|
|
8342
|
-
limitedUntil:
|
|
8343
|
-
lastError:
|
|
8499
|
+
var runtimeHealthSchema = z16.object({
|
|
8500
|
+
quotaState: z16.enum(["ok", "limited", "auth_expired", "unknown"]),
|
|
8501
|
+
limitedUntil: z16.string().nullable(),
|
|
8502
|
+
lastError: z16.string().nullable()
|
|
8344
8503
|
});
|
|
8345
|
-
var runnerSpecSchema =
|
|
8346
|
-
deviceId:
|
|
8504
|
+
var runnerSpecSchema = z16.object({
|
|
8505
|
+
deviceId: z16.string().optional(),
|
|
8347
8506
|
// The launcher's boot id: the one its heartbeat carries, so a runner's lease
|
|
8348
8507
|
// matches the device that holds it.
|
|
8349
|
-
bootId:
|
|
8350
|
-
workspaceId:
|
|
8351
|
-
workspaceSlug:
|
|
8352
|
-
agentId:
|
|
8353
|
-
agentUsername:
|
|
8354
|
-
agentName:
|
|
8508
|
+
bootId: z16.string(),
|
|
8509
|
+
workspaceId: z16.string(),
|
|
8510
|
+
workspaceSlug: z16.string(),
|
|
8511
|
+
agentId: z16.string(),
|
|
8512
|
+
agentUsername: z16.string(),
|
|
8513
|
+
agentName: z16.string(),
|
|
8355
8514
|
local: localAgentConfigSchema,
|
|
8356
8515
|
runConfig: runConfigSchema,
|
|
8357
|
-
opencodeServerUrl:
|
|
8358
|
-
claudeCodeAvailable:
|
|
8359
|
-
codexAvailable:
|
|
8360
|
-
idleTimeoutMs:
|
|
8361
|
-
totalTimeoutMs:
|
|
8362
|
-
leaseRenewalMs:
|
|
8363
|
-
preparingRowDelayMs:
|
|
8516
|
+
opencodeServerUrl: z16.string().optional(),
|
|
8517
|
+
claudeCodeAvailable: z16.boolean(),
|
|
8518
|
+
codexAvailable: z16.boolean(),
|
|
8519
|
+
idleTimeoutMs: z16.number().int().positive().optional(),
|
|
8520
|
+
totalTimeoutMs: z16.number().int().positive().optional(),
|
|
8521
|
+
leaseRenewalMs: z16.number().int().positive().optional(),
|
|
8522
|
+
preparingRowDelayMs: z16.number().int().nonnegative().optional(),
|
|
8364
8523
|
// Whether the turn writes a transcript. The directory is the runner's own
|
|
8365
8524
|
// (`transcriptsDir()`), so `CABANE_RUNNER_STATE_DIR` moves it with the outbox.
|
|
8366
|
-
transcripts:
|
|
8525
|
+
transcripts: z16.boolean(),
|
|
8367
8526
|
// Whether the harness starts under the OS sandbox. The launcher sends 'os'
|
|
8368
8527
|
// only when this host can make one.
|
|
8369
|
-
sandbox:
|
|
8370
|
-
|
|
8528
|
+
sandbox: z16.enum(["os", "none"]),
|
|
8529
|
+
// CT1684: the device's sandbox policy, validated by the launcher. Absent
|
|
8530
|
+
// when the sandbox is off.
|
|
8531
|
+
sandboxPolicy: sandboxPolicySchema.optional(),
|
|
8532
|
+
logLevel: z16.string(),
|
|
8371
8533
|
// The launcher's connector-health observations, by runtime, as of the start.
|
|
8372
|
-
connectorHealth:
|
|
8534
|
+
connectorHealth: z16.record(z16.string(), runtimeHealthSchema),
|
|
8373
8535
|
// True when this (conversation, agent) pair was already told its session
|
|
8374
8536
|
// could not be saved: the notice is once per pair, not once per process.
|
|
8375
|
-
sessionWriteNoticeClaimed:
|
|
8537
|
+
sessionWriteNoticeClaimed: z16.boolean(),
|
|
8376
8538
|
// CT1680: the runner's identity, minted with its credential. The start
|
|
8377
8539
|
// PATCH carries it so the lease knows which runner holds the span.
|
|
8378
|
-
runnerId:
|
|
8540
|
+
runnerId: z16.string(),
|
|
8379
8541
|
// CT1681: the launcher keeps the runtime's session files beside the attempt
|
|
8380
8542
|
// and restores them into this runner's home, so the runner names the attempt
|
|
8381
8543
|
// each context was composed for and waits for the launcher to say it's in
|
|
8382
8544
|
// place. Absent: nothing to wait for (a runner on the launcher's own disk).
|
|
8383
|
-
attemptState:
|
|
8545
|
+
attemptState: z16.boolean().optional(),
|
|
8384
8546
|
// CT1681: MCP `${NAME}` secrets come from the launcher, by name, instead of
|
|
8385
8547
|
// this machine's secrets.json. Absent: the runner reads the device's file.
|
|
8386
|
-
secretsFromLauncher:
|
|
8548
|
+
secretsFromLauncher: z16.boolean().optional()
|
|
8387
8549
|
});
|
|
8388
|
-
var dispatchSchema =
|
|
8389
|
-
type:
|
|
8390
|
-
agentId:
|
|
8391
|
-
deviceId:
|
|
8392
|
-
conversationId:
|
|
8393
|
-
messageId:
|
|
8394
|
-
episode:
|
|
8550
|
+
var dispatchSchema = z16.object({
|
|
8551
|
+
type: z16.literal("device:dispatch_requested"),
|
|
8552
|
+
agentId: z16.string(),
|
|
8553
|
+
deviceId: z16.string(),
|
|
8554
|
+
conversationId: z16.string(),
|
|
8555
|
+
messageId: z16.string(),
|
|
8556
|
+
episode: z16.number().int().optional()
|
|
8395
8557
|
});
|
|
8396
|
-
var credentialsSchema =
|
|
8397
|
-
credential:
|
|
8558
|
+
var credentialsSchema = z16.object({
|
|
8559
|
+
credential: z16.string().startsWith("cabrn_"),
|
|
8560
|
+
sandboxCredentials: z16.array(
|
|
8561
|
+
z16.object({
|
|
8562
|
+
name: z16.string(),
|
|
8563
|
+
value: z16.string(),
|
|
8564
|
+
hosts: z16.array(z16.string()),
|
|
8565
|
+
gitHost: z16.string().optional()
|
|
8566
|
+
})
|
|
8567
|
+
).optional()
|
|
8398
8568
|
});
|
|
8399
8569
|
function runnerEnv(input) {
|
|
8400
8570
|
return {
|
|
@@ -8406,7 +8576,10 @@ function runnerEnv(input) {
|
|
|
8406
8576
|
};
|
|
8407
8577
|
}
|
|
8408
8578
|
function runnerStdin(input) {
|
|
8409
|
-
return `${JSON.stringify({
|
|
8579
|
+
return `${JSON.stringify({
|
|
8580
|
+
credential: input.credential,
|
|
8581
|
+
...input.sandboxCredentials?.length ? { sandboxCredentials: input.sandboxCredentials } : {}
|
|
8582
|
+
})}
|
|
8410
8583
|
`;
|
|
8411
8584
|
}
|
|
8412
8585
|
function encodeLauncherMessage(message) {
|
|
@@ -8471,6 +8644,7 @@ function runnerSpec(opts, launch, duties, runnerId) {
|
|
|
8471
8644
|
sandbox: launch.sandbox ?? "none",
|
|
8472
8645
|
...launch.provider.bindAttempt ? { attemptState: true } : {},
|
|
8473
8646
|
...launch.provider.inheritsEnvironment === false ? { secretsFromLauncher: true } : {},
|
|
8647
|
+
...launch.sandbox === "os" && launch.policy?.ok ? { sandboxPolicy: launch.policy.policy } : {},
|
|
8474
8648
|
logLevel: opts.log.level,
|
|
8475
8649
|
connectorHealth: healthSnapshot(launch.connectorHealth),
|
|
8476
8650
|
sessionWriteNoticeClaimed: duties.sessionWriteNoticeClaimed()
|
|
@@ -8577,6 +8751,32 @@ async function settleUnstarted(opts, payload, turnId, errorReason, log) {
|
|
|
8577
8751
|
);
|
|
8578
8752
|
}
|
|
8579
8753
|
}
|
|
8754
|
+
function sandboxInputs(launch, log) {
|
|
8755
|
+
const policy = launch.policy ?? { ok: true, policy: {} };
|
|
8756
|
+
if (launch.sandbox !== "os") {
|
|
8757
|
+
if (policy.ok && policyHasCredentials(policy.policy)) {
|
|
8758
|
+
log.debug(
|
|
8759
|
+
"runners: runners.policy names credentials, but turns here run without the OS sandbox; none are given to the harness"
|
|
8760
|
+
);
|
|
8761
|
+
}
|
|
8762
|
+
return { ok: true, credentials: [] };
|
|
8763
|
+
}
|
|
8764
|
+
if (!policy.ok) {
|
|
8765
|
+
log.warn({ error: policy.error }, "runners: the sandbox policy is invalid; refusing the turn");
|
|
8766
|
+
return { ok: false, reason: `setup_failed: ${policy.error}`.slice(0, 200) };
|
|
8767
|
+
}
|
|
8768
|
+
if (!policyHasCredentials(policy.policy)) return { ok: true, credentials: [] };
|
|
8769
|
+
const store = (launch.secretStore ?? defaultSecretStore)(log);
|
|
8770
|
+
const { credentials, missing } = resolvePolicyCredentials(policy.policy, store);
|
|
8771
|
+
if (missing.length) {
|
|
8772
|
+
log.debug(
|
|
8773
|
+
{ missing },
|
|
8774
|
+
"runners: the sandbox policy names secrets this device does not declare"
|
|
8775
|
+
);
|
|
8776
|
+
return { ok: false, reason: missingSecretReason(missing) };
|
|
8777
|
+
}
|
|
8778
|
+
return { ok: true, credentials };
|
|
8779
|
+
}
|
|
8580
8780
|
async function runTurnInRunner(opts, launch, duties, payload, handleOpts) {
|
|
8581
8781
|
const startedAt = Date.now();
|
|
8582
8782
|
const { turnId } = handleOpts;
|
|
@@ -8600,7 +8800,8 @@ async function runTurnInRunner(opts, launch, duties, payload, handleOpts) {
|
|
|
8600
8800
|
agentId: payload.agentId,
|
|
8601
8801
|
turnId
|
|
8602
8802
|
};
|
|
8603
|
-
const
|
|
8803
|
+
const sandboxed = sandboxInputs(launch, log);
|
|
8804
|
+
const minted = sandboxed.ok ? await mintForRunner(opts, payload, handleOpts, controller.signal, log) : { ok: false, reason: sandboxed.reason, errorReason: sandboxed.reason };
|
|
8604
8805
|
if (!minted.ok) {
|
|
8605
8806
|
controller.signal.removeEventListener("abort", forwardStop);
|
|
8606
8807
|
duties.releaseAbort(turnId, controller);
|
|
@@ -8653,6 +8854,7 @@ async function runTurnInRunner(opts, launch, duties, payload, handleOpts) {
|
|
|
8653
8854
|
const input = {
|
|
8654
8855
|
baseUrl: launch.runnerBaseUrl ?? opts.baseUrl,
|
|
8655
8856
|
credential: minted.credential,
|
|
8857
|
+
sandboxCredentials: sandboxed.ok ? sandboxed.credentials : [],
|
|
8656
8858
|
dispatch: payload,
|
|
8657
8859
|
turnId,
|
|
8658
8860
|
resumed: handleOpts.resumed,
|
|
@@ -8837,7 +9039,7 @@ import { existsSync as existsSync9 } from "fs";
|
|
|
8837
9039
|
|
|
8838
9040
|
// src/turn-control-tools.ts
|
|
8839
9041
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
8840
|
-
import { z as
|
|
9042
|
+
import { z as z17 } from "zod";
|
|
8841
9043
|
var COMPANION_LOCAL_MCP_SERVER = "cabane_companion";
|
|
8842
9044
|
var COMPANION_LOCAL_TOOL_GLOB = `mcp__${COMPANION_LOCAL_MCP_SERVER}__*`;
|
|
8843
9045
|
var SKIP_TURN_TOOL = "skip_turn";
|
|
@@ -8900,7 +9102,7 @@ function createTurnControlMcpServer(acts, skipState) {
|
|
|
8900
9102
|
SKIP_TURN_TOOL,
|
|
8901
9103
|
TURN_CONTROL_TOOL_DESCRIPTIONS.skip_turn,
|
|
8902
9104
|
{
|
|
8903
|
-
reason:
|
|
9105
|
+
reason: z17.string().min(1).max(MAX_SKIP_REASON_BYTES).describe(TURN_CONTROL_FIELD_DESCRIPTIONS.skip_turn.reason)
|
|
8904
9106
|
},
|
|
8905
9107
|
async (args) => {
|
|
8906
9108
|
skipState.skipped = true;
|
|
@@ -8916,42 +9118,42 @@ function createTurnControlMcpServer(acts, skipState) {
|
|
|
8916
9118
|
ASK_TOOL,
|
|
8917
9119
|
TURN_CONTROL_TOOL_DESCRIPTIONS.ask,
|
|
8918
9120
|
{
|
|
8919
|
-
targetUserId:
|
|
8920
|
-
question:
|
|
8921
|
-
headline:
|
|
8922
|
-
options:
|
|
8923
|
-
|
|
8924
|
-
|
|
8925
|
-
|
|
8926
|
-
label:
|
|
8927
|
-
move:
|
|
8928
|
-
task:
|
|
8929
|
-
status:
|
|
9121
|
+
targetUserId: z17.string().uuid().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask.targetUserId),
|
|
9122
|
+
question: z17.string().min(1).max(MAX_ASK_QUESTION_BYTES).optional().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask.question),
|
|
9123
|
+
headline: z17.string().min(1).max(MAX_ASK_HEADLINE_BYTES).optional().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask.headline),
|
|
9124
|
+
options: z17.array(
|
|
9125
|
+
z17.union([
|
|
9126
|
+
z17.string().min(1).max(MAX_ASK_OPTION_BYTES),
|
|
9127
|
+
z17.object({
|
|
9128
|
+
label: z17.string().min(1).max(MAX_ASK_OPTION_BYTES).describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask["options.label"]),
|
|
9129
|
+
move: z17.object({
|
|
9130
|
+
task: z17.string().min(1).max(200).describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask["options.move.task"]),
|
|
9131
|
+
status: z17.enum(TASK_STAGES).describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask["options.move.status"])
|
|
8930
9132
|
}).describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask["options.move"])
|
|
8931
9133
|
})
|
|
8932
9134
|
])
|
|
8933
9135
|
).min(MIN_ASK_OPTIONS).max(MAX_ASK_OPTIONS).optional().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask.options),
|
|
8934
|
-
questions:
|
|
8935
|
-
|
|
8936
|
-
headline:
|
|
8937
|
-
body:
|
|
8938
|
-
options:
|
|
8939
|
-
|
|
8940
|
-
|
|
8941
|
-
|
|
8942
|
-
label:
|
|
8943
|
-
move:
|
|
8944
|
-
task:
|
|
8945
|
-
status:
|
|
9136
|
+
questions: z17.array(
|
|
9137
|
+
z17.object({
|
|
9138
|
+
headline: z17.string().min(1).max(MAX_ASK_HEADLINE_BYTES).describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask["questions.headline"]),
|
|
9139
|
+
body: z17.string().min(1).max(MAX_ASK_QUESTION_BYTES).optional().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask["questions.body"]),
|
|
9140
|
+
options: z17.array(
|
|
9141
|
+
z17.union([
|
|
9142
|
+
z17.string().min(1).max(MAX_ASK_OPTION_BYTES),
|
|
9143
|
+
z17.object({
|
|
9144
|
+
label: z17.string().min(1).max(MAX_ASK_OPTION_BYTES).describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask["questions.options.label"]),
|
|
9145
|
+
move: z17.object({
|
|
9146
|
+
task: z17.string().min(1).max(200).describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask["questions.options.move.task"]),
|
|
9147
|
+
status: z17.enum(TASK_STAGES).describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask["questions.options.move.status"])
|
|
8946
9148
|
}).describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask["questions.options.move"])
|
|
8947
9149
|
})
|
|
8948
9150
|
])
|
|
8949
9151
|
).min(MIN_ASK_OPTIONS).max(MAX_ASK_OPTIONS).optional().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask["questions.options"])
|
|
8950
9152
|
})
|
|
8951
9153
|
).min(1).max(MAX_ASK_ITEMS).optional().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask.questions),
|
|
8952
|
-
replaces:
|
|
8953
|
-
askId:
|
|
8954
|
-
reason:
|
|
9154
|
+
replaces: z17.object({
|
|
9155
|
+
askId: z17.string().uuid().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask["replaces.askId"]),
|
|
9156
|
+
reason: z17.string().min(1).max(MAX_WITHDRAW_REASON_BYTES).describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask["replaces.reason"])
|
|
8955
9157
|
}).optional().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask.replaces)
|
|
8956
9158
|
},
|
|
8957
9159
|
async (args) => {
|
|
@@ -8992,9 +9194,9 @@ function createTurnControlMcpServer(acts, skipState) {
|
|
|
8992
9194
|
WAKE_ME_TOOL,
|
|
8993
9195
|
TURN_CONTROL_TOOL_DESCRIPTIONS.wake_me,
|
|
8994
9196
|
{
|
|
8995
|
-
afterSeconds:
|
|
8996
|
-
at:
|
|
8997
|
-
note:
|
|
9197
|
+
afterSeconds: z17.number().int().positive().optional().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.wake_me.afterSeconds),
|
|
9198
|
+
at: z17.string().datetime({ offset: true }).optional().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.wake_me.at),
|
|
9199
|
+
note: z17.string().min(1).max(MAX_WAKE_NOTE_BYTES).describe(TURN_CONTROL_FIELD_DESCRIPTIONS.wake_me.note)
|
|
8998
9200
|
},
|
|
8999
9201
|
async (args) => {
|
|
9000
9202
|
const hasAfter = args.afterSeconds !== void 0;
|
|
@@ -9028,8 +9230,8 @@ function createTurnControlMcpServer(acts, skipState) {
|
|
|
9028
9230
|
WITHDRAW_ASK_TOOL,
|
|
9029
9231
|
TURN_CONTROL_TOOL_DESCRIPTIONS.withdraw_ask,
|
|
9030
9232
|
{
|
|
9031
|
-
askId:
|
|
9032
|
-
reason:
|
|
9233
|
+
askId: z17.string().uuid().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.withdraw_ask.askId),
|
|
9234
|
+
reason: z17.string().min(1).max(MAX_WITHDRAW_REASON_BYTES).describe(TURN_CONTROL_FIELD_DESCRIPTIONS.withdraw_ask.reason)
|
|
9033
9235
|
},
|
|
9034
9236
|
async (args) => performAct(acts, {
|
|
9035
9237
|
kind: "withdraw_ask",
|
|
@@ -9548,17 +9750,6 @@ async function absorbBundledLoss(ctx, aborted, outcome) {
|
|
|
9548
9750
|
|
|
9549
9751
|
// src/turn-execution.ts
|
|
9550
9752
|
var PREPARING_TOOL_NAME = "preparing";
|
|
9551
|
-
var ERROR_REASON_MAX = 200;
|
|
9552
|
-
function missingSecretReason(missing) {
|
|
9553
|
-
const prefix = "missing_secret: ";
|
|
9554
|
-
for (let shown = missing.length; shown >= 0; shown--) {
|
|
9555
|
-
const omitted = missing.length - shown;
|
|
9556
|
-
const parts = [...missing.slice(0, shown), ...omitted > 0 ? [`+${omitted}`] : []];
|
|
9557
|
-
const reason = prefix + parts.join(",");
|
|
9558
|
-
if (reason.length <= ERROR_REASON_MAX) return reason;
|
|
9559
|
-
}
|
|
9560
|
-
return `${prefix}+${missing.length}`;
|
|
9561
|
-
}
|
|
9562
9753
|
var RUNTIME_UNAVAILABLE_PREFIX = "**This agent's runtime isn't available on this companion.** The model this agent uses needs a runtime this device isn't running, so I can't run this turn here. Details:";
|
|
9563
9754
|
var NO_TURN_TOKEN_REASON = "no_turn_token: the server issued no turn credential for this turn";
|
|
9564
9755
|
var UNEXPECTED_ROLE_REASON = `refused the wake trigger (role "system" isn't dispatchable on this companion) \u2014 the companion is likely running outdated code; refresh it, then re-address the agent`;
|
|
@@ -10147,7 +10338,18 @@ var TurnExecution = class {
|
|
|
10147
10338
|
const { payload, workspaceId, turnId, turnLog } = this;
|
|
10148
10339
|
await guardBundledBinary(this.integrityContext(), (r, e) => this.concluded(r, e), this.outcome);
|
|
10149
10340
|
const onWarn = (msg, meta) => turnLog.debug(meta ?? {}, msg);
|
|
10150
|
-
|
|
10341
|
+
let sandbox = null;
|
|
10342
|
+
try {
|
|
10343
|
+
sandbox = this.opts.harnessSandbox ? await this.opts.harnessSandbox({
|
|
10344
|
+
cwd: this.effectiveCwd,
|
|
10345
|
+
runtime: this.turnContext.runtime,
|
|
10346
|
+
log: turnLog
|
|
10347
|
+
}) : null;
|
|
10348
|
+
} catch (err) {
|
|
10349
|
+
if (!(err instanceof HarnessSandboxRefused)) throw err;
|
|
10350
|
+
const reason = `setup_failed: ${err.message}`.slice(0, 200);
|
|
10351
|
+
throw this.concluded(reason, reason);
|
|
10352
|
+
}
|
|
10151
10353
|
const adapters = [];
|
|
10152
10354
|
if (this.opts.claudeCodeAvailable?.() ?? true) {
|
|
10153
10355
|
this.containment = createTurnContainment(this.turnId, {
|
|
@@ -11325,6 +11527,15 @@ var UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
|
11325
11527
|
function containerName(attempt) {
|
|
11326
11528
|
return `cabane-runner-${attempt.conversationId}-${attempt.agentId}`;
|
|
11327
11529
|
}
|
|
11530
|
+
var networkName = containerName;
|
|
11531
|
+
function networkCreateError(stderr) {
|
|
11532
|
+
const said = stderr.trim();
|
|
11533
|
+
if (/address pool|fully subnetted/i.test(said))
|
|
11534
|
+
return new Error(
|
|
11535
|
+
`docker: couldn't create this conversation's runner network: the engine has no address pool left for another network. Each conversation and agent with a runner container holds one; remove kept runner containers, or widen \`default-address-pools\` in the daemon.json. Docker said: ${said}`
|
|
11536
|
+
);
|
|
11537
|
+
return new Error(`docker: creating the runner network failed: ${said || "no reason given"}`);
|
|
11538
|
+
}
|
|
11328
11539
|
function sessionArchivePath(deviceDir, attemptId) {
|
|
11329
11540
|
if (!UUID_RE.test(attemptId)) throw new Error(`docker: not an attempt id: ${attemptId}`);
|
|
11330
11541
|
return join17(deviceDir, "attempts", attemptId, "session.tar");
|
|
@@ -11401,7 +11612,7 @@ function createDockerProvider(opts) {
|
|
|
11401
11612
|
const killGraceMs = opts.killGraceMs ?? DEFAULT_KILL_GRACE_MS2;
|
|
11402
11613
|
const sessionCap = opts.sessionCapBytes ?? DEFAULT_SESSION_CAP_BYTES;
|
|
11403
11614
|
const collectCap = opts.collectCapBytes ?? COLLECT_CAP_BYTES;
|
|
11404
|
-
const
|
|
11615
|
+
const sharedNetwork = `cabane-runners-${opts.deviceKey}`;
|
|
11405
11616
|
const companionLabel = createHash4("sha256").update(opts.companion.key).digest("hex").slice(0, 16);
|
|
11406
11617
|
const proxy = `cabane-egress-${opts.deviceKey}`;
|
|
11407
11618
|
const held = /* @__PURE__ */ new Map();
|
|
@@ -11434,32 +11645,14 @@ function createDockerProvider(opts) {
|
|
|
11434
11645
|
}
|
|
11435
11646
|
throw new Error("docker: the egress proxy never started listening");
|
|
11436
11647
|
};
|
|
11437
|
-
let
|
|
11648
|
+
let proxyReady = null;
|
|
11438
11649
|
const proxyKey = async () => {
|
|
11439
11650
|
const id = (await docker.run(["image", "inspect", "-f", "{{.Id}}", opts.image])).stdout.trim();
|
|
11440
11651
|
return createHash4("sha256").update(JSON.stringify([id || opts.image, opts.companion.key, [...opts.egressAllow].sort()])).digest("hex").slice(0, 16);
|
|
11441
11652
|
};
|
|
11442
|
-
const
|
|
11443
|
-
|
|
11653
|
+
const ensureProxy = () => {
|
|
11654
|
+
proxyReady ??= (async () => {
|
|
11444
11655
|
const allowKey = await proxyKey();
|
|
11445
|
-
if ((await docker.run(["network", "inspect", network])).code !== 0) {
|
|
11446
|
-
await must(
|
|
11447
|
-
[
|
|
11448
|
-
"network",
|
|
11449
|
-
"create",
|
|
11450
|
-
// No route off the host, and no address of the host's on the
|
|
11451
|
-
// bridge: the host's own ports are out of reach too. The proxy is
|
|
11452
|
-
// the only other thing on the network.
|
|
11453
|
-
"--internal",
|
|
11454
|
-
"-o",
|
|
11455
|
-
"com.docker.network.bridge.inhibit_ipv4=true",
|
|
11456
|
-
"--label",
|
|
11457
|
-
`cabane.device=${opts.deviceKey}`,
|
|
11458
|
-
network
|
|
11459
|
-
],
|
|
11460
|
-
"creating the runners network"
|
|
11461
|
-
);
|
|
11462
|
-
}
|
|
11463
11656
|
const seen = await docker.run([
|
|
11464
11657
|
"inspect",
|
|
11465
11658
|
"-f",
|
|
@@ -11468,6 +11661,7 @@ function createDockerProvider(opts) {
|
|
|
11468
11661
|
]);
|
|
11469
11662
|
const [key, running] = seen.stdout.trim().split(" ");
|
|
11470
11663
|
if (seen.code === 0 && key === allowKey) {
|
|
11664
|
+
await retireSharedNetwork();
|
|
11471
11665
|
if (running !== "true") {
|
|
11472
11666
|
await must(["start", proxy], "starting the egress proxy");
|
|
11473
11667
|
await proxyListening();
|
|
@@ -11502,17 +11696,61 @@ function createDockerProvider(opts) {
|
|
|
11502
11696
|
],
|
|
11503
11697
|
"starting the egress proxy"
|
|
11504
11698
|
);
|
|
11505
|
-
await
|
|
11506
|
-
["network", "connect", "--alias", PROXY_ALIAS, network, proxy],
|
|
11507
|
-
"connecting the proxy"
|
|
11508
|
-
);
|
|
11699
|
+
await retireSharedNetwork();
|
|
11509
11700
|
await proxyListening();
|
|
11510
|
-
log.info({
|
|
11701
|
+
log.info({ proxy, allow: opts.egressAllow }, "runners: egress proxy ready");
|
|
11511
11702
|
})().catch((err) => {
|
|
11512
|
-
|
|
11703
|
+
proxyReady = null;
|
|
11513
11704
|
throw err;
|
|
11514
11705
|
});
|
|
11515
|
-
return
|
|
11706
|
+
return proxyReady;
|
|
11707
|
+
};
|
|
11708
|
+
const retireSharedNetwork = async () => {
|
|
11709
|
+
if ((await docker.run(["network", "inspect", sharedNetwork])).code !== 0) return;
|
|
11710
|
+
await docker.run(["network", "disconnect", "-f", sharedNetwork, proxy]);
|
|
11711
|
+
const on = await docker.run(["ps", "-aq", "--filter", `network=${sharedNetwork}`]);
|
|
11712
|
+
if (on.code === 0 && !on.stdout.trim()) await docker.run(["network", "rm", sharedNetwork]);
|
|
11713
|
+
};
|
|
11714
|
+
const networksOf = async (container) => (await docker.run([
|
|
11715
|
+
"inspect",
|
|
11716
|
+
"-f",
|
|
11717
|
+
"{{range $k, $_ := .NetworkSettings.Networks}}{{$k}} {{end}}",
|
|
11718
|
+
container
|
|
11719
|
+
])).stdout.split(/\s+/).filter(Boolean);
|
|
11720
|
+
const ensurePairNetwork = async (net, attempt) => {
|
|
11721
|
+
if ((await docker.run(["network", "inspect", net])).code !== 0) {
|
|
11722
|
+
const made = await docker.run([
|
|
11723
|
+
"network",
|
|
11724
|
+
"create",
|
|
11725
|
+
// No route off the host, and no address of the host's on the bridge:
|
|
11726
|
+
// the host's own ports are out of reach too. The proxy is the only
|
|
11727
|
+
// other thing on the network.
|
|
11728
|
+
"--internal",
|
|
11729
|
+
"-o",
|
|
11730
|
+
"com.docker.network.bridge.inhibit_ipv4=true",
|
|
11731
|
+
"--label",
|
|
11732
|
+
`cabane.device=${opts.deviceKey}`,
|
|
11733
|
+
"--label",
|
|
11734
|
+
`cabane.conversation=${attempt.conversationId}`,
|
|
11735
|
+
"--label",
|
|
11736
|
+
`cabane.agent=${attempt.agentId}`,
|
|
11737
|
+
net
|
|
11738
|
+
]);
|
|
11739
|
+
if (made.code !== 0) throw networkCreateError(made.stderr);
|
|
11740
|
+
}
|
|
11741
|
+
if (!(await networksOf(proxy)).includes(net))
|
|
11742
|
+
await must(
|
|
11743
|
+
["network", "connect", "--alias", PROXY_ALIAS, net, proxy],
|
|
11744
|
+
"connecting the proxy"
|
|
11745
|
+
);
|
|
11746
|
+
};
|
|
11747
|
+
const settleNetworks = async (name, net) => {
|
|
11748
|
+
const on = await networksOf(name);
|
|
11749
|
+
if (!on.includes(net)) await must(["network", "connect", net, name], "moving the container");
|
|
11750
|
+
const others = on.filter((n) => n !== net);
|
|
11751
|
+
for (const other of others)
|
|
11752
|
+
await must(["network", "disconnect", "-f", other, name], "moving the container");
|
|
11753
|
+
if (others.includes(sharedNetwork)) await retireSharedNetwork();
|
|
11516
11754
|
};
|
|
11517
11755
|
const socketRoot = opts.socketRoot ?? join17(
|
|
11518
11756
|
process.env.XDG_RUNTIME_DIR && existsSync13(process.env.XDG_RUNTIME_DIR) ? process.env.XDG_RUNTIME_DIR : tmpdir2(),
|
|
@@ -11544,7 +11782,7 @@ function createDockerProvider(opts) {
|
|
|
11544
11782
|
"--label",
|
|
11545
11783
|
`cabane.companion=${companionLabel}`,
|
|
11546
11784
|
"--network",
|
|
11547
|
-
|
|
11785
|
+
networkName(attempt),
|
|
11548
11786
|
"--mount",
|
|
11549
11787
|
`type=volume,src=${name}-home,dst=${CONTAINER_HOME}`,
|
|
11550
11788
|
"--mount",
|
|
@@ -11570,6 +11808,17 @@ function createDockerProvider(opts) {
|
|
|
11570
11808
|
await docker.run(["rm", "-f", "-v", name]);
|
|
11571
11809
|
await docker.run(["volume", "rm", "-f", `${name}-home`, `${name}-work`]);
|
|
11572
11810
|
rmSync9(socketDir(name), { recursive: true, force: true });
|
|
11811
|
+
await dropNetwork(name);
|
|
11812
|
+
};
|
|
11813
|
+
const dropNetwork = async (name) => {
|
|
11814
|
+
if ((await docker.run(["network", "inspect", name])).code !== 0) return;
|
|
11815
|
+
await docker.run(["network", "disconnect", "-f", name, proxy]);
|
|
11816
|
+
const gone = await docker.run(["network", "rm", name]);
|
|
11817
|
+
if (gone.code !== 0)
|
|
11818
|
+
log.warn(
|
|
11819
|
+
{ network: name, err: gone.stderr.trim() },
|
|
11820
|
+
"runners: couldn't remove a pair's network"
|
|
11821
|
+
);
|
|
11573
11822
|
};
|
|
11574
11823
|
const containerEnv = () => {
|
|
11575
11824
|
const proxyUrl = `http://${PROXY_ALIAS}:${PROXY_PORT}`;
|
|
@@ -11736,7 +11985,7 @@ function createDockerProvider(opts) {
|
|
|
11736
11985
|
});
|
|
11737
11986
|
await before;
|
|
11738
11987
|
try {
|
|
11739
|
-
await
|
|
11988
|
+
await ensureProxy();
|
|
11740
11989
|
mkdirSync10(socketRoot, { recursive: true, mode: 448 });
|
|
11741
11990
|
mkdirSync10(socketDir(name), { recursive: true, mode: 457 });
|
|
11742
11991
|
let seen = await docker.run([
|
|
@@ -11766,9 +12015,12 @@ function createDockerProvider(opts) {
|
|
|
11766
12015
|
);
|
|
11767
12016
|
seen = { code: 1, stdout: "", stderr: "" };
|
|
11768
12017
|
}
|
|
12018
|
+
await ensurePairNetwork(networkName(attempt), attempt);
|
|
11769
12019
|
if (seen.code !== 0) {
|
|
11770
12020
|
await create(name, attempt);
|
|
11771
12021
|
log.debug({ container: name }, "runners: container created");
|
|
12022
|
+
} else {
|
|
12023
|
+
await settleNetworks(name, networkName(attempt));
|
|
11772
12024
|
}
|
|
11773
12025
|
if (seen.stdout.trim().split(" ")[0] !== "true")
|
|
11774
12026
|
await must(["start", name], "starting the container");
|
|
@@ -11782,7 +12034,8 @@ function createDockerProvider(opts) {
|
|
|
11782
12034
|
return { provider: "docker", id: name };
|
|
11783
12035
|
} catch (err) {
|
|
11784
12036
|
const keep = opts.keepContainers || readPending(name).length > 0;
|
|
11785
|
-
|
|
12037
|
+
const kept = keep && (await docker.run(["inspect", "-f", "{{.Id}}", name])).code === 0;
|
|
12038
|
+
await (kept ? docker.run(["stop", "-t", "1", name]) : keep ? dropNetwork(networkName(attempt)) : destroy(name)).catch(() => {
|
|
11786
12039
|
});
|
|
11787
12040
|
release();
|
|
11788
12041
|
throw err;
|
|
@@ -12171,10 +12424,20 @@ var DeviceLocks = class {
|
|
|
12171
12424
|
|
|
12172
12425
|
// src/runner/harness-sandbox.ts
|
|
12173
12426
|
import { lookup } from "dns/promises";
|
|
12174
|
-
import {
|
|
12427
|
+
import {
|
|
12428
|
+
chmodSync as chmodSync6,
|
|
12429
|
+
mkdirSync as mkdirSync11,
|
|
12430
|
+
mkdtempSync,
|
|
12431
|
+
readFileSync as readFileSync10,
|
|
12432
|
+
realpathSync as realpathSync3,
|
|
12433
|
+
rmSync as rmSync10,
|
|
12434
|
+
writeFileSync as writeFileSync7
|
|
12435
|
+
} from "fs";
|
|
12436
|
+
import { createRequire as createRequire2 } from "module";
|
|
12175
12437
|
import { isIP } from "net";
|
|
12176
12438
|
import { homedir as homedir3, networkInterfaces, tmpdir as tmpdir3, userInfo as userInfo2 } from "os";
|
|
12177
|
-
import { join as join18 } from "path";
|
|
12439
|
+
import { dirname as dirname8, join as join18 } from "path";
|
|
12440
|
+
import { fileURLToPath as fileURLToPath4 } from "url";
|
|
12178
12441
|
import { SandboxManager } from "@anthropic-ai/sandbox-runtime";
|
|
12179
12442
|
function osSandboxUsable(log) {
|
|
12180
12443
|
if (!SandboxManager.isSupportedPlatform()) {
|
|
@@ -12276,7 +12539,7 @@ import {
|
|
|
12276
12539
|
existsSync as existsSync14,
|
|
12277
12540
|
mkdirSync as mkdirSync12,
|
|
12278
12541
|
readdirSync as readdirSync5,
|
|
12279
|
-
readFileSync as
|
|
12542
|
+
readFileSync as readFileSync11,
|
|
12280
12543
|
renameSync as renameSync6,
|
|
12281
12544
|
rmSync as rmSync11,
|
|
12282
12545
|
writeFileSync as writeFileSync8
|
|
@@ -12341,7 +12604,7 @@ var Outbox = class {
|
|
|
12341
12604
|
if (!name.endsWith(".json")) continue;
|
|
12342
12605
|
const full = join19(dir2, name);
|
|
12343
12606
|
try {
|
|
12344
|
-
const parsed = JSON.parse(
|
|
12607
|
+
const parsed = JSON.parse(readFileSync11(full, "utf8"));
|
|
12345
12608
|
if (parsed && typeof parsed.turnId === "string" && typeof parsed.seq === "number" && typeof parsed.path === "string") {
|
|
12346
12609
|
entries.push(parsed);
|
|
12347
12610
|
} else {
|
|
@@ -12551,8 +12814,8 @@ function sleep3(ms) {
|
|
|
12551
12814
|
}
|
|
12552
12815
|
|
|
12553
12816
|
// src/version.ts
|
|
12554
|
-
import { createRequire as
|
|
12555
|
-
var pkg =
|
|
12817
|
+
import { createRequire as createRequire3 } from "module";
|
|
12818
|
+
var pkg = createRequire3(import.meta.url)("../package.json");
|
|
12556
12819
|
var COMPANION_VERSION = pkg.version;
|
|
12557
12820
|
|
|
12558
12821
|
// src/supervisor.ts
|
|
@@ -12702,6 +12965,13 @@ var CompanionSupervisor = class {
|
|
|
12702
12965
|
}
|
|
12703
12966
|
this.latchBootBundledState();
|
|
12704
12967
|
}
|
|
12968
|
+
if (this.runnerSandbox() === "os") {
|
|
12969
|
+
const policy = this.sandboxPolicy();
|
|
12970
|
+
const posture = postureLines(policy);
|
|
12971
|
+
const summary = posture.join("; ");
|
|
12972
|
+
if (policy.ok) this.log.info({ posture }, `runners: OS sandbox policy \u2014 ${summary}`);
|
|
12973
|
+
else this.log.error({ posture }, `runners: ${summary}`);
|
|
12974
|
+
}
|
|
12705
12975
|
const startupHarnessRefresh = this.refreshHarnessStatuses();
|
|
12706
12976
|
this.trackHeartbeat(startupHarnessRefresh);
|
|
12707
12977
|
if (!this.config.deviceToken) {
|
|
@@ -13143,6 +13413,13 @@ var CompanionSupervisor = class {
|
|
|
13143
13413
|
// CT1679: `runners.sandbox`, checked against this host once. A host that
|
|
13144
13414
|
// can't make the sandbox says so once and runs its turns unwrapped.
|
|
13145
13415
|
sandboxUsable;
|
|
13416
|
+
// CT1684: `runners.policy`, parsed once. An invalid one is kept as the
|
|
13417
|
+
// error, which every sandboxed turn is refused with.
|
|
13418
|
+
parsedSandboxPolicy;
|
|
13419
|
+
sandboxPolicy() {
|
|
13420
|
+
this.parsedSandboxPolicy ??= parseSandboxPolicy(this.config.runners?.policy);
|
|
13421
|
+
return this.parsedSandboxPolicy;
|
|
13422
|
+
}
|
|
13146
13423
|
runnerSandbox() {
|
|
13147
13424
|
if (this.config.runners?.sandbox !== "os" || this.dockerRunners()) return "none";
|
|
13148
13425
|
this.sandboxUsable ??= osSandboxUsable(this.log);
|
|
@@ -13262,7 +13539,8 @@ var CompanionSupervisor = class {
|
|
|
13262
13539
|
reportRunnerGone: (report) => this.reportRunnerGone(report),
|
|
13263
13540
|
connectorHealth: this.connectorHealth,
|
|
13264
13541
|
locks: this.runnerLocks,
|
|
13265
|
-
sandbox: this.runnerSandbox()
|
|
13542
|
+
sandbox: this.runnerSandbox(),
|
|
13543
|
+
policy: this.sandboxPolicy()
|
|
13266
13544
|
}
|
|
13267
13545
|
} : {}
|
|
13268
13546
|
});
|
|
@@ -13959,7 +14237,7 @@ function handleUncaught(log, err, origin) {
|
|
|
13959
14237
|
}
|
|
13960
14238
|
|
|
13961
14239
|
// src/crash-marker.ts
|
|
13962
|
-
import { existsSync as existsSync15, mkdirSync as mkdirSync13, readFileSync as
|
|
14240
|
+
import { existsSync as existsSync15, mkdirSync as mkdirSync13, readFileSync as readFileSync12, rmSync as rmSync12, writeFileSync as writeFileSync9 } from "fs";
|
|
13963
14241
|
import { join as join20 } from "path";
|
|
13964
14242
|
function crashMarkerPath() {
|
|
13965
14243
|
return join20(cabaneDir(), "last-error.json");
|