@cabane/companion 0.6.125 → 0.6.126
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +49 -0
- package/dist/cli.js +563 -167
- package/dist/pairing-config.js +6 -1
- package/dist/runtime.js +346 -119
- 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, {
|
|
@@ -12171,10 +12373,20 @@ var DeviceLocks = class {
|
|
|
12171
12373
|
|
|
12172
12374
|
// src/runner/harness-sandbox.ts
|
|
12173
12375
|
import { lookup } from "dns/promises";
|
|
12174
|
-
import {
|
|
12376
|
+
import {
|
|
12377
|
+
chmodSync as chmodSync6,
|
|
12378
|
+
mkdirSync as mkdirSync11,
|
|
12379
|
+
mkdtempSync,
|
|
12380
|
+
readFileSync as readFileSync10,
|
|
12381
|
+
realpathSync as realpathSync3,
|
|
12382
|
+
rmSync as rmSync10,
|
|
12383
|
+
writeFileSync as writeFileSync7
|
|
12384
|
+
} from "fs";
|
|
12385
|
+
import { createRequire as createRequire2 } from "module";
|
|
12175
12386
|
import { isIP } from "net";
|
|
12176
12387
|
import { homedir as homedir3, networkInterfaces, tmpdir as tmpdir3, userInfo as userInfo2 } from "os";
|
|
12177
|
-
import { join as join18 } from "path";
|
|
12388
|
+
import { dirname as dirname8, join as join18 } from "path";
|
|
12389
|
+
import { fileURLToPath as fileURLToPath4 } from "url";
|
|
12178
12390
|
import { SandboxManager } from "@anthropic-ai/sandbox-runtime";
|
|
12179
12391
|
function osSandboxUsable(log) {
|
|
12180
12392
|
if (!SandboxManager.isSupportedPlatform()) {
|
|
@@ -12276,7 +12488,7 @@ import {
|
|
|
12276
12488
|
existsSync as existsSync14,
|
|
12277
12489
|
mkdirSync as mkdirSync12,
|
|
12278
12490
|
readdirSync as readdirSync5,
|
|
12279
|
-
readFileSync as
|
|
12491
|
+
readFileSync as readFileSync11,
|
|
12280
12492
|
renameSync as renameSync6,
|
|
12281
12493
|
rmSync as rmSync11,
|
|
12282
12494
|
writeFileSync as writeFileSync8
|
|
@@ -12341,7 +12553,7 @@ var Outbox = class {
|
|
|
12341
12553
|
if (!name.endsWith(".json")) continue;
|
|
12342
12554
|
const full = join19(dir2, name);
|
|
12343
12555
|
try {
|
|
12344
|
-
const parsed = JSON.parse(
|
|
12556
|
+
const parsed = JSON.parse(readFileSync11(full, "utf8"));
|
|
12345
12557
|
if (parsed && typeof parsed.turnId === "string" && typeof parsed.seq === "number" && typeof parsed.path === "string") {
|
|
12346
12558
|
entries.push(parsed);
|
|
12347
12559
|
} else {
|
|
@@ -12551,8 +12763,8 @@ function sleep3(ms) {
|
|
|
12551
12763
|
}
|
|
12552
12764
|
|
|
12553
12765
|
// src/version.ts
|
|
12554
|
-
import { createRequire as
|
|
12555
|
-
var pkg =
|
|
12766
|
+
import { createRequire as createRequire3 } from "module";
|
|
12767
|
+
var pkg = createRequire3(import.meta.url)("../package.json");
|
|
12556
12768
|
var COMPANION_VERSION = pkg.version;
|
|
12557
12769
|
|
|
12558
12770
|
// src/supervisor.ts
|
|
@@ -12702,6 +12914,13 @@ var CompanionSupervisor = class {
|
|
|
12702
12914
|
}
|
|
12703
12915
|
this.latchBootBundledState();
|
|
12704
12916
|
}
|
|
12917
|
+
if (this.runnerSandbox() === "os") {
|
|
12918
|
+
const policy = this.sandboxPolicy();
|
|
12919
|
+
const posture = postureLines(policy);
|
|
12920
|
+
const summary = posture.join("; ");
|
|
12921
|
+
if (policy.ok) this.log.info({ posture }, `runners: OS sandbox policy \u2014 ${summary}`);
|
|
12922
|
+
else this.log.error({ posture }, `runners: ${summary}`);
|
|
12923
|
+
}
|
|
12705
12924
|
const startupHarnessRefresh = this.refreshHarnessStatuses();
|
|
12706
12925
|
this.trackHeartbeat(startupHarnessRefresh);
|
|
12707
12926
|
if (!this.config.deviceToken) {
|
|
@@ -13143,6 +13362,13 @@ var CompanionSupervisor = class {
|
|
|
13143
13362
|
// CT1679: `runners.sandbox`, checked against this host once. A host that
|
|
13144
13363
|
// can't make the sandbox says so once and runs its turns unwrapped.
|
|
13145
13364
|
sandboxUsable;
|
|
13365
|
+
// CT1684: `runners.policy`, parsed once. An invalid one is kept as the
|
|
13366
|
+
// error, which every sandboxed turn is refused with.
|
|
13367
|
+
parsedSandboxPolicy;
|
|
13368
|
+
sandboxPolicy() {
|
|
13369
|
+
this.parsedSandboxPolicy ??= parseSandboxPolicy(this.config.runners?.policy);
|
|
13370
|
+
return this.parsedSandboxPolicy;
|
|
13371
|
+
}
|
|
13146
13372
|
runnerSandbox() {
|
|
13147
13373
|
if (this.config.runners?.sandbox !== "os" || this.dockerRunners()) return "none";
|
|
13148
13374
|
this.sandboxUsable ??= osSandboxUsable(this.log);
|
|
@@ -13262,7 +13488,8 @@ var CompanionSupervisor = class {
|
|
|
13262
13488
|
reportRunnerGone: (report) => this.reportRunnerGone(report),
|
|
13263
13489
|
connectorHealth: this.connectorHealth,
|
|
13264
13490
|
locks: this.runnerLocks,
|
|
13265
|
-
sandbox: this.runnerSandbox()
|
|
13491
|
+
sandbox: this.runnerSandbox(),
|
|
13492
|
+
policy: this.sandboxPolicy()
|
|
13266
13493
|
}
|
|
13267
13494
|
} : {}
|
|
13268
13495
|
});
|
|
@@ -13959,7 +14186,7 @@ function handleUncaught(log, err, origin) {
|
|
|
13959
14186
|
}
|
|
13960
14187
|
|
|
13961
14188
|
// src/crash-marker.ts
|
|
13962
|
-
import { existsSync as existsSync15, mkdirSync as mkdirSync13, readFileSync as
|
|
14189
|
+
import { existsSync as existsSync15, mkdirSync as mkdirSync13, readFileSync as readFileSync12, rmSync as rmSync12, writeFileSync as writeFileSync9 } from "fs";
|
|
13963
14190
|
import { join as join20 } from "path";
|
|
13964
14191
|
function crashMarkerPath() {
|
|
13965
14192
|
return join20(cabaneDir(), "last-error.json");
|