@cabane/companion 0.6.188 → 0.6.189

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
@@ -788,7 +788,15 @@ var init_config = __esm({
788
788
  // and an agent with no folder of its own and no prepare hook works there
789
789
  // (`turn-execution.ts`, `prepareEnvironment`). Default `~/.cabane/work`: hidden, so pairing a
790
790
  // laptop creates no new visible folder; set it to make the work visible.
791
- root: z2.string().min(1).refine(isRootPath, { message: "must be an absolute path, or start with ~/" }).optional()
791
+ root: z2.string().min(1).refine(isRootPath, { message: "must be an absolute path, or start with ~/" }).optional(),
792
+ // An argv prefix the operator puts in front of every turn's runner: the companion starts
793
+ // `<prefix…> <its own run-turn command…>` instead of the runner bare. What the prefix does
794
+ // is the operator's (a box of their own making, ENG225); the companion knows nothing of it
795
+ // beyond handing the runner over, with the turn's inputs in its environment and the
796
+ // credential and secrets on its stdin (`runner/launch.ts`, `boxed`).
797
+ runner: z2.object({
798
+ command: z2.array(z2.string().min(1)).min(1)
799
+ }).strict().optional()
792
800
  });
793
801
  legacyOfferSchema = z2.object({
794
802
  claudeCode: z2.object({ enabled: z2.boolean().optional() }).passthrough().optional(),
@@ -1014,6 +1022,142 @@ var init_bundled_binaries = __esm({
1014
1022
  }
1015
1023
  });
1016
1024
 
1025
+ // src/harness-versions.ts
1026
+ function parseVersionToken(raw) {
1027
+ if (!raw) return null;
1028
+ const m = raw.match(/\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?/);
1029
+ return m ? m[0] : null;
1030
+ }
1031
+ async function probeOpencodeVersion(serverUrl, fetchImpl = fetch) {
1032
+ try {
1033
+ const base = serverUrl.endsWith("/") ? serverUrl.slice(0, -1) : serverUrl;
1034
+ const res = await fetchImpl(`${base}/global/health`, {
1035
+ headers: { accept: "application/json" }
1036
+ });
1037
+ if (!res.ok) return null;
1038
+ return extractOpencodeVersion(await res.json());
1039
+ } catch {
1040
+ return null;
1041
+ }
1042
+ }
1043
+ function extractOpencodeVersion(json) {
1044
+ if (!json || typeof json !== "object") return null;
1045
+ const obj = json;
1046
+ const candidates = [obj.version];
1047
+ for (const v of Object.values(obj)) {
1048
+ if (v && typeof v === "object") candidates.push(v.version);
1049
+ }
1050
+ for (const c of candidates) {
1051
+ if (typeof c === "string") {
1052
+ const parsed = parseVersionToken(c);
1053
+ if (parsed) return parsed;
1054
+ }
1055
+ }
1056
+ return null;
1057
+ }
1058
+ function emptyHarnessVersions() {
1059
+ return { ...EMPTY };
1060
+ }
1061
+ var EMPTY;
1062
+ var init_harness_versions = __esm({
1063
+ "src/harness-versions.ts"() {
1064
+ "use strict";
1065
+ EMPTY = { claudeCode: null, opencode: null, codex: null };
1066
+ }
1067
+ });
1068
+
1069
+ // src/harness-usable.ts
1070
+ import { spawn as spawn2 } from "child_process";
1071
+ function apiKeyVariable(runtime, env = process.env) {
1072
+ return API_KEY_VARIABLES[runtime].find((name) => !!env[name]?.trim()) ?? null;
1073
+ }
1074
+ async function checkHarnessUsable(runtime, deps = {}) {
1075
+ const env = deps.env ?? process.env;
1076
+ const resolveBundled = deps.resolveBundled ?? ((r) => resolveBundledBinary(r));
1077
+ const run = deps.run ?? runBounded;
1078
+ const resolution = resolveBundled(runtime);
1079
+ if (resolution.status !== "present" || !resolution.path) {
1080
+ return {
1081
+ status: "bundle_missing",
1082
+ bundled: resolution.status === "present" ? "executable_missing" : resolution.status
1083
+ };
1084
+ }
1085
+ const path = resolution.path;
1086
+ const version = run(path, ["--version"], env).then(
1087
+ (r) => r.code === 0 ? parseVersionToken(r.output) : null
1088
+ );
1089
+ const variable = apiKeyVariable(runtime, env);
1090
+ if (variable) return { status: "api_key", variable, version: await version };
1091
+ const statusArgs = runtime === "codex" ? ["login", "status"] : ["auth", "status", "--json"];
1092
+ const [answer, v] = await Promise.all([run(path, statusArgs, env), version]);
1093
+ if (answer.error) return { status: "check_failed", reason: answer.error };
1094
+ const signedIn = runtime === "codex" ? readCodexStatus(answer) : readClaudeStatus(answer);
1095
+ if (typeof signedIn === "string") return { status: "check_failed", reason: signedIn };
1096
+ return signedIn ? { status: "signed_in", version: v } : { status: "signed_out", version: v };
1097
+ }
1098
+ function readClaudeStatus(answer) {
1099
+ const start2 = answer.output.indexOf("{");
1100
+ const end = answer.output.lastIndexOf("}");
1101
+ if (start2 < 0 || end < start2) return "unreadable";
1102
+ try {
1103
+ const parsed = JSON.parse(answer.output.slice(start2, end + 1));
1104
+ const loggedIn = parsed?.loggedIn;
1105
+ return typeof loggedIn === "boolean" ? loggedIn : "unreadable";
1106
+ } catch {
1107
+ return "unreadable";
1108
+ }
1109
+ }
1110
+ function readCodexStatus(answer) {
1111
+ if (answer.code === 0) return true;
1112
+ if (/not logged in/i.test(answer.output)) return false;
1113
+ if (/config/i.test(answer.output)) return "config";
1114
+ return "unreadable";
1115
+ }
1116
+ function runBounded(path, args, env) {
1117
+ return new Promise((resolve2) => {
1118
+ let settled = false;
1119
+ let out = "";
1120
+ const done = (result) => {
1121
+ if (settled) return;
1122
+ settled = true;
1123
+ clearTimeout(timer);
1124
+ resolve2(result);
1125
+ };
1126
+ let child;
1127
+ try {
1128
+ child = spawn2(path, args, { stdio: ["ignore", "pipe", "pipe"], env });
1129
+ } catch {
1130
+ resolve2({ code: null, output: "", error: "spawn" });
1131
+ return;
1132
+ }
1133
+ const capture = (chunk) => {
1134
+ if (out.length < 8192) out += chunk.toString();
1135
+ };
1136
+ child.stdout?.on("data", capture);
1137
+ child.stderr?.on("data", capture);
1138
+ const timer = setTimeout(() => {
1139
+ child.kill("SIGKILL");
1140
+ done({ code: null, output: out, error: "timeout" });
1141
+ }, CHECK_TIMEOUT_MS);
1142
+ timer.unref?.();
1143
+ child.once("error", () => done({ code: null, output: out, error: "spawn" }));
1144
+ child.once("close", (code) => done({ code, output: out }));
1145
+ });
1146
+ }
1147
+ var API_KEY_VARIABLES, CHECK_TIMEOUT_MS;
1148
+ var init_harness_usable = __esm({
1149
+ "src/harness-usable.ts"() {
1150
+ "use strict";
1151
+ init_bundled_binaries();
1152
+ init_harness_versions();
1153
+ API_KEY_VARIABLES = {
1154
+ "claude-code": ["ANTHROPIC_API_KEY", "CLAUDE_CODE_OAUTH_TOKEN"],
1155
+ codex: ["CODEX_API_KEY"]
1156
+ };
1157
+ CHECK_TIMEOUT_MS = 5e3;
1158
+ }
1159
+ });
1160
+
1017
1161
  // packages/agent-runtime/src/failure.ts
1018
1162
  import { z as z4 } from "zod";
1019
1163
  function encodeFailureReason(failure) {
@@ -7365,6 +7509,14 @@ function required(env, name) {
7365
7509
  if (!value) throw new Error(`runner: ${name} is not set`);
7366
7510
  return value;
7367
7511
  }
7512
+ function runnerPlace(env) {
7513
+ const workRoot2 = env[PLACE_ENV.workRoot];
7514
+ const transcriptDir = env[PLACE_ENV.transcriptDir];
7515
+ return {
7516
+ ...workRoot2 ? { workRoot: workRoot2 } : {},
7517
+ ...transcriptDir ? { transcriptDir } : {}
7518
+ };
7519
+ }
7368
7520
  function parseRunnerInput(env, stdin) {
7369
7521
  const dispatch = dispatchSchema.parse(JSON.parse(required(env, RUNNER_ENV.dispatch)));
7370
7522
  const spec = runnerSpecSchema.parse(JSON.parse(required(env, RUNNER_ENV.spec)));
@@ -7372,10 +7524,12 @@ function parseRunnerInput(env, stdin) {
7372
7524
  return {
7373
7525
  baseUrl: required(env, RUNNER_ENV.url),
7374
7526
  credential: credentials.credential,
7527
+ ...credentials.secrets ? { secrets: credentials.secrets } : {},
7375
7528
  dispatch,
7376
7529
  turnId: required(env, RUNNER_ENV.turnId),
7377
7530
  resumed: env[RUNNER_ENV.resumed] === "1",
7378
- spec
7531
+ spec,
7532
+ place: runnerPlace(env)
7379
7533
  };
7380
7534
  }
7381
7535
  function runnerEnv(input) {
@@ -7388,7 +7542,10 @@ function runnerEnv(input) {
7388
7542
  };
7389
7543
  }
7390
7544
  function runnerStdin(input) {
7391
- return `${JSON.stringify({ credential: input.credential })}
7545
+ return `${JSON.stringify({
7546
+ credential: input.credential,
7547
+ ...input.secrets ? { secrets: input.secrets } : {}
7548
+ })}
7392
7549
  `;
7393
7550
  }
7394
7551
  function encodeLauncherMessage(message2) {
@@ -7418,7 +7575,7 @@ function decodeRunnerEvent(line) {
7418
7575
  if (!value || typeof value !== "object" || !("cabaneRunner" in value)) return null;
7419
7576
  return value;
7420
7577
  }
7421
- var RUNNER_ENV, runtimeHealthSchema, runnerSpecSchema, dispatchSchema, credentialsSchema;
7578
+ var RUNNER_ENV, PLACE_ENV, runtimeHealthSchema, runnerSpecSchema, dispatchSchema, credentialsSchema;
7422
7579
  var init_protocol = __esm({
7423
7580
  "src/runner/protocol.ts"() {
7424
7581
  "use strict";
@@ -7437,6 +7594,10 @@ var init_protocol = __esm({
7437
7594
  // Everything else the turn is constructed from, as JSON: see `RunnerSpec`.
7438
7595
  spec: "CABANE_RUNNER_SPEC"
7439
7596
  };
7597
+ PLACE_ENV = {
7598
+ workRoot: "CABANE_WORK_ROOT",
7599
+ transcriptDir: "CABANE_TRANSCRIPT_DIR"
7600
+ };
7440
7601
  runtimeHealthSchema = z14.object({
7441
7602
  quotaState: z14.enum(["ok", "limited", "auth_expired", "unknown"]),
7442
7603
  limitedUntil: z14.string().nullable(),
@@ -7486,7 +7647,8 @@ var init_protocol = __esm({
7486
7647
  episode: z14.number().int().optional()
7487
7648
  });
7488
7649
  credentialsSchema = z14.object({
7489
- credential: z14.string().startsWith("cabrn_")
7650
+ credential: z14.string().startsWith("cabrn_"),
7651
+ secrets: z14.record(z14.string(), z14.string()).optional()
7490
7652
  });
7491
7653
  }
7492
7654
  });
@@ -7498,6 +7660,19 @@ function selfRunnerCommand() {
7498
7660
  if (!script) throw new Error("runner: cannot tell which script started this companion");
7499
7661
  return [process.execPath, ...process.execArgv, script, "run-turn"];
7500
7662
  }
7663
+ function wrappedRunnerCommand(prefix, own) {
7664
+ return [...prefix ?? [], ...own];
7665
+ }
7666
+ function runnerExecEnv(base, boxed) {
7667
+ const env = {};
7668
+ for (const [k, v] of Object.entries(base)) {
7669
+ if (v === void 0) continue;
7670
+ if (boxed && (BOXED_ENV_DROPPED.includes(k) || k.startsWith(BOXED_ENV_DROPPED_PREFIX)))
7671
+ continue;
7672
+ env[k] = v;
7673
+ }
7674
+ return env;
7675
+ }
7501
7676
  function healthSnapshot(store) {
7502
7677
  const out = {};
7503
7678
  for (const r of store?.reports() ?? []) {
@@ -7694,13 +7869,12 @@ async function runTurnInRunner(opts, launch, duties, payload, handleOpts) {
7694
7869
  return { ok: false, durationMs: durationMs2, reason: reason2 };
7695
7870
  }
7696
7871
  try {
7697
- const env = {};
7698
- for (const [k, v] of Object.entries(launch.env ?? process.env)) {
7699
- if (v !== void 0) env[k] = v;
7700
- }
7872
+ const boxed = launch.boxed === true;
7873
+ const env = runnerExecEnv(launch.env ?? process.env, boxed);
7701
7874
  const input = {
7702
7875
  baseUrl: opts.baseUrl,
7703
7876
  credential: minted.credential,
7877
+ ...boxed ? { secrets: launch.secrets?.() ?? {} } : {},
7704
7878
  dispatch: payload,
7705
7879
  turnId,
7706
7880
  resumed: handleOpts.resumed,
@@ -7772,7 +7946,7 @@ async function runTurnInRunner(opts, launch, duties, payload, handleOpts) {
7772
7946
  locks?.release(lockOwner(event.id));
7773
7947
  return;
7774
7948
  case "process_group":
7775
- groups.push(event.pgid);
7949
+ if (!boxed) groups.push(event.pgid);
7776
7950
  return;
7777
7951
  case "remote_session":
7778
7952
  remote.push({ serverUrl: event.serverUrl, sessionId: event.sessionId });
@@ -7793,7 +7967,8 @@ async function runTurnInRunner(opts, launch, duties, payload, handleOpts) {
7793
7967
  }
7794
7968
  if (result) return result;
7795
7969
  const durationMs = Date.now() - startedAt;
7796
- const reason = thrown !== void 0 ? `runner_failed: ${thrown}` : `runner_exited: ${code}`;
7970
+ const stopped = controller.signal.aborted && thrown === void 0;
7971
+ const reason = stopped ? "cancelled" : thrown !== void 0 ? `runner_failed: ${thrown}` : `runner_exited: ${code}`;
7797
7972
  await child?.reap(groups);
7798
7973
  const reaped = await (launch.reapTurn ?? reapTurnScopes)(turnId).catch((err) => ({
7799
7974
  failures: [err instanceof Error ? err.message : String(err)]
@@ -7808,8 +7983,7 @@ async function runTurnInRunner(opts, launch, duties, payload, handleOpts) {
7808
7983
  await opts.api.setActiveRun(opts.workspaceId, payload.conversationId, payload.agentId, {
7809
7984
  activeRunStartedAt: null,
7810
7985
  turnId,
7811
- outcome: "failed",
7812
- errorReason: reason.slice(0, ERROR_REASON_MAX),
7986
+ ...stopped ? { outcome: "interrupted" } : { outcome: "failed", errorReason: reason.slice(0, ERROR_REASON_MAX) },
7813
7987
  readThrough: payload.messageId
7814
7988
  });
7815
7989
  } catch (err) {
@@ -7827,11 +8001,12 @@ async function runTurnInRunner(opts, launch, duties, payload, handleOpts) {
7827
8001
  reason: "exited"
7828
8002
  });
7829
8003
  if (thrown !== void 0) throw new Error(thrown);
7830
- log2.warn({ turnId, code }, "runner: exited without reporting a result");
8004
+ if (stopped) log2.info({ turnId, code }, "runner: stopped before it could report");
8005
+ else log2.warn({ turnId, code }, "runner: exited without reporting a result");
7831
8006
  duties.notifyEnd({ id: payload.messageId, ok: false, durationMs, reason });
7832
8007
  return { ok: false, durationMs, reason };
7833
8008
  }
7834
- var PINO_LEVELS;
8009
+ var BOXED_ENV_DROPPED, BOXED_ENV_DROPPED_PREFIX, PINO_LEVELS;
7835
8010
  var init_launch = __esm({
7836
8011
  "src/runner/launch.ts"() {
7837
8012
  "use strict";
@@ -7842,6 +8017,16 @@ var init_launch = __esm({
7842
8017
  init_boot_id();
7843
8018
  init_turn_containment();
7844
8019
  init_protocol();
8020
+ BOXED_ENV_DROPPED = [
8021
+ "ANTHROPIC_API_KEY",
8022
+ "CLAUDE_CODE_OAUTH_TOKEN",
8023
+ "CODEX_API_KEY",
8024
+ "GH_TOKEN",
8025
+ "GITHUB_TOKEN",
8026
+ "DBUS_SESSION_BUS_ADDRESS",
8027
+ "XDG_RUNTIME_DIR"
8028
+ ];
8029
+ BOXED_ENV_DROPPED_PREFIX = "CABANE_GATE_";
7845
8030
  PINO_LEVELS = {
7846
8031
  10: "trace",
7847
8032
  20: "debug",
@@ -8456,9 +8641,9 @@ import { z as z16 } from "zod";
8456
8641
  function secretsPath() {
8457
8642
  return join19(cabaneDir(), "secrets.json");
8458
8643
  }
8459
- function loadSecretStore() {
8644
+ function loadSecretValues() {
8460
8645
  const path = secretsPath();
8461
- if (!existsSync10(path)) return makeStore({});
8646
+ if (!existsSync10(path)) return {};
8462
8647
  let raw;
8463
8648
  try {
8464
8649
  raw = readFileSync8(path, "utf8");
@@ -8467,7 +8652,7 @@ function loadSecretStore() {
8467
8652
  `couldn't read ${path}: ${err instanceof Error ? err.message : String(err)}`
8468
8653
  );
8469
8654
  }
8470
- if (raw.trim().length === 0) return makeStore({});
8655
+ if (raw.trim().length === 0) return {};
8471
8656
  let parsed;
8472
8657
  try {
8473
8658
  parsed = JSON.parse(raw);
@@ -8482,14 +8667,17 @@ function loadSecretStore() {
8482
8667
  `${path} must be a flat object mapping secret names to string values (e.g. { "GITHUB_TOKEN": "ghp_\u2026" }).`
8483
8668
  );
8484
8669
  }
8485
- return makeStore(result.data);
8670
+ return result.data;
8486
8671
  }
8487
8672
  function loadSecretStoreTolerant(onWarn) {
8673
+ return makeStore(loadSecretValuesTolerant(onWarn));
8674
+ }
8675
+ function loadSecretValuesTolerant(onWarn) {
8488
8676
  try {
8489
- return loadSecretStore();
8677
+ return loadSecretValues();
8490
8678
  } catch (err) {
8491
8679
  onWarn?.(err instanceof Error ? err.message : String(err));
8492
- return makeStore({});
8680
+ return {};
8493
8681
  }
8494
8682
  }
8495
8683
  function makeStore(map) {
@@ -8581,6 +8769,31 @@ var init_secrets = __esm({
8581
8769
  }
8582
8770
  });
8583
8771
 
8772
+ // src/turn-env.ts
8773
+ function harnessLoginEnv(store, runtime) {
8774
+ const names = [
8775
+ ...runtime in API_KEY_VARIABLES ? API_KEY_VARIABLES[runtime] : [],
8776
+ GITHUB_TOKEN_NAME
8777
+ ];
8778
+ const env = {};
8779
+ for (const name of names) {
8780
+ const value = store.get(name);
8781
+ if (value) env[name] = value;
8782
+ }
8783
+ return env;
8784
+ }
8785
+ function turnIdentityEnv(ids) {
8786
+ return { CABANE_CONVERSATION_ID: ids.conversationId, CABANE_AGENT_ID: ids.agentId };
8787
+ }
8788
+ var GITHUB_TOKEN_NAME;
8789
+ var init_turn_env = __esm({
8790
+ "src/turn-env.ts"() {
8791
+ "use strict";
8792
+ init_harness_usable();
8793
+ GITHUB_TOKEN_NAME = "GH_TOKEN";
8794
+ }
8795
+ });
8796
+
8584
8797
  // src/transcript-writer.ts
8585
8798
  import { appendFileSync, chmodSync as chmodSync4, copyFileSync, mkdirSync as mkdirSync8, readdirSync, rmSync as rmSync6 } from "fs";
8586
8799
  import { basename as basename3, dirname as dirname7, join as join20 } from "path";
@@ -8971,6 +9184,7 @@ var init_turn_execution = __esm({
8971
9184
  init_prepare_failure();
8972
9185
  init_prepared();
8973
9186
  init_secrets();
9187
+ init_turn_env();
8974
9188
  init_turn_control_tools();
8975
9189
  init_transcript_writer();
8976
9190
  init_turn_containment();
@@ -9038,6 +9252,7 @@ var init_turn_execution = __esm({
9038
9252
  // The harness's secret environment for this turn. Only ever handed to the
9039
9253
  // adapter; never logged, never merged into `process.env`.
9040
9254
  secretEnv = {};
9255
+ secretStore;
9041
9256
  effectiveCwd;
9042
9257
  hookEnv;
9043
9258
  turnEnv;
@@ -9326,7 +9541,8 @@ var init_turn_execution = __esm({
9326
9541
  throw this.concluded(reason, reason);
9327
9542
  }
9328
9543
  this.resolvedMcpServers = resolvedMcpServers;
9329
- this.secretEnv = secretEnv;
9544
+ this.secretEnv = { ...secretEnv, ...harnessLoginEnv(secretStore, this.turnContext.runtime) };
9545
+ this.secretStore = secretStore;
9330
9546
  }
9331
9547
  async prepareEnvironment() {
9332
9548
  const { payload, workspaceId, turnId, turnLog } = this;
@@ -9477,6 +9693,10 @@ var init_turn_execution = __esm({
9477
9693
  }
9478
9694
  const turnEnv = {
9479
9695
  ...hookEnv,
9696
+ ...turnIdentityEnv({
9697
+ conversationId: this.payload.conversationId,
9698
+ agentId: this.payload.agentId
9699
+ }),
9480
9700
  CABANE_COMPANION_HOME: agentCompanionHome()
9481
9701
  };
9482
9702
  this.codexLocal = this.turnContext.runtime === "codex" ? { configSetsSandboxMode: await readCodexConfigSetsSandboxMode() } : void 0;
@@ -10551,8 +10771,14 @@ function runnerTurnOptions(input, log2, emit, extras = {}) {
10551
10771
  agentUsername: spec.agentUsername,
10552
10772
  credential: input.credential,
10553
10773
  local: spec.local,
10554
- workRoot: spec.workRoot,
10774
+ // The place the runner stands in names its own root and transcript
10775
+ // directory when it has them (`PLACE_ENV`); the launcher's host-computed
10776
+ // values are what a runner on the person's own machine gets.
10777
+ workRoot: input.place?.workRoot ?? spec.workRoot,
10555
10778
  runConfig: spec.runConfig,
10779
+ // The launcher's copy of the device's secrets, when it sent one: a runner
10780
+ // in a box has no `secrets.json` to read.
10781
+ ...input.secrets ? { secretStore: makeStore(input.secrets) } : {},
10556
10782
  log: log2,
10557
10783
  ...queryFn ? { queryFn } : {},
10558
10784
  ...spec.opencodeServerUrl ? { opencodeServerUrl: spec.opencodeServerUrl } : {},
@@ -10562,7 +10788,7 @@ function runnerTurnOptions(input, log2, emit, extras = {}) {
10562
10788
  ...spec.totalTimeoutMs !== void 0 ? { totalTimeoutMs: spec.totalTimeoutMs } : {},
10563
10789
  ...spec.leaseRenewalMs !== void 0 ? { leaseRenewalMs: spec.leaseRenewalMs } : {},
10564
10790
  ...spec.preparingRowDelayMs !== void 0 ? { preparingRowDelayMs: spec.preparingRowDelayMs } : {},
10565
- ...spec.transcripts ? { transcriptDir: transcriptsDir() } : {},
10791
+ ...spec.transcripts ? { transcriptDir: input.place?.transcriptDir ?? transcriptsDir() } : {},
10566
10792
  connectorHealth,
10567
10793
  inRunner: {
10568
10794
  bootId: spec.bootId,
@@ -10697,6 +10923,7 @@ var init_run_turn = __esm({
10697
10923
  init_exec_shim();
10698
10924
  init_launcher_channel();
10699
10925
  init_opencode_lock();
10926
+ init_secrets();
10700
10927
  init_transcript_writer();
10701
10928
  init_turn_execution();
10702
10929
  init_protocol();
@@ -11134,133 +11361,8 @@ function spaced(sentence) {
11134
11361
 
11135
11362
  // src/harness-detect.ts
11136
11363
  init_config();
11137
-
11138
- // src/harness-usable.ts
11139
- init_bundled_binaries();
11140
- import { spawn as spawn2 } from "child_process";
11141
-
11142
- // src/harness-versions.ts
11143
- var EMPTY = { claudeCode: null, opencode: null, codex: null };
11144
- function parseVersionToken(raw) {
11145
- if (!raw) return null;
11146
- const m = raw.match(/\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?/);
11147
- return m ? m[0] : null;
11148
- }
11149
- async function probeOpencodeVersion(serverUrl, fetchImpl = fetch) {
11150
- try {
11151
- const base = serverUrl.endsWith("/") ? serverUrl.slice(0, -1) : serverUrl;
11152
- const res = await fetchImpl(`${base}/global/health`, {
11153
- headers: { accept: "application/json" }
11154
- });
11155
- if (!res.ok) return null;
11156
- return extractOpencodeVersion(await res.json());
11157
- } catch {
11158
- return null;
11159
- }
11160
- }
11161
- function extractOpencodeVersion(json) {
11162
- if (!json || typeof json !== "object") return null;
11163
- const obj = json;
11164
- const candidates = [obj.version];
11165
- for (const v of Object.values(obj)) {
11166
- if (v && typeof v === "object") candidates.push(v.version);
11167
- }
11168
- for (const c of candidates) {
11169
- if (typeof c === "string") {
11170
- const parsed = parseVersionToken(c);
11171
- if (parsed) return parsed;
11172
- }
11173
- }
11174
- return null;
11175
- }
11176
- function emptyHarnessVersions() {
11177
- return { ...EMPTY };
11178
- }
11179
-
11180
- // src/harness-usable.ts
11181
- var API_KEY_VARIABLES = {
11182
- "claude-code": ["ANTHROPIC_API_KEY", "CLAUDE_CODE_OAUTH_TOKEN"],
11183
- codex: ["CODEX_API_KEY"]
11184
- };
11185
- function apiKeyVariable(runtime, env = process.env) {
11186
- return API_KEY_VARIABLES[runtime].find((name) => !!env[name]?.trim()) ?? null;
11187
- }
11188
- var CHECK_TIMEOUT_MS = 5e3;
11189
- async function checkHarnessUsable(runtime, deps = {}) {
11190
- const env = deps.env ?? process.env;
11191
- const resolveBundled = deps.resolveBundled ?? ((r) => resolveBundledBinary(r));
11192
- const run = deps.run ?? runBounded;
11193
- const resolution = resolveBundled(runtime);
11194
- if (resolution.status !== "present" || !resolution.path) {
11195
- return {
11196
- status: "bundle_missing",
11197
- bundled: resolution.status === "present" ? "executable_missing" : resolution.status
11198
- };
11199
- }
11200
- const path = resolution.path;
11201
- const version = run(path, ["--version"], env).then(
11202
- (r) => r.code === 0 ? parseVersionToken(r.output) : null
11203
- );
11204
- const variable = apiKeyVariable(runtime, env);
11205
- if (variable) return { status: "api_key", variable, version: await version };
11206
- const statusArgs = runtime === "codex" ? ["login", "status"] : ["auth", "status", "--json"];
11207
- const [answer, v] = await Promise.all([run(path, statusArgs, env), version]);
11208
- if (answer.error) return { status: "check_failed", reason: answer.error };
11209
- const signedIn = runtime === "codex" ? readCodexStatus(answer) : readClaudeStatus(answer);
11210
- if (typeof signedIn === "string") return { status: "check_failed", reason: signedIn };
11211
- return signedIn ? { status: "signed_in", version: v } : { status: "signed_out", version: v };
11212
- }
11213
- function readClaudeStatus(answer) {
11214
- const start2 = answer.output.indexOf("{");
11215
- const end = answer.output.lastIndexOf("}");
11216
- if (start2 < 0 || end < start2) return "unreadable";
11217
- try {
11218
- const parsed = JSON.parse(answer.output.slice(start2, end + 1));
11219
- const loggedIn = parsed?.loggedIn;
11220
- return typeof loggedIn === "boolean" ? loggedIn : "unreadable";
11221
- } catch {
11222
- return "unreadable";
11223
- }
11224
- }
11225
- function readCodexStatus(answer) {
11226
- if (answer.code === 0) return true;
11227
- if (/not logged in/i.test(answer.output)) return false;
11228
- if (/config/i.test(answer.output)) return "config";
11229
- return "unreadable";
11230
- }
11231
- function runBounded(path, args, env) {
11232
- return new Promise((resolve2) => {
11233
- let settled = false;
11234
- let out = "";
11235
- const done = (result) => {
11236
- if (settled) return;
11237
- settled = true;
11238
- clearTimeout(timer);
11239
- resolve2(result);
11240
- };
11241
- let child;
11242
- try {
11243
- child = spawn2(path, args, { stdio: ["ignore", "pipe", "pipe"], env });
11244
- } catch {
11245
- resolve2({ code: null, output: "", error: "spawn" });
11246
- return;
11247
- }
11248
- const capture = (chunk) => {
11249
- if (out.length < 8192) out += chunk.toString();
11250
- };
11251
- child.stdout?.on("data", capture);
11252
- child.stderr?.on("data", capture);
11253
- const timer = setTimeout(() => {
11254
- child.kill("SIGKILL");
11255
- done({ code: null, output: out, error: "timeout" });
11256
- }, CHECK_TIMEOUT_MS);
11257
- timer.unref?.();
11258
- child.once("error", () => done({ code: null, output: out, error: "spawn" }));
11259
- child.once("close", (code) => done({ code, output: out }));
11260
- });
11261
- }
11262
-
11263
- // src/harness-detect.ts
11364
+ init_harness_usable();
11365
+ init_harness_versions();
11264
11366
  var DEFAULT_OPENCODE_SERVER_URL = "http://127.0.0.1:4096";
11265
11367
  var DEFAULT_OPENCODE_PROBE_MS = 1e3;
11266
11368
  var CONFIGURED_OPENCODE_PROBE_MS = 4e3;
@@ -12985,6 +13087,10 @@ function runtimeIssuesFor(gates) {
12985
13087
  return issues;
12986
13088
  }
12987
13089
 
13090
+ // src/supervisor.ts
13091
+ init_harness_versions();
13092
+ init_harness_usable();
13093
+
12988
13094
  // src/opencode-models.ts
12989
13095
  var OPENCODE_RUNTIME = "opencode";
12990
13096
  function mapOpencodeProviders(json) {
@@ -14473,7 +14579,16 @@ var CompanionSupervisor = class {
14473
14579
  },
14474
14580
  runner: {
14475
14581
  provider: this.runnerProvider,
14476
- command: this.runnerCommand ?? selfRunnerCommand(),
14582
+ command: wrappedRunnerCommand(
14583
+ this.config.runner?.command,
14584
+ this.runnerCommand ?? selfRunnerCommand()
14585
+ ),
14586
+ ...this.config.runner?.command ? {
14587
+ boxed: true,
14588
+ secrets: () => loadSecretValuesTolerant(
14589
+ (m) => this.log.warn({ err: m }, "Could not read the secrets file for a boxed turn.")
14590
+ )
14591
+ } : {},
14477
14592
  ...this.runnerEnv ? { env: this.runnerEnv } : {},
14478
14593
  reportRunnerGone: (report) => this.reportRunnerGone(report),
14479
14594
  connectorHealth: this.connectorHealth,
@@ -17109,7 +17224,7 @@ init_errors();
17109
17224
  init_src();
17110
17225
  init_transcript_writer();
17111
17226
  import { existsSync as existsSync19, readFileSync as readFileSync17, readdirSync as readdirSync6 } from "fs";
17112
- import { isAbsolute as isAbsolute2, join as join31 } from "path";
17227
+ import { basename as basename4, isAbsolute as isAbsolute2, join as join31 } from "path";
17113
17228
  async function transcript(opts = {}) {
17114
17229
  const dir2 = transcriptsDir();
17115
17230
  if (opts.follow) {
@@ -17232,13 +17347,28 @@ async function followTranscripts(dir2) {
17232
17347
  follower.tick();
17233
17348
  });
17234
17349
  }
17235
- function listFiles(dir2) {
17350
+ function listTranscriptFiles(dir2) {
17351
+ let entries;
17236
17352
  try {
17237
- return readdirSync6(dir2).filter((f) => f.endsWith(".jsonl")).sort().reverse();
17353
+ entries = readdirSync6(dir2, { withFileTypes: true });
17238
17354
  } catch {
17239
17355
  return [];
17240
17356
  }
17357
+ const files = [];
17358
+ for (const entry of entries) {
17359
+ if (entry.isFile() && entry.name.endsWith(".jsonl")) files.push(entry.name);
17360
+ if (!entry.isDirectory() || entry.name === "anomalies") continue;
17361
+ let inner;
17362
+ try {
17363
+ inner = readdirSync6(join31(dir2, entry.name));
17364
+ } catch {
17365
+ continue;
17366
+ }
17367
+ for (const name of inner) if (name.endsWith(".jsonl")) files.push(`${entry.name}/${name}`);
17368
+ }
17369
+ return files.sort((a, b) => basename4(a) < basename4(b) ? 1 : basename4(a) > basename4(b) ? -1 : 0);
17241
17370
  }
17371
+ var listFiles = listTranscriptFiles;
17242
17372
  function printList(dir2) {
17243
17373
  const files = listFiles(dir2);
17244
17374
  if (files.length === 0) {
@@ -17285,6 +17415,7 @@ function peek(path) {
17285
17415
  function resolveTarget(dir2, target) {
17286
17416
  if (isAbsolute2(target) || target.includes("/")) {
17287
17417
  if (existsSync19(target)) return target;
17418
+ if (!isAbsolute2(target) && existsSync19(join31(dir2, target))) return join31(dir2, target);
17288
17419
  throw new CompanionError(`no transcript at ${target}.`);
17289
17420
  }
17290
17421
  const exact = join31(dir2, target);
package/dist/launch.js CHANGED
@@ -196,7 +196,15 @@ var companionConfigSchema = z2.object({
196
196
  // and an agent with no folder of its own and no prepare hook works there
197
197
  // (`turn-execution.ts`, `prepareEnvironment`). Default `~/.cabane/work`: hidden, so pairing a
198
198
  // laptop creates no new visible folder; set it to make the work visible.
199
- root: z2.string().min(1).refine(isRootPath, { message: "must be an absolute path, or start with ~/" }).optional()
199
+ root: z2.string().min(1).refine(isRootPath, { message: "must be an absolute path, or start with ~/" }).optional(),
200
+ // An argv prefix the operator puts in front of every turn's runner: the companion starts
201
+ // `<prefix…> <its own run-turn command…>` instead of the runner bare. What the prefix does
202
+ // is the operator's (a box of their own making, ENG225); the companion knows nothing of it
203
+ // beyond handing the runner over, with the turn's inputs in its environment and the
204
+ // credential and secrets on its stdin (`runner/launch.ts`, `boxed`).
205
+ runner: z2.object({
206
+ command: z2.array(z2.string().min(1)).min(1)
207
+ }).strict().optional()
200
208
  });
201
209
  var legacyOfferSchema = z2.object({
202
210
  claudeCode: z2.object({ enabled: z2.boolean().optional() }).passthrough().optional(),
@@ -221,7 +221,15 @@ var companionConfigSchema = z2.object({
221
221
  // and an agent with no folder of its own and no prepare hook works there
222
222
  // (`turn-execution.ts`, `prepareEnvironment`). Default `~/.cabane/work`: hidden, so pairing a
223
223
  // laptop creates no new visible folder; set it to make the work visible.
224
- root: z2.string().min(1).refine(isRootPath, { message: "must be an absolute path, or start with ~/" }).optional()
224
+ root: z2.string().min(1).refine(isRootPath, { message: "must be an absolute path, or start with ~/" }).optional(),
225
+ // An argv prefix the operator puts in front of every turn's runner: the companion starts
226
+ // `<prefix…> <its own run-turn command…>` instead of the runner bare. What the prefix does
227
+ // is the operator's (a box of their own making, ENG225); the companion knows nothing of it
228
+ // beyond handing the runner over, with the turn's inputs in its environment and the
229
+ // credential and secrets on its stdin (`runner/launch.ts`, `boxed`).
230
+ runner: z2.object({
231
+ command: z2.array(z2.string().min(1)).min(1)
232
+ }).strict().optional()
225
233
  });
226
234
  function retiredConfigKeys(raw) {
227
235
  if (!raw || typeof raw !== "object") return [];
package/dist/runtime.js CHANGED
@@ -295,7 +295,15 @@ var companionConfigSchema = z2.object({
295
295
  // and an agent with no folder of its own and no prepare hook works there
296
296
  // (`turn-execution.ts`, `prepareEnvironment`). Default `~/.cabane/work`: hidden, so pairing a
297
297
  // laptop creates no new visible folder; set it to make the work visible.
298
- root: z2.string().min(1).refine(isRootPath, { message: "must be an absolute path, or start with ~/" }).optional()
298
+ root: z2.string().min(1).refine(isRootPath, { message: "must be an absolute path, or start with ~/" }).optional(),
299
+ // An argv prefix the operator puts in front of every turn's runner: the companion starts
300
+ // `<prefix…> <its own run-turn command…>` instead of the runner bare. What the prefix does
301
+ // is the operator's (a box of their own making, ENG225); the companion knows nothing of it
302
+ // beyond handing the runner over, with the turn's inputs in its environment and the
303
+ // credential and secrets on its stdin (`runner/launch.ts`, `boxed`).
304
+ runner: z2.object({
305
+ command: z2.array(z2.string().min(1)).min(1)
306
+ }).strict().optional()
299
307
  });
300
308
  function workRoot(cfg) {
301
309
  const root = cfg.root;
@@ -7445,7 +7453,8 @@ var dispatchSchema = z14.object({
7445
7453
  episode: z14.number().int().optional()
7446
7454
  });
7447
7455
  var credentialsSchema = z14.object({
7448
- credential: z14.string().startsWith("cabrn_")
7456
+ credential: z14.string().startsWith("cabrn_"),
7457
+ secrets: z14.record(z14.string(), z14.string()).optional()
7449
7458
  });
7450
7459
  function runnerEnv(input) {
7451
7460
  return {
@@ -7457,7 +7466,10 @@ function runnerEnv(input) {
7457
7466
  };
7458
7467
  }
7459
7468
  function runnerStdin(input) {
7460
- return `${JSON.stringify({ credential: input.credential })}
7469
+ return `${JSON.stringify({
7470
+ credential: input.credential,
7471
+ ...input.secrets ? { secrets: input.secrets } : {}
7472
+ })}
7461
7473
  `;
7462
7474
  }
7463
7475
  function encodeLauncherMessage(message) {
@@ -7482,6 +7494,29 @@ function selfRunnerCommand() {
7482
7494
  if (!script) throw new Error("runner: cannot tell which script started this companion");
7483
7495
  return [process.execPath, ...process.execArgv, script, "run-turn"];
7484
7496
  }
7497
+ function wrappedRunnerCommand(prefix, own) {
7498
+ return [...prefix ?? [], ...own];
7499
+ }
7500
+ var BOXED_ENV_DROPPED = [
7501
+ "ANTHROPIC_API_KEY",
7502
+ "CLAUDE_CODE_OAUTH_TOKEN",
7503
+ "CODEX_API_KEY",
7504
+ "GH_TOKEN",
7505
+ "GITHUB_TOKEN",
7506
+ "DBUS_SESSION_BUS_ADDRESS",
7507
+ "XDG_RUNTIME_DIR"
7508
+ ];
7509
+ var BOXED_ENV_DROPPED_PREFIX = "CABANE_GATE_";
7510
+ function runnerExecEnv(base, boxed) {
7511
+ const env = {};
7512
+ for (const [k, v] of Object.entries(base)) {
7513
+ if (v === void 0) continue;
7514
+ if (boxed && (BOXED_ENV_DROPPED.includes(k) || k.startsWith(BOXED_ENV_DROPPED_PREFIX)))
7515
+ continue;
7516
+ env[k] = v;
7517
+ }
7518
+ return env;
7519
+ }
7485
7520
  function healthSnapshot(store) {
7486
7521
  const out = {};
7487
7522
  for (const r of store?.reports() ?? []) {
@@ -7686,13 +7721,12 @@ async function runTurnInRunner(opts, launch, duties, payload, handleOpts) {
7686
7721
  return { ok: false, durationMs: durationMs2, reason: reason2 };
7687
7722
  }
7688
7723
  try {
7689
- const env = {};
7690
- for (const [k, v] of Object.entries(launch.env ?? process.env)) {
7691
- if (v !== void 0) env[k] = v;
7692
- }
7724
+ const boxed = launch.boxed === true;
7725
+ const env = runnerExecEnv(launch.env ?? process.env, boxed);
7693
7726
  const input = {
7694
7727
  baseUrl: opts.baseUrl,
7695
7728
  credential: minted.credential,
7729
+ ...boxed ? { secrets: launch.secrets?.() ?? {} } : {},
7696
7730
  dispatch: payload,
7697
7731
  turnId,
7698
7732
  resumed: handleOpts.resumed,
@@ -7764,7 +7798,7 @@ async function runTurnInRunner(opts, launch, duties, payload, handleOpts) {
7764
7798
  locks?.release(lockOwner(event.id));
7765
7799
  return;
7766
7800
  case "process_group":
7767
- groups.push(event.pgid);
7801
+ if (!boxed) groups.push(event.pgid);
7768
7802
  return;
7769
7803
  case "remote_session":
7770
7804
  remote.push({ serverUrl: event.serverUrl, sessionId: event.sessionId });
@@ -7785,7 +7819,8 @@ async function runTurnInRunner(opts, launch, duties, payload, handleOpts) {
7785
7819
  }
7786
7820
  if (result) return result;
7787
7821
  const durationMs = Date.now() - startedAt;
7788
- const reason = thrown !== void 0 ? `runner_failed: ${thrown}` : `runner_exited: ${code}`;
7822
+ const stopped = controller.signal.aborted && thrown === void 0;
7823
+ const reason = stopped ? "cancelled" : thrown !== void 0 ? `runner_failed: ${thrown}` : `runner_exited: ${code}`;
7789
7824
  await child?.reap(groups);
7790
7825
  const reaped = await (launch.reapTurn ?? reapTurnScopes)(turnId).catch((err) => ({
7791
7826
  failures: [err instanceof Error ? err.message : String(err)]
@@ -7800,8 +7835,7 @@ async function runTurnInRunner(opts, launch, duties, payload, handleOpts) {
7800
7835
  await opts.api.setActiveRun(opts.workspaceId, payload.conversationId, payload.agentId, {
7801
7836
  activeRunStartedAt: null,
7802
7837
  turnId,
7803
- outcome: "failed",
7804
- errorReason: reason.slice(0, ERROR_REASON_MAX),
7838
+ ...stopped ? { outcome: "interrupted" } : { outcome: "failed", errorReason: reason.slice(0, ERROR_REASON_MAX) },
7805
7839
  readThrough: payload.messageId
7806
7840
  });
7807
7841
  } catch (err) {
@@ -7819,7 +7853,8 @@ async function runTurnInRunner(opts, launch, duties, payload, handleOpts) {
7819
7853
  reason: "exited"
7820
7854
  });
7821
7855
  if (thrown !== void 0) throw new Error(thrown);
7822
- log.warn({ turnId, code }, "runner: exited without reporting a result");
7856
+ if (stopped) log.info({ turnId, code }, "runner: stopped before it could report");
7857
+ else log.warn({ turnId, code }, "runner: exited without reporting a result");
7823
7858
  duties.notifyEnd({ id: payload.messageId, ok: false, durationMs, reason });
7824
7859
  return { ok: false, durationMs, reason };
7825
7860
  }
@@ -7881,9 +7916,9 @@ function secretsPath() {
7881
7916
  return join18(cabaneDir(), "secrets.json");
7882
7917
  }
7883
7918
  var secretStoreSchema = z16.record(z16.string(), z16.string());
7884
- function loadSecretStore() {
7919
+ function loadSecretValues() {
7885
7920
  const path = secretsPath();
7886
- if (!existsSync10(path)) return makeStore({});
7921
+ if (!existsSync10(path)) return {};
7887
7922
  let raw;
7888
7923
  try {
7889
7924
  raw = readFileSync8(path, "utf8");
@@ -7892,7 +7927,7 @@ function loadSecretStore() {
7892
7927
  `couldn't read ${path}: ${err instanceof Error ? err.message : String(err)}`
7893
7928
  );
7894
7929
  }
7895
- if (raw.trim().length === 0) return makeStore({});
7930
+ if (raw.trim().length === 0) return {};
7896
7931
  let parsed;
7897
7932
  try {
7898
7933
  parsed = JSON.parse(raw);
@@ -7907,14 +7942,17 @@ function loadSecretStore() {
7907
7942
  `${path} must be a flat object mapping secret names to string values (e.g. { "GITHUB_TOKEN": "ghp_\u2026" }).`
7908
7943
  );
7909
7944
  }
7910
- return makeStore(result.data);
7945
+ return result.data;
7911
7946
  }
7912
7947
  function loadSecretStoreTolerant(onWarn) {
7948
+ return makeStore(loadSecretValuesTolerant(onWarn));
7949
+ }
7950
+ function loadSecretValuesTolerant(onWarn) {
7913
7951
  try {
7914
- return loadSecretStore();
7952
+ return loadSecretValues();
7915
7953
  } catch (err) {
7916
7954
  onWarn?.(err instanceof Error ? err.message : String(err));
7917
- return makeStore({});
7955
+ return {};
7918
7956
  }
7919
7957
  }
7920
7958
  function makeStore(map) {
@@ -7939,6 +7977,130 @@ function requiredSecretNames(mcpServers) {
7939
7977
  return [...names].sort();
7940
7978
  }
7941
7979
 
7980
+ // src/harness-usable.ts
7981
+ import { spawn as spawn4 } from "child_process";
7982
+
7983
+ // src/harness-versions.ts
7984
+ var EMPTY = { claudeCode: null, opencode: null, codex: null };
7985
+ function parseVersionToken(raw) {
7986
+ if (!raw) return null;
7987
+ const m = raw.match(/\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?/);
7988
+ return m ? m[0] : null;
7989
+ }
7990
+ async function probeOpencodeVersion(serverUrl, fetchImpl = fetch) {
7991
+ try {
7992
+ const base = serverUrl.endsWith("/") ? serverUrl.slice(0, -1) : serverUrl;
7993
+ const res = await fetchImpl(`${base}/global/health`, {
7994
+ headers: { accept: "application/json" }
7995
+ });
7996
+ if (!res.ok) return null;
7997
+ return extractOpencodeVersion(await res.json());
7998
+ } catch {
7999
+ return null;
8000
+ }
8001
+ }
8002
+ function extractOpencodeVersion(json) {
8003
+ if (!json || typeof json !== "object") return null;
8004
+ const obj = json;
8005
+ const candidates = [obj.version];
8006
+ for (const v of Object.values(obj)) {
8007
+ if (v && typeof v === "object") candidates.push(v.version);
8008
+ }
8009
+ for (const c of candidates) {
8010
+ if (typeof c === "string") {
8011
+ const parsed = parseVersionToken(c);
8012
+ if (parsed) return parsed;
8013
+ }
8014
+ }
8015
+ return null;
8016
+ }
8017
+ function emptyHarnessVersions() {
8018
+ return { ...EMPTY };
8019
+ }
8020
+
8021
+ // src/harness-usable.ts
8022
+ var API_KEY_VARIABLES = {
8023
+ "claude-code": ["ANTHROPIC_API_KEY", "CLAUDE_CODE_OAUTH_TOKEN"],
8024
+ codex: ["CODEX_API_KEY"]
8025
+ };
8026
+ function apiKeyVariable(runtime, env = process.env) {
8027
+ return API_KEY_VARIABLES[runtime].find((name) => !!env[name]?.trim()) ?? null;
8028
+ }
8029
+ var CHECK_TIMEOUT_MS = 5e3;
8030
+ async function checkHarnessUsable(runtime, deps = {}) {
8031
+ const env = deps.env ?? process.env;
8032
+ const resolveBundled = deps.resolveBundled ?? ((r) => resolveBundledBinary(r));
8033
+ const run = deps.run ?? runBounded;
8034
+ const resolution = resolveBundled(runtime);
8035
+ if (resolution.status !== "present" || !resolution.path) {
8036
+ return {
8037
+ status: "bundle_missing",
8038
+ bundled: resolution.status === "present" ? "executable_missing" : resolution.status
8039
+ };
8040
+ }
8041
+ const path = resolution.path;
8042
+ const version = run(path, ["--version"], env).then(
8043
+ (r) => r.code === 0 ? parseVersionToken(r.output) : null
8044
+ );
8045
+ const variable = apiKeyVariable(runtime, env);
8046
+ if (variable) return { status: "api_key", variable, version: await version };
8047
+ const statusArgs = runtime === "codex" ? ["login", "status"] : ["auth", "status", "--json"];
8048
+ const [answer, v] = await Promise.all([run(path, statusArgs, env), version]);
8049
+ if (answer.error) return { status: "check_failed", reason: answer.error };
8050
+ const signedIn = runtime === "codex" ? readCodexStatus(answer) : readClaudeStatus(answer);
8051
+ if (typeof signedIn === "string") return { status: "check_failed", reason: signedIn };
8052
+ return signedIn ? { status: "signed_in", version: v } : { status: "signed_out", version: v };
8053
+ }
8054
+ function readClaudeStatus(answer) {
8055
+ const start = answer.output.indexOf("{");
8056
+ const end = answer.output.lastIndexOf("}");
8057
+ if (start < 0 || end < start) return "unreadable";
8058
+ try {
8059
+ const parsed = JSON.parse(answer.output.slice(start, end + 1));
8060
+ const loggedIn = parsed?.loggedIn;
8061
+ return typeof loggedIn === "boolean" ? loggedIn : "unreadable";
8062
+ } catch {
8063
+ return "unreadable";
8064
+ }
8065
+ }
8066
+ function readCodexStatus(answer) {
8067
+ if (answer.code === 0) return true;
8068
+ if (/not logged in/i.test(answer.output)) return false;
8069
+ if (/config/i.test(answer.output)) return "config";
8070
+ return "unreadable";
8071
+ }
8072
+ function runBounded(path, args, env) {
8073
+ return new Promise((resolve) => {
8074
+ let settled = false;
8075
+ let out = "";
8076
+ const done = (result) => {
8077
+ if (settled) return;
8078
+ settled = true;
8079
+ clearTimeout(timer);
8080
+ resolve(result);
8081
+ };
8082
+ let child;
8083
+ try {
8084
+ child = spawn4(path, args, { stdio: ["ignore", "pipe", "pipe"], env });
8085
+ } catch {
8086
+ resolve({ code: null, output: "", error: "spawn" });
8087
+ return;
8088
+ }
8089
+ const capture = (chunk) => {
8090
+ if (out.length < 8192) out += chunk.toString();
8091
+ };
8092
+ child.stdout?.on("data", capture);
8093
+ child.stderr?.on("data", capture);
8094
+ const timer = setTimeout(() => {
8095
+ child.kill("SIGKILL");
8096
+ done({ code: null, output: out, error: "timeout" });
8097
+ }, CHECK_TIMEOUT_MS);
8098
+ timer.unref?.();
8099
+ child.once("error", () => done({ code: null, output: out, error: "spawn" }));
8100
+ child.once("close", (code) => done({ code, output: out }));
8101
+ });
8102
+ }
8103
+
7942
8104
  // src/transcript-writer.ts
7943
8105
  import { appendFileSync, chmodSync as chmodSync4, copyFileSync, mkdirSync as mkdirSync8, readdirSync, rmSync as rmSync6 } from "fs";
7944
8106
  import { basename as basename3, dirname as dirname7, join as join19 } from "path";
@@ -8083,7 +8245,7 @@ function hostReport() {
8083
8245
  }
8084
8246
 
8085
8247
  // src/providers/local-process.ts
8086
- import { spawn as spawn4 } from "child_process";
8248
+ import { spawn as spawn5 } from "child_process";
8087
8249
  import { constants as constants2 } from "os";
8088
8250
  var DEFAULT_KILL_GRACE_MS = 3e4;
8089
8251
  var DEFAULT_REAP_GRACE_MS = 3e3;
@@ -8094,7 +8256,7 @@ function exitCode(code, signal) {
8094
8256
  }
8095
8257
  function createLocalProcessProvider(opts = {}) {
8096
8258
  const killGraceMs = opts.killGraceMs ?? DEFAULT_KILL_GRACE_MS;
8097
- const spawnFn = opts.spawnFn ?? spawn4;
8259
+ const spawnFn = opts.spawnFn ?? spawn5;
8098
8260
  const reapGraceMs = opts.reapGraceMs ?? DEFAULT_REAP_GRACE_MS;
8099
8261
  const killFn = opts.killFn ?? ((pid, signal) => process.kill(pid, signal));
8100
8262
  const groupAlive = (pgid) => {
@@ -8296,44 +8458,6 @@ function checkReason(check) {
8296
8458
  }
8297
8459
  }
8298
8460
 
8299
- // src/harness-versions.ts
8300
- var EMPTY = { claudeCode: null, opencode: null, codex: null };
8301
- function parseVersionToken(raw) {
8302
- if (!raw) return null;
8303
- const m = raw.match(/\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?/);
8304
- return m ? m[0] : null;
8305
- }
8306
- async function probeOpencodeVersion(serverUrl, fetchImpl = fetch) {
8307
- try {
8308
- const base = serverUrl.endsWith("/") ? serverUrl.slice(0, -1) : serverUrl;
8309
- const res = await fetchImpl(`${base}/global/health`, {
8310
- headers: { accept: "application/json" }
8311
- });
8312
- if (!res.ok) return null;
8313
- return extractOpencodeVersion(await res.json());
8314
- } catch {
8315
- return null;
8316
- }
8317
- }
8318
- function extractOpencodeVersion(json) {
8319
- if (!json || typeof json !== "object") return null;
8320
- const obj = json;
8321
- const candidates = [obj.version];
8322
- for (const v of Object.values(obj)) {
8323
- if (v && typeof v === "object") candidates.push(v.version);
8324
- }
8325
- for (const c of candidates) {
8326
- if (typeof c === "string") {
8327
- const parsed = parseVersionToken(c);
8328
- if (parsed) return parsed;
8329
- }
8330
- }
8331
- return null;
8332
- }
8333
- function emptyHarnessVersions() {
8334
- return { ...EMPTY };
8335
- }
8336
-
8337
8461
  // src/harness-copy.ts
8338
8462
  import { homedir as homedir8 } from "os";
8339
8463
  import { join as join21 } from "path";
@@ -8369,90 +8493,6 @@ function spaced(sentence) {
8369
8493
  return sentence ? ` ${sentence}` : "";
8370
8494
  }
8371
8495
 
8372
- // src/harness-usable.ts
8373
- import { spawn as spawn5 } from "child_process";
8374
- var API_KEY_VARIABLES = {
8375
- "claude-code": ["ANTHROPIC_API_KEY", "CLAUDE_CODE_OAUTH_TOKEN"],
8376
- codex: ["CODEX_API_KEY"]
8377
- };
8378
- function apiKeyVariable(runtime, env = process.env) {
8379
- return API_KEY_VARIABLES[runtime].find((name) => !!env[name]?.trim()) ?? null;
8380
- }
8381
- var CHECK_TIMEOUT_MS = 5e3;
8382
- async function checkHarnessUsable(runtime, deps = {}) {
8383
- const env = deps.env ?? process.env;
8384
- const resolveBundled = deps.resolveBundled ?? ((r) => resolveBundledBinary(r));
8385
- const run = deps.run ?? runBounded;
8386
- const resolution = resolveBundled(runtime);
8387
- if (resolution.status !== "present" || !resolution.path) {
8388
- return {
8389
- status: "bundle_missing",
8390
- bundled: resolution.status === "present" ? "executable_missing" : resolution.status
8391
- };
8392
- }
8393
- const path = resolution.path;
8394
- const version = run(path, ["--version"], env).then(
8395
- (r) => r.code === 0 ? parseVersionToken(r.output) : null
8396
- );
8397
- const variable = apiKeyVariable(runtime, env);
8398
- if (variable) return { status: "api_key", variable, version: await version };
8399
- const statusArgs = runtime === "codex" ? ["login", "status"] : ["auth", "status", "--json"];
8400
- const [answer, v] = await Promise.all([run(path, statusArgs, env), version]);
8401
- if (answer.error) return { status: "check_failed", reason: answer.error };
8402
- const signedIn = runtime === "codex" ? readCodexStatus(answer) : readClaudeStatus(answer);
8403
- if (typeof signedIn === "string") return { status: "check_failed", reason: signedIn };
8404
- return signedIn ? { status: "signed_in", version: v } : { status: "signed_out", version: v };
8405
- }
8406
- function readClaudeStatus(answer) {
8407
- const start = answer.output.indexOf("{");
8408
- const end = answer.output.lastIndexOf("}");
8409
- if (start < 0 || end < start) return "unreadable";
8410
- try {
8411
- const parsed = JSON.parse(answer.output.slice(start, end + 1));
8412
- const loggedIn = parsed?.loggedIn;
8413
- return typeof loggedIn === "boolean" ? loggedIn : "unreadable";
8414
- } catch {
8415
- return "unreadable";
8416
- }
8417
- }
8418
- function readCodexStatus(answer) {
8419
- if (answer.code === 0) return true;
8420
- if (/not logged in/i.test(answer.output)) return false;
8421
- if (/config/i.test(answer.output)) return "config";
8422
- return "unreadable";
8423
- }
8424
- function runBounded(path, args, env) {
8425
- return new Promise((resolve) => {
8426
- let settled = false;
8427
- let out = "";
8428
- const done = (result) => {
8429
- if (settled) return;
8430
- settled = true;
8431
- clearTimeout(timer);
8432
- resolve(result);
8433
- };
8434
- let child;
8435
- try {
8436
- child = spawn5(path, args, { stdio: ["ignore", "pipe", "pipe"], env });
8437
- } catch {
8438
- resolve({ code: null, output: "", error: "spawn" });
8439
- return;
8440
- }
8441
- const capture = (chunk) => {
8442
- if (out.length < 8192) out += chunk.toString();
8443
- };
8444
- child.stdout?.on("data", capture);
8445
- child.stderr?.on("data", capture);
8446
- const timer = setTimeout(() => {
8447
- child.kill("SIGKILL");
8448
- done({ code: null, output: out, error: "timeout" });
8449
- }, CHECK_TIMEOUT_MS);
8450
- timer.unref?.();
8451
- child.once("error", () => done({ code: null, output: out, error: "spawn" }));
8452
- child.once("close", (code) => done({ code, output: out }));
8453
- });
8454
- }
8455
-
8456
8496
  // src/harness-detect.ts
8457
8497
  var DEFAULT_OPENCODE_SERVER_URL = "http://127.0.0.1:4096";
8458
8498
  var DEFAULT_OPENCODE_PROBE_MS = 1e3;
@@ -10254,7 +10294,16 @@ var CompanionSupervisor = class {
10254
10294
  },
10255
10295
  runner: {
10256
10296
  provider: this.runnerProvider,
10257
- command: this.runnerCommand ?? selfRunnerCommand(),
10297
+ command: wrappedRunnerCommand(
10298
+ this.config.runner?.command,
10299
+ this.runnerCommand ?? selfRunnerCommand()
10300
+ ),
10301
+ ...this.config.runner?.command ? {
10302
+ boxed: true,
10303
+ secrets: () => loadSecretValuesTolerant(
10304
+ (m) => this.log.warn({ err: m }, "Could not read the secrets file for a boxed turn.")
10305
+ )
10306
+ } : {},
10258
10307
  ...this.runnerEnv ? { env: this.runnerEnv } : {},
10259
10308
  reportRunnerGone: (report) => this.reportRunnerGone(report),
10260
10309
  connectorHealth: this.connectorHealth,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cabane/companion",
3
- "version": "0.6.188",
3
+ "version": "0.6.189",
4
4
  "type": "module",
5
5
  "description": "The Cabane Companion (headless): connect an agent on your machine to your Cabane workspace as a responder — drive work against your own codebase, files, and MCP servers without putting any of it in Cabane.",
6
6
  "license": "UNLICENSED",