@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/cli.js CHANGED
@@ -541,6 +541,10 @@ var init_config = __esm({
541
541
  // wraps the runner in an OS sandbox where the host supports one; `'none'`
542
542
  // (the default) runs it as a plain child beside this process, a supported
543
543
  // configuration in its own right rather than a degraded one.
544
+ // CT1684: `policy` is what the OS sandbox lets a turn reach, read and hold
545
+ // (`runner/sandbox-policy.ts`). It is validated there rather than here, so a
546
+ // bad policy refuses the turns it governs, with the field named, instead of
547
+ // keeping the companion from starting at all.
544
548
  runners: z2.object({
545
549
  enabled: z2.boolean().optional(),
546
550
  provider: z2.enum(["local-process", "docker"]).optional(),
@@ -574,7 +578,8 @@ var init_config = __esm({
574
578
  // container's git for an allowed host, e.g. `!gh auth git-credential`.
575
579
  // Absent: git's own configured helpers on this device.
576
580
  gitCredentialHelper: z2.string().min(1).optional()
577
- }).strict().optional()
581
+ }).strict().optional(),
582
+ policy: z2.unknown().optional()
578
583
  }).strict().optional()
579
584
  });
580
585
  }
@@ -6662,6 +6667,173 @@ var init_src = __esm({
6662
6667
  }
6663
6668
  });
6664
6669
 
6670
+ // src/runner/sandbox-policy.ts
6671
+ import { NetworkConfigSchema } from "@anthropic-ai/sandbox-runtime";
6672
+ import { globToRegex } from "@anthropic-ai/sandbox-runtime/dist/sandbox/sandbox-utils.js";
6673
+ import { z as z13 } from "zod";
6674
+ function parseSandboxPolicy(raw) {
6675
+ if (raw === void 0) return { ok: true, policy: {} };
6676
+ const result = sandboxPolicySchema.safeParse(raw);
6677
+ if (!result.success) {
6678
+ const issue = result.error.issues[0];
6679
+ return { ok: false, error: `${fieldPath(issue.path)}: ${issue.message}` };
6680
+ }
6681
+ const refused = unenforceable(result.data);
6682
+ return refused ? { ok: false, error: refused } : { ok: true, policy: result.data };
6683
+ }
6684
+ function fieldPath(path) {
6685
+ return ["runners", "policy", ...path.map(String)].join(".");
6686
+ }
6687
+ function unenforceable(policy) {
6688
+ const hostLists = [
6689
+ ["hosts.allow", "allowedDomains", policy.hosts?.allow],
6690
+ ["hosts.deny", "deniedDomains", policy.hosts?.deny],
6691
+ ...Object.entries(policy.credentials?.env ?? {}).map(
6692
+ ([name, entry]) => [
6693
+ `credentials.env.${name}.hosts`,
6694
+ "allowedDomains",
6695
+ entry.hosts
6696
+ ]
6697
+ )
6698
+ ];
6699
+ for (const [field, as, hosts] of hostLists) {
6700
+ for (const [i, host] of (hosts ?? []).entries()) {
6701
+ const check = NetworkConfigSchema.safeParse({
6702
+ allowedDomains: as === "allowedDomains" ? [host] : [],
6703
+ deniedDomains: as === "deniedDomains" ? [host] : []
6704
+ });
6705
+ if (!check.success) {
6706
+ const message = check.error.issues[0]?.message ?? "not a host pattern the sandbox accepts";
6707
+ return `runners.policy.${field}.${i}: ${message}`;
6708
+ }
6709
+ }
6710
+ }
6711
+ const pathLists = [
6712
+ ["filesystem.denyRead", policy.filesystem?.denyRead],
6713
+ ["filesystem.allowRead", policy.filesystem?.allowRead],
6714
+ ["filesystem.allowWrite", policy.filesystem?.allowWrite]
6715
+ ];
6716
+ for (const [field, paths] of pathLists) {
6717
+ for (const [i, path] of (paths ?? []).entries()) {
6718
+ try {
6719
+ new RegExp(globToRegex(path));
6720
+ } catch (err) {
6721
+ const message = err instanceof Error ? err.message : String(err);
6722
+ return `runners.policy.${field}.${i}: not a pattern the sandbox can match (${message})`;
6723
+ }
6724
+ }
6725
+ }
6726
+ return null;
6727
+ }
6728
+ function modelHostsFor(runtime) {
6729
+ if (runtime === "claude-code") return ANTHROPIC_HOSTS;
6730
+ if (runtime === "codex") return OPENAI_HOSTS;
6731
+ return [...ANTHROPIC_HOSTS, ...OPENAI_HOSTS];
6732
+ }
6733
+ function gitAuthVar(index) {
6734
+ return `CABANE_GIT_AUTH_${index}`;
6735
+ }
6736
+ function resolvePolicyCredentials(policy, store) {
6737
+ const missing = /* @__PURE__ */ new Set();
6738
+ const credentials = [];
6739
+ const lookup2 = (ref) => {
6740
+ const name = placeholderName(ref);
6741
+ const value = store.get(name);
6742
+ if (value === void 0) missing.add(name);
6743
+ return value;
6744
+ };
6745
+ for (const [name, entry] of Object.entries(policy.credentials?.env ?? {})) {
6746
+ const value = lookup2(entry.secret);
6747
+ if (value === void 0) continue;
6748
+ credentials.push({ name, value, hosts: entry.hosts });
6749
+ }
6750
+ (policy.credentials?.git ?? []).forEach((entry, i) => {
6751
+ const secret = lookup2(entry.secret);
6752
+ if (secret === void 0) return;
6753
+ const user = entry.username ?? "x-access-token";
6754
+ credentials.push({
6755
+ name: gitAuthVar(i),
6756
+ value: Buffer.from(`${user}:${secret}`).toString("base64"),
6757
+ hosts: [entry.host.replace(/:\d+$/, "")],
6758
+ gitHost: entry.host
6759
+ });
6760
+ });
6761
+ return { credentials, missing: [...missing].sort() };
6762
+ }
6763
+ function policyHasCredentials(policy) {
6764
+ return Object.keys(policy.credentials?.env ?? {}).length > 0 || (policy.credentials?.git ?? []).length > 0;
6765
+ }
6766
+ function postureLines(parsed) {
6767
+ if (!parsed.ok) return [`policy invalid \u2014 turns are refused until it's fixed: ${parsed.error}`];
6768
+ const { policy } = parsed;
6769
+ const mode = policy.filesystem?.mode ?? "blocklist";
6770
+ const defaults = policy.hosts?.defaults !== false;
6771
+ const lines2 = [
6772
+ `filesystem: ${mode}${mode === "strict" ? " (home unreadable except the checkout, harness state and allowRead)" : ""}`,
6773
+ `hosts: Cabane + the turn's model provider${defaults ? " + defaults (GitHub, npm, PyPI, crates.io)" : ""}${policy.hosts?.allow?.length ? ` + ${policy.hosts.allow.join(", ")}` : ""}`
6774
+ ];
6775
+ if (policy.hosts?.deny?.length) lines2.push(`hosts denied: ${policy.hosts.deny.join(", ")}`);
6776
+ const extraDeny = policy.filesystem?.denyRead ?? [];
6777
+ if (extraDeny.length) lines2.push(`denied reads (extra): ${extraDeny.join(", ")}`);
6778
+ const allowRead = policy.filesystem?.allowRead ?? [];
6779
+ if (allowRead.length) lines2.push(`reads allowed back: ${allowRead.join(", ")}`);
6780
+ const allowWrite = policy.filesystem?.allowWrite ?? [];
6781
+ if (allowWrite.length) lines2.push(`writes allowed (extra): ${allowWrite.join(", ")}`);
6782
+ const env = Object.keys(policy.credentials?.env ?? {});
6783
+ const git = (policy.credentials?.git ?? []).map((g) => `git@${g.host}`);
6784
+ if (env.length || git.length) {
6785
+ lines2.push(`masked credentials: ${[...env, ...git].join(", ")}`);
6786
+ }
6787
+ return lines2;
6788
+ }
6789
+ var hostPattern, secretRef, envCredentialSchema, gitCredentialSchema, sandboxPolicySchema, DEFAULT_ALLOWED_HOSTS, ANTHROPIC_HOSTS, OPENAI_HOSTS, placeholderName, HarnessSandboxRefused;
6790
+ var init_sandbox_policy = __esm({
6791
+ "src/runner/sandbox-policy.ts"() {
6792
+ "use strict";
6793
+ hostPattern = z13.string().min(1);
6794
+ secretRef = z13.string().regex(/^\$\{[A-Za-z_][A-Za-z0-9_]*\}$/, "must be a ${NAME} reference into secrets.json");
6795
+ envCredentialSchema = z13.object({ secret: secretRef, hosts: z13.array(hostPattern).min(1) }).strict();
6796
+ gitCredentialSchema = z13.object({
6797
+ host: z13.string().regex(/^[A-Za-z0-9.-]+(:\d+)?$/, "must be a host name, like github.com"),
6798
+ username: z13.string().min(1).optional(),
6799
+ secret: secretRef
6800
+ }).strict();
6801
+ sandboxPolicySchema = z13.object({
6802
+ hosts: z13.object({
6803
+ allow: z13.array(hostPattern).optional(),
6804
+ deny: z13.array(hostPattern).optional(),
6805
+ defaults: z13.boolean().optional()
6806
+ }).strict().optional(),
6807
+ filesystem: z13.object({
6808
+ mode: z13.enum(["blocklist", "strict"]).optional(),
6809
+ denyRead: z13.array(z13.string().min(1)).optional(),
6810
+ allowRead: z13.array(z13.string().min(1)).optional(),
6811
+ allowWrite: z13.array(z13.string().min(1)).optional()
6812
+ }).strict().optional(),
6813
+ credentials: z13.object({
6814
+ env: z13.record(z13.string().regex(/^[A-Za-z_][A-Za-z0-9_]*$/), envCredentialSchema).optional(),
6815
+ git: z13.array(gitCredentialSchema).optional()
6816
+ }).strict().optional()
6817
+ }).strict();
6818
+ DEFAULT_ALLOWED_HOSTS = [
6819
+ "github.com",
6820
+ "*.github.com",
6821
+ "*.githubusercontent.com",
6822
+ "registry.npmjs.org",
6823
+ "pypi.org",
6824
+ "files.pythonhosted.org",
6825
+ "crates.io",
6826
+ "static.crates.io",
6827
+ "index.crates.io"
6828
+ ];
6829
+ ANTHROPIC_HOSTS = ["api.anthropic.com", "*.anthropic.com", "claude.ai", "*.claude.ai"];
6830
+ OPENAI_HOSTS = ["api.openai.com", "auth.openai.com", "chatgpt.com", "*.chatgpt.com"];
6831
+ placeholderName = (ref) => ref.slice(2, -1);
6832
+ HarnessSandboxRefused = class extends Error {
6833
+ };
6834
+ }
6835
+ });
6836
+
6665
6837
  // src/runner/opencode-lock.ts
6666
6838
  function opencodeLockKey(serverUrl) {
6667
6839
  return `opencode:${serverUrl}`;
@@ -7023,7 +7195,7 @@ var init_turn_containment = __esm({
7023
7195
  // src/secrets.ts
7024
7196
  import { existsSync as existsSync7, readFileSync as readFileSync6 } from "fs";
7025
7197
  import { join as join10 } from "path";
7026
- import { z as z13 } from "zod";
7198
+ import { z as z14 } from "zod";
7027
7199
  function secretsPath() {
7028
7200
  return join10(cabaneDir(), "secrets.json");
7029
7201
  }
@@ -7108,19 +7280,30 @@ function resolveMcpSecrets(mcpServers, store) {
7108
7280
  }
7109
7281
  return { servers, missing: [...missing].sort() };
7110
7282
  }
7111
- var PLACEHOLDER_RE, secretStoreSchema;
7283
+ function missingSecretReason(missing) {
7284
+ const prefix = "missing_secret: ";
7285
+ for (let shown = missing.length; shown >= 0; shown--) {
7286
+ const omitted = missing.length - shown;
7287
+ const parts = [...missing.slice(0, shown), ...omitted > 0 ? [`+${omitted}`] : []];
7288
+ const reason = prefix + parts.join(",");
7289
+ if (reason.length <= ERROR_REASON_MAX) return reason;
7290
+ }
7291
+ return `${prefix}+${missing.length}`;
7292
+ }
7293
+ var PLACEHOLDER_RE, secretStoreSchema, ERROR_REASON_MAX;
7112
7294
  var init_secrets = __esm({
7113
7295
  "src/secrets.ts"() {
7114
7296
  "use strict";
7115
7297
  init_config();
7116
7298
  init_errors();
7117
7299
  PLACEHOLDER_RE = /\$\{([A-Za-z_][A-Za-z0-9_]*)\}/g;
7118
- secretStoreSchema = z13.record(z13.string(), z13.string());
7300
+ secretStoreSchema = z14.record(z14.string(), z14.string());
7301
+ ERROR_REASON_MAX = 200;
7119
7302
  }
7120
7303
  });
7121
7304
 
7122
7305
  // src/run-config.ts
7123
- import { z as z14 } from "zod";
7306
+ import { z as z15 } from "zod";
7124
7307
  function parseRunConfig(raw, onWarn) {
7125
7308
  const result = runConfigSchema.safeParse(raw ?? {});
7126
7309
  if (result.success) return result.data;
@@ -7133,38 +7316,38 @@ var mcpStdioServerSchema, mcpHttpServerSchema, mcpSseServerSchema, mcpServerDefS
7133
7316
  var init_run_config = __esm({
7134
7317
  "src/run-config.ts"() {
7135
7318
  "use strict";
7136
- mcpStdioServerSchema = z14.object({
7137
- type: z14.literal("stdio").optional(),
7138
- command: z14.string().min(1),
7139
- args: z14.array(z14.string()).optional(),
7140
- env: z14.record(z14.string(), z14.string()).optional()
7319
+ mcpStdioServerSchema = z15.object({
7320
+ type: z15.literal("stdio").optional(),
7321
+ command: z15.string().min(1),
7322
+ args: z15.array(z15.string()).optional(),
7323
+ env: z15.record(z15.string(), z15.string()).optional()
7141
7324
  });
7142
- mcpHttpServerSchema = z14.object({
7143
- type: z14.literal("http"),
7144
- url: z14.string().url(),
7145
- headers: z14.record(z14.string(), z14.string()).optional()
7325
+ mcpHttpServerSchema = z15.object({
7326
+ type: z15.literal("http"),
7327
+ url: z15.string().url(),
7328
+ headers: z15.record(z15.string(), z15.string()).optional()
7146
7329
  });
7147
- mcpSseServerSchema = z14.object({
7148
- type: z14.literal("sse"),
7149
- url: z14.string().url(),
7150
- headers: z14.record(z14.string(), z14.string()).optional()
7330
+ mcpSseServerSchema = z15.object({
7331
+ type: z15.literal("sse"),
7332
+ url: z15.string().url(),
7333
+ headers: z15.record(z15.string(), z15.string()).optional()
7151
7334
  });
7152
- mcpServerDefSchema = z14.union([mcpHttpServerSchema, mcpSseServerSchema, mcpStdioServerSchema]);
7153
- thinkingConfigSchema = z14.discriminatedUnion("type", [
7154
- z14.object({ type: z14.literal("adaptive") }),
7155
- z14.object({ type: z14.literal("enabled"), budgetTokens: z14.number().int().positive().optional() }),
7156
- z14.object({ type: z14.literal("disabled") })
7335
+ mcpServerDefSchema = z15.union([mcpHttpServerSchema, mcpSseServerSchema, mcpStdioServerSchema]);
7336
+ thinkingConfigSchema = z15.discriminatedUnion("type", [
7337
+ z15.object({ type: z15.literal("adaptive") }),
7338
+ z15.object({ type: z15.literal("enabled"), budgetTokens: z15.number().int().positive().optional() }),
7339
+ z15.object({ type: z15.literal("disabled") })
7157
7340
  ]);
7158
- effortSchema = z14.enum(["low", "medium", "high", "xhigh", "max"]);
7159
- runConfigSchema = z14.object({
7341
+ effortSchema = z15.enum(["low", "medium", "high", "xhigh", "max"]);
7342
+ runConfigSchema = z15.object({
7160
7343
  // CT788: the host-access binary replaced the `assistant`/`coding`/`custom` mode
7161
7344
  // trio + its custom tool lists — `true` grants the host filesystem/shell, absent
7162
7345
  // is the locked surface. Kept in lockstep with `@cabane/shared`'s
7163
7346
  // `agentRunConfigSchema` (independent zod, forward-compatible: unknown keys are
7164
7347
  // stripped, so an older companion riding a newer server never rejects the config).
7165
- hostAccess: z14.boolean().optional(),
7166
- mcpServers: z14.record(z14.string(), mcpServerDefSchema).optional(),
7167
- model: z14.string().min(1).optional(),
7348
+ hostAccess: z15.boolean().optional(),
7349
+ mcpServers: z15.record(z15.string(), mcpServerDefSchema).optional(),
7350
+ model: z15.string().min(1).optional(),
7168
7351
  thinking: thinkingConfigSchema.optional(),
7169
7352
  effort: effortSchema.optional()
7170
7353
  });
@@ -7173,7 +7356,7 @@ var init_run_config = __esm({
7173
7356
  });
7174
7357
 
7175
7358
  // src/runner/protocol.ts
7176
- import { z as z15 } from "zod";
7359
+ import { z as z16 } from "zod";
7177
7360
  function required(env, name) {
7178
7361
  const value = env[name];
7179
7362
  if (!value) throw new Error(`runner: ${name} is not set`);
@@ -7186,6 +7369,7 @@ function parseRunnerInput(env, stdin) {
7186
7369
  return {
7187
7370
  baseUrl: required(env, RUNNER_ENV.url),
7188
7371
  credential: credentials.credential,
7372
+ sandboxCredentials: credentials.sandboxCredentials ?? [],
7189
7373
  dispatch,
7190
7374
  turnId: required(env, RUNNER_ENV.turnId),
7191
7375
  resumed: env[RUNNER_ENV.resumed] === "1",
@@ -7202,7 +7386,10 @@ function runnerEnv(input) {
7202
7386
  };
7203
7387
  }
7204
7388
  function runnerStdin(input) {
7205
- return `${JSON.stringify({ credential: input.credential })}
7389
+ return `${JSON.stringify({
7390
+ credential: input.credential,
7391
+ ...input.sandboxCredentials?.length ? { sandboxCredentials: input.sandboxCredentials } : {}
7392
+ })}
7206
7393
  `;
7207
7394
  }
7208
7395
  function encodeLauncherMessage(message) {
@@ -7238,6 +7425,7 @@ var init_protocol = __esm({
7238
7425
  "use strict";
7239
7426
  init_config();
7240
7427
  init_run_config();
7428
+ init_sandbox_policy();
7241
7429
  RUNNER_ENV = {
7242
7430
  // The wire the runner speaks.
7243
7431
  url: "CABANE_URL",
@@ -7251,64 +7439,75 @@ var init_protocol = __esm({
7251
7439
  // Everything else the turn is constructed from, as JSON: see `RunnerSpec`.
7252
7440
  spec: "CABANE_RUNNER_SPEC"
7253
7441
  };
7254
- runtimeHealthSchema = z15.object({
7255
- quotaState: z15.enum(["ok", "limited", "auth_expired", "unknown"]),
7256
- limitedUntil: z15.string().nullable(),
7257
- lastError: z15.string().nullable()
7442
+ runtimeHealthSchema = z16.object({
7443
+ quotaState: z16.enum(["ok", "limited", "auth_expired", "unknown"]),
7444
+ limitedUntil: z16.string().nullable(),
7445
+ lastError: z16.string().nullable()
7258
7446
  });
7259
- runnerSpecSchema = z15.object({
7260
- deviceId: z15.string().optional(),
7447
+ runnerSpecSchema = z16.object({
7448
+ deviceId: z16.string().optional(),
7261
7449
  // The launcher's boot id: the one its heartbeat carries, so a runner's lease
7262
7450
  // matches the device that holds it.
7263
- bootId: z15.string(),
7264
- workspaceId: z15.string(),
7265
- workspaceSlug: z15.string(),
7266
- agentId: z15.string(),
7267
- agentUsername: z15.string(),
7268
- agentName: z15.string(),
7451
+ bootId: z16.string(),
7452
+ workspaceId: z16.string(),
7453
+ workspaceSlug: z16.string(),
7454
+ agentId: z16.string(),
7455
+ agentUsername: z16.string(),
7456
+ agentName: z16.string(),
7269
7457
  local: localAgentConfigSchema,
7270
7458
  runConfig: runConfigSchema,
7271
- opencodeServerUrl: z15.string().optional(),
7272
- claudeCodeAvailable: z15.boolean(),
7273
- codexAvailable: z15.boolean(),
7274
- idleTimeoutMs: z15.number().int().positive().optional(),
7275
- totalTimeoutMs: z15.number().int().positive().optional(),
7276
- leaseRenewalMs: z15.number().int().positive().optional(),
7277
- preparingRowDelayMs: z15.number().int().nonnegative().optional(),
7459
+ opencodeServerUrl: z16.string().optional(),
7460
+ claudeCodeAvailable: z16.boolean(),
7461
+ codexAvailable: z16.boolean(),
7462
+ idleTimeoutMs: z16.number().int().positive().optional(),
7463
+ totalTimeoutMs: z16.number().int().positive().optional(),
7464
+ leaseRenewalMs: z16.number().int().positive().optional(),
7465
+ preparingRowDelayMs: z16.number().int().nonnegative().optional(),
7278
7466
  // Whether the turn writes a transcript. The directory is the runner's own
7279
7467
  // (`transcriptsDir()`), so `CABANE_RUNNER_STATE_DIR` moves it with the outbox.
7280
- transcripts: z15.boolean(),
7468
+ transcripts: z16.boolean(),
7281
7469
  // Whether the harness starts under the OS sandbox. The launcher sends 'os'
7282
7470
  // only when this host can make one.
7283
- sandbox: z15.enum(["os", "none"]),
7284
- logLevel: z15.string(),
7471
+ sandbox: z16.enum(["os", "none"]),
7472
+ // CT1684: the device's sandbox policy, validated by the launcher. Absent
7473
+ // when the sandbox is off.
7474
+ sandboxPolicy: sandboxPolicySchema.optional(),
7475
+ logLevel: z16.string(),
7285
7476
  // The launcher's connector-health observations, by runtime, as of the start.
7286
- connectorHealth: z15.record(z15.string(), runtimeHealthSchema),
7477
+ connectorHealth: z16.record(z16.string(), runtimeHealthSchema),
7287
7478
  // True when this (conversation, agent) pair was already told its session
7288
7479
  // could not be saved: the notice is once per pair, not once per process.
7289
- sessionWriteNoticeClaimed: z15.boolean(),
7480
+ sessionWriteNoticeClaimed: z16.boolean(),
7290
7481
  // CT1680: the runner's identity, minted with its credential. The start
7291
7482
  // PATCH carries it so the lease knows which runner holds the span.
7292
- runnerId: z15.string(),
7483
+ runnerId: z16.string(),
7293
7484
  // CT1681: the launcher keeps the runtime's session files beside the attempt
7294
7485
  // and restores them into this runner's home, so the runner names the attempt
7295
7486
  // each context was composed for and waits for the launcher to say it's in
7296
7487
  // place. Absent: nothing to wait for (a runner on the launcher's own disk).
7297
- attemptState: z15.boolean().optional(),
7488
+ attemptState: z16.boolean().optional(),
7298
7489
  // CT1681: MCP `${NAME}` secrets come from the launcher, by name, instead of
7299
7490
  // this machine's secrets.json. Absent: the runner reads the device's file.
7300
- secretsFromLauncher: z15.boolean().optional()
7491
+ secretsFromLauncher: z16.boolean().optional()
7301
7492
  });
7302
- dispatchSchema = z15.object({
7303
- type: z15.literal("device:dispatch_requested"),
7304
- agentId: z15.string(),
7305
- deviceId: z15.string(),
7306
- conversationId: z15.string(),
7307
- messageId: z15.string(),
7308
- episode: z15.number().int().optional()
7493
+ dispatchSchema = z16.object({
7494
+ type: z16.literal("device:dispatch_requested"),
7495
+ agentId: z16.string(),
7496
+ deviceId: z16.string(),
7497
+ conversationId: z16.string(),
7498
+ messageId: z16.string(),
7499
+ episode: z16.number().int().optional()
7309
7500
  });
7310
- credentialsSchema = z15.object({
7311
- credential: z15.string().startsWith("cabrn_")
7501
+ credentialsSchema = z16.object({
7502
+ credential: z16.string().startsWith("cabrn_"),
7503
+ sandboxCredentials: z16.array(
7504
+ z16.object({
7505
+ name: z16.string(),
7506
+ value: z16.string(),
7507
+ hosts: z16.array(z16.string()),
7508
+ gitHost: z16.string().optional()
7509
+ })
7510
+ ).optional()
7312
7511
  });
7313
7512
  }
7314
7513
  });
@@ -7360,6 +7559,7 @@ function runnerSpec(opts, launch, duties, runnerId) {
7360
7559
  sandbox: launch.sandbox ?? "none",
7361
7560
  ...launch.provider.bindAttempt ? { attemptState: true } : {},
7362
7561
  ...launch.provider.inheritsEnvironment === false ? { secretsFromLauncher: true } : {},
7562
+ ...launch.sandbox === "os" && launch.policy?.ok ? { sandboxPolicy: launch.policy.policy } : {},
7363
7563
  logLevel: opts.log.level,
7364
7564
  connectorHealth: healthSnapshot(launch.connectorHealth),
7365
7565
  sessionWriteNoticeClaimed: duties.sessionWriteNoticeClaimed()
@@ -7458,6 +7658,32 @@ async function settleUnstarted(opts, payload, turnId, errorReason, log) {
7458
7658
  );
7459
7659
  }
7460
7660
  }
7661
+ function sandboxInputs(launch, log) {
7662
+ const policy = launch.policy ?? { ok: true, policy: {} };
7663
+ if (launch.sandbox !== "os") {
7664
+ if (policy.ok && policyHasCredentials(policy.policy)) {
7665
+ log.debug(
7666
+ "runners: runners.policy names credentials, but turns here run without the OS sandbox; none are given to the harness"
7667
+ );
7668
+ }
7669
+ return { ok: true, credentials: [] };
7670
+ }
7671
+ if (!policy.ok) {
7672
+ log.warn({ error: policy.error }, "runners: the sandbox policy is invalid; refusing the turn");
7673
+ return { ok: false, reason: `setup_failed: ${policy.error}`.slice(0, 200) };
7674
+ }
7675
+ if (!policyHasCredentials(policy.policy)) return { ok: true, credentials: [] };
7676
+ const store = (launch.secretStore ?? defaultSecretStore)(log);
7677
+ const { credentials, missing } = resolvePolicyCredentials(policy.policy, store);
7678
+ if (missing.length) {
7679
+ log.debug(
7680
+ { missing },
7681
+ "runners: the sandbox policy names secrets this device does not declare"
7682
+ );
7683
+ return { ok: false, reason: missingSecretReason(missing) };
7684
+ }
7685
+ return { ok: true, credentials };
7686
+ }
7461
7687
  async function runTurnInRunner(opts, launch, duties, payload, handleOpts) {
7462
7688
  const startedAt = Date.now();
7463
7689
  const { turnId } = handleOpts;
@@ -7481,7 +7707,8 @@ async function runTurnInRunner(opts, launch, duties, payload, handleOpts) {
7481
7707
  agentId: payload.agentId,
7482
7708
  turnId
7483
7709
  };
7484
- const minted = await mintForRunner(opts, payload, handleOpts, controller.signal, log);
7710
+ const sandboxed = sandboxInputs(launch, log);
7711
+ const minted = sandboxed.ok ? await mintForRunner(opts, payload, handleOpts, controller.signal, log) : { ok: false, reason: sandboxed.reason, errorReason: sandboxed.reason };
7485
7712
  if (!minted.ok) {
7486
7713
  controller.signal.removeEventListener("abort", forwardStop);
7487
7714
  duties.releaseAbort(turnId, controller);
@@ -7534,6 +7761,7 @@ async function runTurnInRunner(opts, launch, duties, payload, handleOpts) {
7534
7761
  const input = {
7535
7762
  baseUrl: launch.runnerBaseUrl ?? opts.baseUrl,
7536
7763
  credential: minted.credential,
7764
+ sandboxCredentials: sandboxed.ok ? sandboxed.credentials : [],
7537
7765
  dispatch: payload,
7538
7766
  turnId,
7539
7767
  resumed: handleOpts.resumed,
@@ -7705,6 +7933,7 @@ var PINO_LEVELS;
7705
7933
  var init_launch = __esm({
7706
7934
  "src/runner/launch.ts"() {
7707
7935
  "use strict";
7936
+ init_sandbox_policy();
7708
7937
  init_opencode_lock();
7709
7938
  init_api_error_shape();
7710
7939
  init_errors();
@@ -7725,7 +7954,7 @@ var init_launch = __esm({
7725
7954
 
7726
7955
  // src/turn-control-tools.ts
7727
7956
  import { randomUUID as randomUUID2 } from "crypto";
7728
- import { z as z16 } from "zod";
7957
+ import { z as z17 } from "zod";
7729
7958
  function createSkipState() {
7730
7959
  return { skipped: false, reason: null };
7731
7960
  }
@@ -7776,7 +8005,7 @@ function createTurnControlMcpServer(acts, skipState) {
7776
8005
  SKIP_TURN_TOOL,
7777
8006
  TURN_CONTROL_TOOL_DESCRIPTIONS.skip_turn,
7778
8007
  {
7779
- reason: z16.string().min(1).max(MAX_SKIP_REASON_BYTES).describe(TURN_CONTROL_FIELD_DESCRIPTIONS.skip_turn.reason)
8008
+ reason: z17.string().min(1).max(MAX_SKIP_REASON_BYTES).describe(TURN_CONTROL_FIELD_DESCRIPTIONS.skip_turn.reason)
7780
8009
  },
7781
8010
  async (args) => {
7782
8011
  skipState.skipped = true;
@@ -7792,42 +8021,42 @@ function createTurnControlMcpServer(acts, skipState) {
7792
8021
  ASK_TOOL,
7793
8022
  TURN_CONTROL_TOOL_DESCRIPTIONS.ask,
7794
8023
  {
7795
- targetUserId: z16.string().uuid().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask.targetUserId),
7796
- question: z16.string().min(1).max(MAX_ASK_QUESTION_BYTES).optional().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask.question),
7797
- headline: z16.string().min(1).max(MAX_ASK_HEADLINE_BYTES).optional().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask.headline),
7798
- options: z16.array(
7799
- z16.union([
7800
- z16.string().min(1).max(MAX_ASK_OPTION_BYTES),
7801
- z16.object({
7802
- label: z16.string().min(1).max(MAX_ASK_OPTION_BYTES).describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask["options.label"]),
7803
- move: z16.object({
7804
- task: z16.string().min(1).max(200).describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask["options.move.task"]),
7805
- status: z16.enum(TASK_STAGES).describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask["options.move.status"])
8024
+ targetUserId: z17.string().uuid().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask.targetUserId),
8025
+ question: z17.string().min(1).max(MAX_ASK_QUESTION_BYTES).optional().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask.question),
8026
+ headline: z17.string().min(1).max(MAX_ASK_HEADLINE_BYTES).optional().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask.headline),
8027
+ options: z17.array(
8028
+ z17.union([
8029
+ z17.string().min(1).max(MAX_ASK_OPTION_BYTES),
8030
+ z17.object({
8031
+ label: z17.string().min(1).max(MAX_ASK_OPTION_BYTES).describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask["options.label"]),
8032
+ move: z17.object({
8033
+ task: z17.string().min(1).max(200).describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask["options.move.task"]),
8034
+ status: z17.enum(TASK_STAGES).describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask["options.move.status"])
7806
8035
  }).describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask["options.move"])
7807
8036
  })
7808
8037
  ])
7809
8038
  ).min(MIN_ASK_OPTIONS).max(MAX_ASK_OPTIONS).optional().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask.options),
7810
- questions: z16.array(
7811
- z16.object({
7812
- headline: z16.string().min(1).max(MAX_ASK_HEADLINE_BYTES).describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask["questions.headline"]),
7813
- body: z16.string().min(1).max(MAX_ASK_QUESTION_BYTES).optional().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask["questions.body"]),
7814
- options: z16.array(
7815
- z16.union([
7816
- z16.string().min(1).max(MAX_ASK_OPTION_BYTES),
7817
- z16.object({
7818
- label: z16.string().min(1).max(MAX_ASK_OPTION_BYTES).describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask["questions.options.label"]),
7819
- move: z16.object({
7820
- task: z16.string().min(1).max(200).describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask["questions.options.move.task"]),
7821
- status: z16.enum(TASK_STAGES).describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask["questions.options.move.status"])
8039
+ questions: z17.array(
8040
+ z17.object({
8041
+ headline: z17.string().min(1).max(MAX_ASK_HEADLINE_BYTES).describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask["questions.headline"]),
8042
+ body: z17.string().min(1).max(MAX_ASK_QUESTION_BYTES).optional().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask["questions.body"]),
8043
+ options: z17.array(
8044
+ z17.union([
8045
+ z17.string().min(1).max(MAX_ASK_OPTION_BYTES),
8046
+ z17.object({
8047
+ label: z17.string().min(1).max(MAX_ASK_OPTION_BYTES).describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask["questions.options.label"]),
8048
+ move: z17.object({
8049
+ task: z17.string().min(1).max(200).describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask["questions.options.move.task"]),
8050
+ status: z17.enum(TASK_STAGES).describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask["questions.options.move.status"])
7822
8051
  }).describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask["questions.options.move"])
7823
8052
  })
7824
8053
  ])
7825
8054
  ).min(MIN_ASK_OPTIONS).max(MAX_ASK_OPTIONS).optional().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask["questions.options"])
7826
8055
  })
7827
8056
  ).min(1).max(MAX_ASK_ITEMS).optional().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask.questions),
7828
- replaces: z16.object({
7829
- askId: z16.string().uuid().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask["replaces.askId"]),
7830
- reason: z16.string().min(1).max(MAX_WITHDRAW_REASON_BYTES).describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask["replaces.reason"])
8057
+ replaces: z17.object({
8058
+ askId: z17.string().uuid().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask["replaces.askId"]),
8059
+ reason: z17.string().min(1).max(MAX_WITHDRAW_REASON_BYTES).describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask["replaces.reason"])
7831
8060
  }).optional().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask.replaces)
7832
8061
  },
7833
8062
  async (args) => {
@@ -7868,9 +8097,9 @@ function createTurnControlMcpServer(acts, skipState) {
7868
8097
  WAKE_ME_TOOL,
7869
8098
  TURN_CONTROL_TOOL_DESCRIPTIONS.wake_me,
7870
8099
  {
7871
- afterSeconds: z16.number().int().positive().optional().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.wake_me.afterSeconds),
7872
- at: z16.string().datetime({ offset: true }).optional().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.wake_me.at),
7873
- note: z16.string().min(1).max(MAX_WAKE_NOTE_BYTES).describe(TURN_CONTROL_FIELD_DESCRIPTIONS.wake_me.note)
8100
+ afterSeconds: z17.number().int().positive().optional().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.wake_me.afterSeconds),
8101
+ at: z17.string().datetime({ offset: true }).optional().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.wake_me.at),
8102
+ note: z17.string().min(1).max(MAX_WAKE_NOTE_BYTES).describe(TURN_CONTROL_FIELD_DESCRIPTIONS.wake_me.note)
7874
8103
  },
7875
8104
  async (args) => {
7876
8105
  const hasAfter = args.afterSeconds !== void 0;
@@ -7904,8 +8133,8 @@ function createTurnControlMcpServer(acts, skipState) {
7904
8133
  WITHDRAW_ASK_TOOL,
7905
8134
  TURN_CONTROL_TOOL_DESCRIPTIONS.withdraw_ask,
7906
8135
  {
7907
- askId: z16.string().uuid().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.withdraw_ask.askId),
7908
- reason: z16.string().min(1).max(MAX_WITHDRAW_REASON_BYTES).describe(TURN_CONTROL_FIELD_DESCRIPTIONS.withdraw_ask.reason)
8136
+ askId: z17.string().uuid().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.withdraw_ask.askId),
8137
+ reason: z17.string().min(1).max(MAX_WITHDRAW_REASON_BYTES).describe(TURN_CONTROL_FIELD_DESCRIPTIONS.withdraw_ask.reason)
7909
8138
  },
7910
8139
  async (args) => performAct(acts, {
7911
8140
  kind: "withdraw_ask",
@@ -8498,16 +8727,6 @@ var init_turn_runtime_integrity = __esm({
8498
8727
  // src/turn-execution.ts
8499
8728
  import { createHash as createHash2, randomUUID as randomUUID3 } from "crypto";
8500
8729
  import { existsSync as existsSync9 } from "fs";
8501
- function missingSecretReason(missing) {
8502
- const prefix = "missing_secret: ";
8503
- for (let shown = missing.length; shown >= 0; shown--) {
8504
- const omitted = missing.length - shown;
8505
- const parts = [...missing.slice(0, shown), ...omitted > 0 ? [`+${omitted}`] : []];
8506
- const reason = prefix + parts.join(",");
8507
- if (reason.length <= ERROR_REASON_MAX) return reason;
8508
- }
8509
- return `${prefix}+${missing.length}`;
8510
- }
8511
8730
  function resolveIdleTimeoutMs(override) {
8512
8731
  return override ?? DEFAULT_AGENT_IDLE_TIMEOUT_MS;
8513
8732
  }
@@ -8544,7 +8763,7 @@ function fingerprintSessionState(state) {
8544
8763
  }
8545
8764
  return createHash2("sha256").update(opaqueId).digest("hex").slice(0, 16);
8546
8765
  }
8547
- var PREPARING_TOOL_NAME, ERROR_REASON_MAX, RUNTIME_UNAVAILABLE_PREFIX, NO_TURN_TOKEN_REASON, UNEXPECTED_ROLE_REASON, DEFAULT_PREPARING_ROW_DELAY_MS, DEFAULT_AGENT_IDLE_TIMEOUT_MS, DEFAULT_AGENT_TOTAL_TIMEOUT_MS, DEFAULT_LEASE_RENEWAL_MS, TurnConcluded, TurnExecution;
8766
+ var PREPARING_TOOL_NAME, RUNTIME_UNAVAILABLE_PREFIX, NO_TURN_TOKEN_REASON, UNEXPECTED_ROLE_REASON, DEFAULT_PREPARING_ROW_DELAY_MS, DEFAULT_AGENT_IDLE_TIMEOUT_MS, DEFAULT_AGENT_TOTAL_TIMEOUT_MS, DEFAULT_LEASE_RENEWAL_MS, TurnConcluded, TurnExecution;
8548
8767
  var init_turn_execution = __esm({
8549
8768
  "src/turn-execution.ts"() {
8550
8769
  "use strict";
@@ -8568,8 +8787,8 @@ var init_turn_execution = __esm({
8568
8787
  init_turn_committer();
8569
8788
  init_turn_runtime_integrity();
8570
8789
  init_dispatcher();
8790
+ init_sandbox_policy();
8571
8791
  PREPARING_TOOL_NAME = "preparing";
8572
- ERROR_REASON_MAX = 200;
8573
8792
  RUNTIME_UNAVAILABLE_PREFIX = "**This agent's runtime isn't available on this companion.** The model this agent uses needs a runtime this device isn't running, so I can't run this turn here. Details:";
8574
8793
  NO_TURN_TOKEN_REASON = "no_turn_token: the server issued no turn credential for this turn";
8575
8794
  UNEXPECTED_ROLE_REASON = `refused the wake trigger (role "system" isn't dispatchable on this companion) \u2014 the companion is likely running outdated code; refresh it, then re-address the agent`;
@@ -9133,7 +9352,18 @@ var init_turn_execution = __esm({
9133
9352
  const { payload, workspaceId, turnId, turnLog } = this;
9134
9353
  await guardBundledBinary(this.integrityContext(), (r, e) => this.concluded(r, e), this.outcome);
9135
9354
  const onWarn = (msg, meta) => turnLog.debug(meta ?? {}, msg);
9136
- const sandbox = this.opts.harnessSandbox ? await this.opts.harnessSandbox({ cwd: this.effectiveCwd, log: turnLog }) : null;
9355
+ let sandbox = null;
9356
+ try {
9357
+ sandbox = this.opts.harnessSandbox ? await this.opts.harnessSandbox({
9358
+ cwd: this.effectiveCwd,
9359
+ runtime: this.turnContext.runtime,
9360
+ log: turnLog
9361
+ }) : null;
9362
+ } catch (err) {
9363
+ if (!(err instanceof HarnessSandboxRefused)) throw err;
9364
+ const reason = `setup_failed: ${err.message}`.slice(0, 200);
9365
+ throw this.concluded(reason, reason);
9366
+ }
9137
9367
  const adapters = [];
9138
9368
  if (this.opts.claudeCodeAvailable?.() ?? true) {
9139
9369
  this.containment = createTurnContainment(this.turnId, {
@@ -10009,10 +10239,20 @@ var init_git_credential = __esm({
10009
10239
 
10010
10240
  // src/runner/harness-sandbox.ts
10011
10241
  import { lookup } from "dns/promises";
10012
- import { chmodSync as chmodSync6, mkdirSync as mkdirSync11, mkdtempSync, rmSync as rmSync10, writeFileSync as writeFileSync7 } from "fs";
10242
+ import {
10243
+ chmodSync as chmodSync6,
10244
+ mkdirSync as mkdirSync11,
10245
+ mkdtempSync,
10246
+ readFileSync as readFileSync10,
10247
+ realpathSync as realpathSync3,
10248
+ rmSync as rmSync10,
10249
+ writeFileSync as writeFileSync7
10250
+ } from "fs";
10251
+ import { createRequire as createRequire2 } from "module";
10013
10252
  import { isIP } from "net";
10014
10253
  import { homedir as homedir3, networkInterfaces, tmpdir as tmpdir3, userInfo as userInfo2 } from "os";
10015
- import { join as join18 } from "path";
10254
+ import { dirname as dirname8, join as join18 } from "path";
10255
+ import { fileURLToPath as fileURLToPath4 } from "url";
10016
10256
  import { SandboxManager } from "@anthropic-ai/sandbox-runtime";
10017
10257
  function accountHome() {
10018
10258
  try {
@@ -10042,32 +10282,117 @@ async function resolvedAddresses(baseUrl) {
10042
10282
  return [];
10043
10283
  }
10044
10284
  }
10285
+ function companionPackageRoot(modulePath) {
10286
+ for (let dir2 = dirname8(modulePath); ; dir2 = dirname8(dir2)) {
10287
+ try {
10288
+ const manifest = JSON.parse(readFileSync10(join18(dir2, "package.json"), "utf8"));
10289
+ if (manifest.name === "@cabane/companion") return dir2;
10290
+ } catch {
10291
+ }
10292
+ if (dirname8(dir2) === dir2) return null;
10293
+ }
10294
+ }
10295
+ function harnessRuntimePaths() {
10296
+ const paths = [dirname8(dirname8(process.execPath))];
10297
+ const root = companionPackageRoot(fileURLToPath4(import.meta.url));
10298
+ if (root) paths.push(root);
10299
+ try {
10300
+ const require2 = createRequire2(import.meta.url);
10301
+ paths.push(
10302
+ dirname8(realpathSync3(require2.resolve("@anthropic-ai/sandbox-runtime/package.json")))
10303
+ );
10304
+ } catch {
10305
+ }
10306
+ for (const runtime of ["claude-code", "codex"]) {
10307
+ const bundled = resolveBundledBinary(runtime).path;
10308
+ if (bundled) {
10309
+ try {
10310
+ paths.push(dirname8(realpathSync3(bundled)));
10311
+ } catch {
10312
+ }
10313
+ }
10314
+ }
10315
+ return [...new Set(paths)];
10316
+ }
10045
10317
  function ownAddresses() {
10046
10318
  return Object.values(networkInterfaces()).flat().flatMap((i) => i ? [i.address] : []);
10047
10319
  }
10320
+ function expandHome(path, home) {
10321
+ if (path === "~") return home;
10322
+ return path.startsWith("~/") ? join18(home, path.slice(2)) : path;
10323
+ }
10324
+ function stripPort(pattern) {
10325
+ return pattern.startsWith("[") ? pattern.replace(/^\[([^\]]+)\](:\d+)?$/, "$1") : pattern.replace(/:\d+$/, "");
10326
+ }
10327
+ function hostMatches(host, pattern) {
10328
+ return pattern.startsWith("*.") ? host.endsWith(pattern.slice(1)) : host === pattern;
10329
+ }
10330
+ function patternsOverlap(a, b) {
10331
+ if (a === b) return true;
10332
+ const bare = (p) => p.startsWith("*.") ? p.slice(1) : p;
10333
+ if (a.startsWith("*.") && b.startsWith("*.")) {
10334
+ return bare(a).endsWith(bare(b)) || bare(b).endsWith(bare(a));
10335
+ }
10336
+ return hostMatches(a, b) || hostMatches(b, a);
10337
+ }
10048
10338
  function harnessSandboxConfig(input) {
10339
+ const { policy } = input;
10049
10340
  const claudeDir = input.claudeConfigDir ?? join18(input.home, ".claude");
10050
10341
  const codexDir = input.codexHome ?? join18(input.home, ".codex");
10342
+ const homes = [.../* @__PURE__ */ new Set([input.home, input.accountHome ?? input.home])];
10343
+ const expand = (paths) => (paths ?? []).map((p) => expandHome(p, input.home));
10344
+ const cabane = cabaneHostEntries(input.baseUrl, input.resolved, input.ownAddresses);
10345
+ const models = modelHostsFor(input.runtime);
10346
+ const reachable = [
10347
+ ...policy.hosts?.defaults === false ? [] : DEFAULT_ALLOWED_HOSTS,
10348
+ ...policy.hosts?.allow ?? []
10349
+ ];
10350
+ const allowedDomains = [.../* @__PURE__ */ new Set([...cabane, ...models, ...reachable])];
10351
+ const envGlobs = input.cwd ? [join18(input.cwd, "**/.env"), join18(input.cwd, "**/.env.*")] : [];
10352
+ const secrets = [
10353
+ input.cabaneDir,
10354
+ ...homes.flatMap((home) => SECRET_PATHS_UNDER_HOME.map((p) => join18(home, p))),
10355
+ ...envGlobs,
10356
+ ...expand(policy.filesystem?.denyRead)
10357
+ ];
10358
+ const strict = policy.filesystem?.mode === "strict";
10359
+ const denyRead = strict ? [...homes, ...secrets] : secrets;
10360
+ const allowRead = strict ? [
10361
+ ...input.cwd ? [input.cwd] : [],
10362
+ claudeDir,
10363
+ join18(input.home, ".claude.json"),
10364
+ codexDir,
10365
+ ...input.runtimePaths ?? [],
10366
+ ...expand(policy.filesystem?.allowRead)
10367
+ ] : expand(policy.filesystem?.allowRead);
10368
+ const masked = input.credentials.map((c) => ({
10369
+ name: c.name,
10370
+ mode: "mask",
10371
+ injectHosts: [...new Set(c.hosts.map(stripPort))]
10372
+ }));
10373
+ const injectAt = masked.flatMap((c) => c.injectHosts);
10051
10374
  return {
10052
10375
  network: {
10053
- allowedDomains: [
10054
- ...cabaneHostEntries(input.baseUrl, input.resolved, input.ownAddresses),
10055
- ...MODEL_AND_GIT_HOSTS
10056
- ],
10057
- deniedDomains: [],
10058
- allowLocalBinding: true
10376
+ allowedDomains,
10377
+ deniedDomains: policy.hosts?.deny ?? [],
10378
+ allowLocalBinding: true,
10379
+ // A masked credential is swapped in by a proxy that reads the request,
10380
+ // so TLS is terminated — but only for the hosts a credential goes to.
10381
+ // Cabane, the model provider and every other host keep their own
10382
+ // end-to-end TLS through an opaque tunnel.
10383
+ ...masked.length ? {
10384
+ tlsTerminate: {
10385
+ excludeDomains: [
10386
+ ...new Set(
10387
+ allowedDomains.map(stripPort).filter((host) => !injectAt.some((p) => patternsOverlap(host, p)))
10388
+ )
10389
+ ]
10390
+ }
10391
+ } : {}
10059
10392
  },
10060
10393
  filesystem: {
10061
- // The device's own state (its token, the agents' PATs, the secret store)
10062
- // and the account's keys, under whichever homes this process has.
10063
- denyRead: [
10064
- input.cabaneDir,
10065
- ...[.../* @__PURE__ */ new Set([input.home, input.accountHome ?? input.home])].flatMap((home) => [
10066
- join18(home, ".ssh"),
10067
- join18(home, ".aws"),
10068
- join18(home, ".gnupg")
10069
- ])
10070
- ],
10394
+ denyRead,
10395
+ ...allowRead.length ? { allowRead } : {},
10071
10396
  // The checkout, scratch, and the harnesses' own session state — a turn
10072
10397
  // resumes from `~/.claude` and `~/.codex`, and without write there every
10073
10398
  // resume starts from nothing.
@@ -10076,12 +10401,27 @@ function harnessSandboxConfig(input) {
10076
10401
  input.tmp,
10077
10402
  claudeDir,
10078
10403
  join18(input.home, ".claude.json"),
10079
- codexDir
10404
+ codexDir,
10405
+ ...expand(policy.filesystem?.allowWrite)
10080
10406
  ],
10081
10407
  denyWrite: []
10082
- }
10408
+ },
10409
+ ...masked.length ? { credentials: { envVars: masked } } : {}
10083
10410
  };
10084
10411
  }
10412
+ function gitCredentialExports(credentials) {
10413
+ const git = credentials.filter((c) => c.gitHost);
10414
+ if (!git.length) return "";
10415
+ const lines2 = ["n=${GIT_CONFIG_COUNT:-0}"];
10416
+ for (const c of git) {
10417
+ lines2.push(
10418
+ `export "GIT_CONFIG_KEY_$n=http.https://${c.gitHost}/.extraHeader" "GIT_CONFIG_VALUE_$n=Authorization: Basic $${c.name}"`,
10419
+ "n=$((n+1))"
10420
+ );
10421
+ }
10422
+ lines2.push("export GIT_CONFIG_COUNT=$n");
10423
+ return lines2.join("; ") + "; ";
10424
+ }
10085
10425
  function osSandboxUsable(log) {
10086
10426
  if (!SandboxManager.isSupportedPlatform()) {
10087
10427
  log.warn(
@@ -10102,7 +10442,9 @@ function osSandboxUsable(log) {
10102
10442
  }
10103
10443
  async function prepareHarnessSandbox(input) {
10104
10444
  let wrapped;
10445
+ const held = input.credentials.map((c) => c.name);
10105
10446
  try {
10447
+ for (const c of input.credentials) process.env[c.name] = c.value;
10106
10448
  const config = harnessSandboxConfig({
10107
10449
  cwd: input.cwd,
10108
10450
  baseUrl: input.baseUrl,
@@ -10113,28 +10455,50 @@ async function prepareHarnessSandbox(input) {
10113
10455
  tmp: tmpdir3(),
10114
10456
  cabaneDir: cabaneDir(),
10115
10457
  claudeConfigDir: process.env.CLAUDE_CONFIG_DIR,
10116
- codexHome: process.env.CODEX_HOME
10458
+ codexHome: process.env.CODEX_HOME,
10459
+ runtime: input.runtime,
10460
+ policy: input.policy,
10461
+ credentials: input.credentials,
10462
+ runtimePaths: harnessRuntimePaths()
10117
10463
  });
10118
10464
  await SandboxManager.initialize(config);
10465
+ if (input.policy.filesystem?.mode === "strict") {
10466
+ input.log.info(
10467
+ { readable: config.filesystem.allowRead },
10468
+ "runners: strict sandbox \u2014 the home is unreadable except these paths; a path missing inside a turn and not listed here is hidden by the policy (add it to runners.policy.filesystem.allowRead)"
10469
+ );
10470
+ }
10119
10471
  input.log.debug(
10120
10472
  {
10121
10473
  allowWrite: config.filesystem.allowWrite,
10122
10474
  denyRead: config.filesystem.denyRead,
10123
- hosts: config.network.allowedDomains.length
10475
+ allowRead: config.filesystem.allowRead,
10476
+ hosts: config.network.allowedDomains,
10477
+ masked: held
10124
10478
  },
10125
10479
  "runners: harness sandbox ready"
10126
10480
  );
10127
10481
  wrapped = await SandboxManager.wrapWithSandbox(
10128
- 'export NO_PROXY= no_proxy=; eval "exec $CABANE_SANDBOXED_ARGV"'
10482
+ "export NO_PROXY= no_proxy=; " + gitCredentialExports(input.credentials) + 'eval "exec $CABANE_SANDBOXED_ARGV"'
10129
10483
  );
10130
10484
  } catch (err) {
10485
+ const message = err instanceof Error ? err.message : String(err);
10486
+ await SandboxManager.reset().catch(() => {
10487
+ });
10488
+ if (held.length || Object.keys(input.policy).length) {
10489
+ input.log.warn(
10490
+ { err: message, masked: held },
10491
+ "runners: the OS sandbox could not start under this device's runners.policy; refusing the turn rather than running it unsandboxed"
10492
+ );
10493
+ throw new HarnessSandboxRefused(`the OS sandbox could not start: ${message}`);
10494
+ }
10131
10495
  input.log.warn(
10132
- { err: err instanceof Error ? err.message : String(err) },
10496
+ { err: message },
10133
10497
  "runners: the OS sandbox could not start; running the turn unsandboxed"
10134
10498
  );
10135
- await SandboxManager.reset().catch(() => {
10136
- });
10137
10499
  return null;
10500
+ } finally {
10501
+ for (const name of held) delete process.env[name];
10138
10502
  }
10139
10503
  mkdirSync11(join18(cabaneDir(), "sandbox"), { recursive: true });
10140
10504
  const dir2 = mkdtempSync(join18(cabaneDir(), "sandbox", "turn-"));
@@ -10167,23 +10531,28 @@ exec ${shellQuote(script)} ${shellQuote(real)} "$@"
10167
10531
  }
10168
10532
  };
10169
10533
  }
10170
- var MODEL_AND_GIT_HOSTS;
10534
+ var SECRET_PATHS_UNDER_HOME;
10171
10535
  var init_harness_sandbox = __esm({
10172
10536
  "src/runner/harness-sandbox.ts"() {
10173
10537
  "use strict";
10538
+ init_bundled_binaries();
10174
10539
  init_config();
10175
- MODEL_AND_GIT_HOSTS = [
10176
- "api.anthropic.com",
10177
- "*.anthropic.com",
10178
- "claude.ai",
10179
- "*.claude.ai",
10180
- "api.openai.com",
10181
- "auth.openai.com",
10182
- "chatgpt.com",
10183
- "*.chatgpt.com",
10184
- "github.com",
10185
- "*.github.com",
10186
- "*.githubusercontent.com"
10540
+ init_sandbox_policy();
10541
+ SECRET_PATHS_UNDER_HOME = [
10542
+ ".ssh",
10543
+ ".aws",
10544
+ ".gnupg",
10545
+ ".config/gh",
10546
+ ".git-credentials",
10547
+ ".netrc",
10548
+ ".npmrc",
10549
+ ".pypirc",
10550
+ ".docker/config.json",
10551
+ ".kube",
10552
+ ".config/gcloud",
10553
+ ".azure",
10554
+ ".config/op",
10555
+ ".password-store"
10187
10556
  ];
10188
10557
  }
10189
10558
  });
@@ -10193,7 +10562,7 @@ import {
10193
10562
  existsSync as existsSync14,
10194
10563
  mkdirSync as mkdirSync12,
10195
10564
  readdirSync as readdirSync5,
10196
- readFileSync as readFileSync10,
10565
+ readFileSync as readFileSync11,
10197
10566
  renameSync as renameSync6,
10198
10567
  rmSync as rmSync11,
10199
10568
  writeFileSync as writeFileSync8
@@ -10263,7 +10632,7 @@ var init_outbox = __esm({
10263
10632
  if (!name.endsWith(".json")) continue;
10264
10633
  const full = join19(dir2, name);
10265
10634
  try {
10266
- const parsed = JSON.parse(readFileSync10(full, "utf8"));
10635
+ const parsed = JSON.parse(readFileSync11(full, "utf8"));
10267
10636
  if (parsed && typeof parsed.turnId === "string" && typeof parsed.seq === "number" && typeof parsed.path === "string") {
10268
10637
  entries.push(parsed);
10269
10638
  } else {
@@ -10367,12 +10736,12 @@ var init_outbox = __esm({
10367
10736
  });
10368
10737
 
10369
10738
  // src/version.ts
10370
- import { createRequire as createRequire2 } from "module";
10739
+ import { createRequire as createRequire3 } from "module";
10371
10740
  var pkg, COMPANION_VERSION;
10372
10741
  var init_version2 = __esm({
10373
10742
  "src/version.ts"() {
10374
10743
  "use strict";
10375
- pkg = createRequire2(import.meta.url)("../package.json");
10744
+ pkg = createRequire3(import.meta.url)("../package.json");
10376
10745
  COMPANION_VERSION = pkg.version;
10377
10746
  }
10378
10747
  });
@@ -10609,10 +10978,13 @@ function runnerDispatcherOptions(input, log, emit, extras = {}) {
10609
10978
  }
10610
10979
  },
10611
10980
  ...spec.sandbox === "os" ? {
10612
- harnessSandbox: async ({ cwd, log: turnLog }) => {
10981
+ harnessSandbox: async ({ cwd, runtime, log: turnLog }) => {
10613
10982
  const sandbox = await prepareHarnessSandbox({
10614
10983
  cwd,
10615
10984
  baseUrl: input.baseUrl,
10985
+ runtime,
10986
+ policy: spec.sandboxPolicy ?? {},
10987
+ credentials: input.sandboxCredentials ?? [],
10616
10988
  log: turnLog
10617
10989
  });
10618
10990
  if (sandbox) onSandbox?.(sandbox);
@@ -11886,7 +12258,7 @@ async function pair(opts = {}) {
11886
12258
  }
11887
12259
 
11888
12260
  // src/cli.ts
11889
- import { readFileSync as readFileSync13 } from "fs";
12261
+ import { readFileSync as readFileSync14 } from "fs";
11890
12262
 
11891
12263
  // src/commands/logs.ts
11892
12264
  import { once } from "events";
@@ -13438,6 +13810,15 @@ var UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
13438
13810
  function containerName(attempt) {
13439
13811
  return `cabane-runner-${attempt.conversationId}-${attempt.agentId}`;
13440
13812
  }
13813
+ var networkName = containerName;
13814
+ function networkCreateError(stderr) {
13815
+ const said = stderr.trim();
13816
+ if (/address pool|fully subnetted/i.test(said))
13817
+ return new Error(
13818
+ `docker: couldn't create this conversation's runner network: the engine has no address pool left for another network. Each conversation and agent with a runner container holds one; remove kept runner containers, or widen \`default-address-pools\` in the daemon.json. Docker said: ${said}`
13819
+ );
13820
+ return new Error(`docker: creating the runner network failed: ${said || "no reason given"}`);
13821
+ }
13441
13822
  function sessionArchivePath(deviceDir, attemptId) {
13442
13823
  if (!UUID_RE.test(attemptId)) throw new Error(`docker: not an attempt id: ${attemptId}`);
13443
13824
  return join17(deviceDir, "attempts", attemptId, "session.tar");
@@ -13514,7 +13895,7 @@ function createDockerProvider(opts) {
13514
13895
  const killGraceMs = opts.killGraceMs ?? DEFAULT_KILL_GRACE_MS2;
13515
13896
  const sessionCap = opts.sessionCapBytes ?? DEFAULT_SESSION_CAP_BYTES;
13516
13897
  const collectCap = opts.collectCapBytes ?? COLLECT_CAP_BYTES;
13517
- const network = `cabane-runners-${opts.deviceKey}`;
13898
+ const sharedNetwork = `cabane-runners-${opts.deviceKey}`;
13518
13899
  const companionLabel = createHash4("sha256").update(opts.companion.key).digest("hex").slice(0, 16);
13519
13900
  const proxy = `cabane-egress-${opts.deviceKey}`;
13520
13901
  const held = /* @__PURE__ */ new Map();
@@ -13547,32 +13928,14 @@ function createDockerProvider(opts) {
13547
13928
  }
13548
13929
  throw new Error("docker: the egress proxy never started listening");
13549
13930
  };
13550
- let networkReady = null;
13931
+ let proxyReady = null;
13551
13932
  const proxyKey = async () => {
13552
13933
  const id = (await docker.run(["image", "inspect", "-f", "{{.Id}}", opts.image])).stdout.trim();
13553
13934
  return createHash4("sha256").update(JSON.stringify([id || opts.image, opts.companion.key, [...opts.egressAllow].sort()])).digest("hex").slice(0, 16);
13554
13935
  };
13555
- const ensureNetwork = () => {
13556
- networkReady ??= (async () => {
13936
+ const ensureProxy = () => {
13937
+ proxyReady ??= (async () => {
13557
13938
  const allowKey = await proxyKey();
13558
- if ((await docker.run(["network", "inspect", network])).code !== 0) {
13559
- await must(
13560
- [
13561
- "network",
13562
- "create",
13563
- // No route off the host, and no address of the host's on the
13564
- // bridge: the host's own ports are out of reach too. The proxy is
13565
- // the only other thing on the network.
13566
- "--internal",
13567
- "-o",
13568
- "com.docker.network.bridge.inhibit_ipv4=true",
13569
- "--label",
13570
- `cabane.device=${opts.deviceKey}`,
13571
- network
13572
- ],
13573
- "creating the runners network"
13574
- );
13575
- }
13576
13939
  const seen = await docker.run([
13577
13940
  "inspect",
13578
13941
  "-f",
@@ -13581,6 +13944,7 @@ function createDockerProvider(opts) {
13581
13944
  ]);
13582
13945
  const [key, running] = seen.stdout.trim().split(" ");
13583
13946
  if (seen.code === 0 && key === allowKey) {
13947
+ await retireSharedNetwork();
13584
13948
  if (running !== "true") {
13585
13949
  await must(["start", proxy], "starting the egress proxy");
13586
13950
  await proxyListening();
@@ -13615,17 +13979,61 @@ function createDockerProvider(opts) {
13615
13979
  ],
13616
13980
  "starting the egress proxy"
13617
13981
  );
13618
- await must(
13619
- ["network", "connect", "--alias", PROXY_ALIAS, network, proxy],
13620
- "connecting the proxy"
13621
- );
13982
+ await retireSharedNetwork();
13622
13983
  await proxyListening();
13623
- log.info({ network, proxy, allow: opts.egressAllow }, "runners: egress proxy ready");
13984
+ log.info({ proxy, allow: opts.egressAllow }, "runners: egress proxy ready");
13624
13985
  })().catch((err) => {
13625
- networkReady = null;
13986
+ proxyReady = null;
13626
13987
  throw err;
13627
13988
  });
13628
- return networkReady;
13989
+ return proxyReady;
13990
+ };
13991
+ const retireSharedNetwork = async () => {
13992
+ if ((await docker.run(["network", "inspect", sharedNetwork])).code !== 0) return;
13993
+ await docker.run(["network", "disconnect", "-f", sharedNetwork, proxy]);
13994
+ const on = await docker.run(["ps", "-aq", "--filter", `network=${sharedNetwork}`]);
13995
+ if (on.code === 0 && !on.stdout.trim()) await docker.run(["network", "rm", sharedNetwork]);
13996
+ };
13997
+ const networksOf = async (container) => (await docker.run([
13998
+ "inspect",
13999
+ "-f",
14000
+ "{{range $k, $_ := .NetworkSettings.Networks}}{{$k}} {{end}}",
14001
+ container
14002
+ ])).stdout.split(/\s+/).filter(Boolean);
14003
+ const ensurePairNetwork = async (net, attempt) => {
14004
+ if ((await docker.run(["network", "inspect", net])).code !== 0) {
14005
+ const made = await docker.run([
14006
+ "network",
14007
+ "create",
14008
+ // No route off the host, and no address of the host's on the bridge:
14009
+ // the host's own ports are out of reach too. The proxy is the only
14010
+ // other thing on the network.
14011
+ "--internal",
14012
+ "-o",
14013
+ "com.docker.network.bridge.inhibit_ipv4=true",
14014
+ "--label",
14015
+ `cabane.device=${opts.deviceKey}`,
14016
+ "--label",
14017
+ `cabane.conversation=${attempt.conversationId}`,
14018
+ "--label",
14019
+ `cabane.agent=${attempt.agentId}`,
14020
+ net
14021
+ ]);
14022
+ if (made.code !== 0) throw networkCreateError(made.stderr);
14023
+ }
14024
+ if (!(await networksOf(proxy)).includes(net))
14025
+ await must(
14026
+ ["network", "connect", "--alias", PROXY_ALIAS, net, proxy],
14027
+ "connecting the proxy"
14028
+ );
14029
+ };
14030
+ const settleNetworks = async (name, net) => {
14031
+ const on = await networksOf(name);
14032
+ if (!on.includes(net)) await must(["network", "connect", net, name], "moving the container");
14033
+ const others = on.filter((n) => n !== net);
14034
+ for (const other of others)
14035
+ await must(["network", "disconnect", "-f", other, name], "moving the container");
14036
+ if (others.includes(sharedNetwork)) await retireSharedNetwork();
13629
14037
  };
13630
14038
  const socketRoot = opts.socketRoot ?? join17(
13631
14039
  process.env.XDG_RUNTIME_DIR && existsSync13(process.env.XDG_RUNTIME_DIR) ? process.env.XDG_RUNTIME_DIR : tmpdir2(),
@@ -13657,7 +14065,7 @@ function createDockerProvider(opts) {
13657
14065
  "--label",
13658
14066
  `cabane.companion=${companionLabel}`,
13659
14067
  "--network",
13660
- network,
14068
+ networkName(attempt),
13661
14069
  "--mount",
13662
14070
  `type=volume,src=${name}-home,dst=${CONTAINER_HOME}`,
13663
14071
  "--mount",
@@ -13683,6 +14091,17 @@ function createDockerProvider(opts) {
13683
14091
  await docker.run(["rm", "-f", "-v", name]);
13684
14092
  await docker.run(["volume", "rm", "-f", `${name}-home`, `${name}-work`]);
13685
14093
  rmSync9(socketDir(name), { recursive: true, force: true });
14094
+ await dropNetwork(name);
14095
+ };
14096
+ const dropNetwork = async (name) => {
14097
+ if ((await docker.run(["network", "inspect", name])).code !== 0) return;
14098
+ await docker.run(["network", "disconnect", "-f", name, proxy]);
14099
+ const gone = await docker.run(["network", "rm", name]);
14100
+ if (gone.code !== 0)
14101
+ log.warn(
14102
+ { network: name, err: gone.stderr.trim() },
14103
+ "runners: couldn't remove a pair's network"
14104
+ );
13686
14105
  };
13687
14106
  const containerEnv = () => {
13688
14107
  const proxyUrl = `http://${PROXY_ALIAS}:${PROXY_PORT}`;
@@ -13849,7 +14268,7 @@ function createDockerProvider(opts) {
13849
14268
  });
13850
14269
  await before;
13851
14270
  try {
13852
- await ensureNetwork();
14271
+ await ensureProxy();
13853
14272
  mkdirSync10(socketRoot, { recursive: true, mode: 448 });
13854
14273
  mkdirSync10(socketDir(name), { recursive: true, mode: 457 });
13855
14274
  let seen = await docker.run([
@@ -13879,9 +14298,12 @@ function createDockerProvider(opts) {
13879
14298
  );
13880
14299
  seen = { code: 1, stdout: "", stderr: "" };
13881
14300
  }
14301
+ await ensurePairNetwork(networkName(attempt), attempt);
13882
14302
  if (seen.code !== 0) {
13883
14303
  await create(name, attempt);
13884
14304
  log.debug({ container: name }, "runners: container created");
14305
+ } else {
14306
+ await settleNetworks(name, networkName(attempt));
13885
14307
  }
13886
14308
  if (seen.stdout.trim().split(" ")[0] !== "true")
13887
14309
  await must(["start", name], "starting the container");
@@ -13895,7 +14317,8 @@ function createDockerProvider(opts) {
13895
14317
  return { provider: "docker", id: name };
13896
14318
  } catch (err) {
13897
14319
  const keep = opts.keepContainers || readPending(name).length > 0;
13898
- await (keep ? docker.run(["stop", "-t", "1", name]) : destroy(name)).catch(() => {
14320
+ const kept = keep && (await docker.run(["inspect", "-f", "{{.Id}}", name])).code === 0;
14321
+ await (kept ? docker.run(["stop", "-t", "1", name]) : keep ? dropNetwork(networkName(attempt)) : destroy(name)).catch(() => {
13899
14322
  });
13900
14323
  release();
13901
14324
  throw err;
@@ -14287,6 +14710,7 @@ var DeviceLocks = class {
14287
14710
 
14288
14711
  // src/supervisor.ts
14289
14712
  init_harness_sandbox();
14713
+ init_sandbox_policy();
14290
14714
  init_launch();
14291
14715
 
14292
14716
  // src/opencode-models.ts
@@ -14629,6 +15053,13 @@ var CompanionSupervisor = class {
14629
15053
  }
14630
15054
  this.latchBootBundledState();
14631
15055
  }
15056
+ if (this.runnerSandbox() === "os") {
15057
+ const policy = this.sandboxPolicy();
15058
+ const posture = postureLines(policy);
15059
+ const summary = posture.join("; ");
15060
+ if (policy.ok) this.log.info({ posture }, `runners: OS sandbox policy \u2014 ${summary}`);
15061
+ else this.log.error({ posture }, `runners: ${summary}`);
15062
+ }
14632
15063
  const startupHarnessRefresh = this.refreshHarnessStatuses();
14633
15064
  this.trackHeartbeat(startupHarnessRefresh);
14634
15065
  if (!this.config.deviceToken) {
@@ -15070,6 +15501,13 @@ var CompanionSupervisor = class {
15070
15501
  // CT1679: `runners.sandbox`, checked against this host once. A host that
15071
15502
  // can't make the sandbox says so once and runs its turns unwrapped.
15072
15503
  sandboxUsable;
15504
+ // CT1684: `runners.policy`, parsed once. An invalid one is kept as the
15505
+ // error, which every sandboxed turn is refused with.
15506
+ parsedSandboxPolicy;
15507
+ sandboxPolicy() {
15508
+ this.parsedSandboxPolicy ??= parseSandboxPolicy(this.config.runners?.policy);
15509
+ return this.parsedSandboxPolicy;
15510
+ }
15073
15511
  runnerSandbox() {
15074
15512
  if (this.config.runners?.sandbox !== "os" || this.dockerRunners()) return "none";
15075
15513
  this.sandboxUsable ??= osSandboxUsable(this.log);
@@ -15189,7 +15627,8 @@ var CompanionSupervisor = class {
15189
15627
  reportRunnerGone: (report) => this.reportRunnerGone(report),
15190
15628
  connectorHealth: this.connectorHealth,
15191
15629
  locks: this.runnerLocks,
15192
- sandbox: this.runnerSandbox()
15630
+ sandbox: this.runnerSandbox(),
15631
+ policy: this.sandboxPolicy()
15193
15632
  }
15194
15633
  } : {}
15195
15634
  });
@@ -15890,7 +16329,7 @@ function handleUncaught(log, err, origin) {
15890
16329
 
15891
16330
  // src/crash-marker.ts
15892
16331
  init_config();
15893
- import { existsSync as existsSync15, mkdirSync as mkdirSync13, readFileSync as readFileSync11, rmSync as rmSync12, writeFileSync as writeFileSync9 } from "fs";
16332
+ import { existsSync as existsSync15, mkdirSync as mkdirSync13, readFileSync as readFileSync12, rmSync as rmSync12, writeFileSync as writeFileSync9 } from "fs";
15894
16333
  import { join as join20 } from "path";
15895
16334
  function crashMarkerPath() {
15896
16335
  return join20(cabaneDir(), "last-error.json");
@@ -16083,11 +16522,11 @@ import { closeSync as closeSync3, mkdirSync as mkdirSync14, openSync as openSync
16083
16522
 
16084
16523
  // src/cli-entry.ts
16085
16524
  import { existsSync as existsSync16 } from "fs";
16086
- import { fileURLToPath as fileURLToPath4 } from "url";
16525
+ import { fileURLToPath as fileURLToPath5 } from "url";
16087
16526
  var RELATIVE_CANDIDATES = ["./cli.js", "../dist/cli.js", "../cli.js"];
16088
16527
  function companionCliEntry(deps = {}) {
16089
16528
  const exists = deps.exists ?? existsSync16;
16090
- const candidates = deps.candidates ?? RELATIVE_CANDIDATES.map((rel) => fileURLToPath4(new URL(rel, import.meta.url)));
16529
+ const candidates = deps.candidates ?? RELATIVE_CANDIDATES.map((rel) => fileURLToPath5(new URL(rel, import.meta.url)));
16091
16530
  for (const candidate of candidates) {
16092
16531
  if (exists(candidate)) return candidate;
16093
16532
  }
@@ -16481,6 +16920,7 @@ async function runAttached(runtime) {
16481
16920
  // src/commands/status.ts
16482
16921
  init_config();
16483
16922
  import { readdirSync as readdirSync6 } from "fs";
16923
+ init_sandbox_policy();
16484
16924
  init_secrets();
16485
16925
  init_transcript_writer();
16486
16926
  async function status() {
@@ -16531,6 +16971,13 @@ async function status() {
16531
16971
  process.stdout.write(`prepare hook: ${cfg.prepareHook.command} (device-level, all agents)
16532
16972
  `);
16533
16973
  }
16974
+ if (runnersEnabled(cfg) && cfg.runners?.sandbox === "os") {
16975
+ process.stdout.write("sandbox: os\n");
16976
+ for (const line of postureLines(parseSandboxPolicy(cfg.runners.policy))) {
16977
+ process.stdout.write(` ${line}
16978
+ `);
16979
+ }
16980
+ }
16534
16981
  const overrides2 = Object.keys(cfg.agents ?? {});
16535
16982
  if (overrides2.length > 0) {
16536
16983
  process.stdout.write(`local overrides for: ${overrides2.join(", ")}
@@ -16623,7 +17070,7 @@ function isAlive(kill, pid) {
16623
17070
  init_errors();
16624
17071
  init_src();
16625
17072
  init_transcript_writer();
16626
- import { existsSync as existsSync17, readFileSync as readFileSync12, readdirSync as readdirSync7 } from "fs";
17073
+ import { existsSync as existsSync17, readFileSync as readFileSync13, readdirSync as readdirSync7 } from "fs";
16627
17074
  import { isAbsolute, join as join21 } from "path";
16628
17075
  async function transcript(opts = {}) {
16629
17076
  const dir2 = transcriptsDir();
@@ -16724,7 +17171,7 @@ function isComplete(content) {
16724
17171
  async function followTranscripts(dir2) {
16725
17172
  const follower = new TranscriptFollower({
16726
17173
  listFiles: () => listFiles(dir2),
16727
- read: (f) => readFileSync12(join21(dir2, f), "utf8"),
17174
+ read: (f) => readFileSync13(join21(dir2, f), "utf8"),
16728
17175
  write: (s) => process.stdout.write(s),
16729
17176
  // CSI: cursor up `n` lines, then erase from cursor to end of screen.
16730
17177
  clearLines: (n) => process.stdout.write(`\x1B[${n}A\x1B[0J`),
@@ -16785,7 +17232,7 @@ function peek(path) {
16785
17232
  let meta;
16786
17233
  let outcome;
16787
17234
  try {
16788
- for (const line of readFileSync12(path, "utf8").split("\n")) {
17235
+ for (const line of readFileSync13(path, "utf8").split("\n")) {
16789
17236
  if (!line.trim()) continue;
16790
17237
  const o = safeParse(line);
16791
17238
  const t = str2(rec(o)?.type);
@@ -16818,7 +17265,7 @@ function resolveTarget(dir2, target) {
16818
17265
  function renderFile(path) {
16819
17266
  let content;
16820
17267
  try {
16821
- content = readFileSync12(path, "utf8");
17268
+ content = readFileSync13(path, "utf8");
16822
17269
  } catch (err) {
16823
17270
  throw new CompanionError(
16824
17271
  `couldn't read ${path}: ${err instanceof Error ? err.message : String(err)}`
@@ -16980,7 +17427,7 @@ program.command("pair").description(
16980
17427
  });
16981
17428
  });
16982
17429
  program.command("write-paired-config", { hidden: true }).description("persist an already-completed device enrollment payload from stdin.").action(() => {
16983
- writeCompletedPairing(readFileSync13(0, "utf8"));
17430
+ writeCompletedPairing(readFileSync14(0, "utf8"));
16984
17431
  });
16985
17432
  program.command("start").description(
16986
17433
  "pair this device if needed, connect a coding agent on it, and run in the background."