@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/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, {
@@ -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 network = `cabane-runners-${opts.deviceKey}`;
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 networkReady = null;
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 ensureNetwork = () => {
11443
- networkReady ??= (async () => {
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 must(
11506
- ["network", "connect", "--alias", PROXY_ALIAS, network, proxy],
11507
- "connecting the proxy"
11508
- );
11699
+ await retireSharedNetwork();
11509
11700
  await proxyListening();
11510
- log.info({ network, proxy, allow: opts.egressAllow }, "runners: egress proxy ready");
11701
+ log.info({ proxy, allow: opts.egressAllow }, "runners: egress proxy ready");
11511
11702
  })().catch((err) => {
11512
- networkReady = null;
11703
+ proxyReady = null;
11513
11704
  throw err;
11514
11705
  });
11515
- return networkReady;
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
- network,
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 ensureNetwork();
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
- await (keep ? docker.run(["stop", "-t", "1", name]) : destroy(name)).catch(() => {
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 { chmodSync as chmodSync6, mkdirSync as mkdirSync11, mkdtempSync, rmSync as rmSync10, writeFileSync as writeFileSync7 } from "fs";
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 readFileSync10,
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(readFileSync10(full, "utf8"));
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 createRequire2 } from "module";
12555
- var pkg = createRequire2(import.meta.url)("../package.json");
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 readFileSync11, rmSync as rmSync12, writeFileSync as writeFileSync9 } from "fs";
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");