@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/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 z13 } from "zod";
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 = z13.record(z13.string(), z13.string());
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 z15 } from "zod";
8436
+ import { z as z16 } from "zod";
8278
8437
 
8279
8438
  // src/run-config.ts
8280
- import { z as z14 } from "zod";
8281
- var mcpStdioServerSchema = z14.object({
8282
- type: z14.literal("stdio").optional(),
8283
- command: z14.string().min(1),
8284
- args: z14.array(z14.string()).optional(),
8285
- env: z14.record(z14.string(), z14.string()).optional()
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 = z14.object({
8288
- type: z14.literal("http"),
8289
- url: z14.string().url(),
8290
- headers: z14.record(z14.string(), z14.string()).optional()
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 = z14.object({
8293
- type: z14.literal("sse"),
8294
- url: z14.string().url(),
8295
- headers: z14.record(z14.string(), z14.string()).optional()
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 = z14.union([mcpHttpServerSchema, mcpSseServerSchema, mcpStdioServerSchema]);
8298
- var thinkingConfigSchema = z14.discriminatedUnion("type", [
8299
- z14.object({ type: z14.literal("adaptive") }),
8300
- z14.object({ type: z14.literal("enabled"), budgetTokens: z14.number().int().positive().optional() }),
8301
- z14.object({ type: z14.literal("disabled") })
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 = z14.enum(["low", "medium", "high", "xhigh", "max"]);
8304
- var runConfigSchema = z14.object({
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: z14.boolean().optional(),
8311
- mcpServers: z14.record(z14.string(), mcpServerDefSchema).optional(),
8312
- model: z14.string().min(1).optional(),
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 = z15.object({
8341
- quotaState: z15.enum(["ok", "limited", "auth_expired", "unknown"]),
8342
- limitedUntil: z15.string().nullable(),
8343
- lastError: z15.string().nullable()
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 = z15.object({
8346
- deviceId: z15.string().optional(),
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: z15.string(),
8350
- workspaceId: z15.string(),
8351
- workspaceSlug: z15.string(),
8352
- agentId: z15.string(),
8353
- agentUsername: z15.string(),
8354
- agentName: z15.string(),
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: z15.string().optional(),
8358
- claudeCodeAvailable: z15.boolean(),
8359
- codexAvailable: z15.boolean(),
8360
- idleTimeoutMs: z15.number().int().positive().optional(),
8361
- totalTimeoutMs: z15.number().int().positive().optional(),
8362
- leaseRenewalMs: z15.number().int().positive().optional(),
8363
- preparingRowDelayMs: z15.number().int().nonnegative().optional(),
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: z15.boolean(),
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: z15.enum(["os", "none"]),
8370
- logLevel: z15.string(),
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: z15.record(z15.string(), runtimeHealthSchema),
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: z15.boolean(),
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: z15.string(),
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: z15.boolean().optional(),
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: z15.boolean().optional()
8548
+ secretsFromLauncher: z16.boolean().optional()
8387
8549
  });
8388
- var dispatchSchema = z15.object({
8389
- type: z15.literal("device:dispatch_requested"),
8390
- agentId: z15.string(),
8391
- deviceId: z15.string(),
8392
- conversationId: z15.string(),
8393
- messageId: z15.string(),
8394
- episode: z15.number().int().optional()
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 = z15.object({
8397
- credential: z15.string().startsWith("cabrn_")
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({ credential: input.credential })}
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 minted = await mintForRunner(opts, payload, handleOpts, controller.signal, log);
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 z16 } from "zod";
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: z16.string().min(1).max(MAX_SKIP_REASON_BYTES).describe(TURN_CONTROL_FIELD_DESCRIPTIONS.skip_turn.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: z16.string().uuid().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask.targetUserId),
8920
- question: z16.string().min(1).max(MAX_ASK_QUESTION_BYTES).optional().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask.question),
8921
- headline: z16.string().min(1).max(MAX_ASK_HEADLINE_BYTES).optional().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask.headline),
8922
- options: z16.array(
8923
- z16.union([
8924
- z16.string().min(1).max(MAX_ASK_OPTION_BYTES),
8925
- z16.object({
8926
- label: z16.string().min(1).max(MAX_ASK_OPTION_BYTES).describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask["options.label"]),
8927
- move: z16.object({
8928
- task: z16.string().min(1).max(200).describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask["options.move.task"]),
8929
- status: z16.enum(TASK_STAGES).describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask["options.move.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: z16.array(
8935
- z16.object({
8936
- headline: z16.string().min(1).max(MAX_ASK_HEADLINE_BYTES).describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask["questions.headline"]),
8937
- body: z16.string().min(1).max(MAX_ASK_QUESTION_BYTES).optional().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask["questions.body"]),
8938
- options: z16.array(
8939
- z16.union([
8940
- z16.string().min(1).max(MAX_ASK_OPTION_BYTES),
8941
- z16.object({
8942
- label: z16.string().min(1).max(MAX_ASK_OPTION_BYTES).describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask["questions.options.label"]),
8943
- move: z16.object({
8944
- task: z16.string().min(1).max(200).describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask["questions.options.move.task"]),
8945
- status: z16.enum(TASK_STAGES).describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask["questions.options.move.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: z16.object({
8953
- askId: z16.string().uuid().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask["replaces.askId"]),
8954
- reason: z16.string().min(1).max(MAX_WITHDRAW_REASON_BYTES).describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask["replaces.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: z16.number().int().positive().optional().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.wake_me.afterSeconds),
8996
- at: z16.string().datetime({ offset: true }).optional().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.wake_me.at),
8997
- note: z16.string().min(1).max(MAX_WAKE_NOTE_BYTES).describe(TURN_CONTROL_FIELD_DESCRIPTIONS.wake_me.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: z16.string().uuid().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.withdraw_ask.askId),
9032
- reason: z16.string().min(1).max(MAX_WITHDRAW_REASON_BYTES).describe(TURN_CONTROL_FIELD_DESCRIPTIONS.withdraw_ask.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
- const sandbox = this.opts.harnessSandbox ? await this.opts.harnessSandbox({ cwd: this.effectiveCwd, log: turnLog }) : null;
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 { chmodSync as chmodSync6, mkdirSync as mkdirSync11, mkdtempSync, rmSync as rmSync10, writeFileSync as writeFileSync7 } from "fs";
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 readFileSync10,
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(readFileSync10(full, "utf8"));
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 createRequire2 } from "module";
12555
- var pkg = createRequire2(import.meta.url)("../package.json");
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 readFileSync11, rmSync as rmSync12, writeFileSync as writeFileSync9 } from "fs";
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");