@cabane/companion 0.6.188 → 0.6.190

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/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;
@@ -6150,6 +6158,10 @@ function createSdkCodexTransport(opts = {}) {
6150
6158
  }
6151
6159
 
6152
6160
  // packages/agent-runtime/src/codex/index.ts
6161
+ function isMissingRollout(err) {
6162
+ const message = err instanceof Error ? err.message : String(err);
6163
+ return /no rollout found for thread id/i.test(message);
6164
+ }
6153
6165
  function createCodexAdapter(deps = {}) {
6154
6166
  const transport = deps.transport ?? (deps.enabled ? createSdkCodexTransport({ ...deps.apiKey ? { apiKey: deps.apiKey } : {} }) : null);
6155
6167
  return {
@@ -6168,8 +6180,8 @@ function createCodexAdapter(deps = {}) {
6168
6180
  }
6169
6181
  const directory = req.local.cwd ?? "";
6170
6182
  const decision = decideResume3(req.session, directory);
6171
- const resumeThreadId = "resume" in decision ? decision.resume : null;
6172
- const threadUsageBaseline = "resume" in decision ? decision.usageBaseline : null;
6183
+ let resumeThreadId = "resume" in decision ? decision.resume : null;
6184
+ let threadUsageBaseline = "resume" in decision ? decision.usageBaseline : null;
6173
6185
  if ("fresh" in decision && decision.reason === "cwd_mismatch") {
6174
6186
  deps.onWarn?.(
6175
6187
  "codex adapter: stored session directory no longer matches the current environment \u2014 starting a fresh thread (SJ527-analogue guard)",
@@ -6234,38 +6246,69 @@ function createCodexAdapter(deps = {}) {
6234
6246
  }
6235
6247
  if (signal.aborted) return;
6236
6248
  }
6237
- const spec = buildRunSpec2(req, resumeThreadId, threadStanding, images?.paths ?? []);
6238
- if (spec.model === null) {
6239
- deps.onWarn?.("Codex is using its default model because no model was resolved", {
6240
- model: req.config.model
6241
- });
6249
+ for (; ; ) {
6250
+ const spec = buildRunSpec2(req, resumeThreadId, threadStanding, images?.paths ?? []);
6251
+ if (spec.model === null) {
6252
+ deps.onWarn?.("Codex is using its default model because no model was resolved", {
6253
+ model: req.config.model
6254
+ });
6255
+ }
6256
+ let emitted = false;
6257
+ try {
6258
+ const result = await transport.run(spec, signal);
6259
+ if (signal.aborted) return;
6260
+ for await (const event of decodeCodexStream(result.events, {
6261
+ signal,
6262
+ cwd: req.local.cwd,
6263
+ resumedThreadId: resumeThreadId,
6264
+ degraded,
6265
+ // Which segments the thread carries once this turn lands. A segment sent is that one —
6266
+ // the later copy is what the model reads. One withheld is what the thread had. Read off
6267
+ // the spec, so the record can't disagree with what was sent.
6268
+ promptFingerprint: spec.preamble.core ? spec.fingerprints.core : threadStanding?.core ?? null,
6269
+ conversationFingerprint: spec.preamble.conversation ? spec.fingerprints.conversation : threadStanding?.conversation ?? null,
6270
+ compactions,
6271
+ // What the thread held before this turn, for the write at `thread.started`.
6272
+ heldStanding: {
6273
+ promptFingerprint: threadStanding?.core ?? null,
6274
+ conversationFingerprint: threadStanding?.conversation ?? null,
6275
+ compactions: threadStanding ? compactions : null
6276
+ },
6277
+ storedState: req.session,
6278
+ usageBaseline: threadUsageBaseline,
6279
+ // Codex reports no model in-stream, so record what the thread was started with (null on the
6280
+ // default token). Same for the reasoning effort.
6281
+ resolvedModel: spec.model,
6282
+ resolvedReasoningEffort: spec.modelReasoningEffort ?? null
6283
+ })) {
6284
+ emitted = true;
6285
+ yield event;
6286
+ }
6287
+ return;
6288
+ } catch (err) {
6289
+ if (resumeThreadId === null || emitted || signal.aborted || !isMissingRollout(err)) {
6290
+ throw err;
6291
+ }
6292
+ deps.onWarn?.(
6293
+ "codex adapter: the stored thread has no rollout on this host \u2014 starting a fresh thread",
6294
+ { threadId: resumeThreadId }
6295
+ );
6296
+ resumeThreadId = null;
6297
+ threadUsageBaseline = null;
6298
+ threadStanding = null;
6299
+ compactions = 0;
6300
+ req = { ...req, session: null };
6301
+ degraded = true;
6302
+ if (hooks?.fresh) {
6303
+ const recomposed = await hooks.fresh("session_start_failed");
6304
+ if (signal.aborted) return;
6305
+ if (recomposed) {
6306
+ req = { ...req, ...recomposed, session: null };
6307
+ degraded = false;
6308
+ }
6309
+ }
6310
+ }
6242
6311
  }
6243
- const result = await transport.run(spec, signal);
6244
- if (signal.aborted) return;
6245
- yield* decodeCodexStream(result.events, {
6246
- signal,
6247
- cwd: req.local.cwd,
6248
- resumedThreadId: resumeThreadId,
6249
- degraded,
6250
- // Which segments the thread carries once this turn lands. A segment sent is that one —
6251
- // the later copy is what the model reads. One withheld is what the thread had. Read off
6252
- // the spec, so the record can't disagree with what was sent.
6253
- promptFingerprint: spec.preamble.core ? spec.fingerprints.core : threadStanding?.core ?? null,
6254
- conversationFingerprint: spec.preamble.conversation ? spec.fingerprints.conversation : threadStanding?.conversation ?? null,
6255
- compactions,
6256
- // What the thread held before this turn, for the write at `thread.started`.
6257
- heldStanding: {
6258
- promptFingerprint: threadStanding?.core ?? null,
6259
- conversationFingerprint: threadStanding?.conversation ?? null,
6260
- compactions: threadStanding ? compactions : null
6261
- },
6262
- storedState: req.session,
6263
- usageBaseline: threadUsageBaseline,
6264
- // Codex reports no model in-stream, so record what the thread was started with (null on the
6265
- // default token). Same for the reasoning effort.
6266
- resolvedModel: spec.model,
6267
- resolvedReasoningEffort: spec.modelReasoningEffort ?? null
6268
- });
6269
6312
  } finally {
6270
6313
  if (images) {
6271
6314
  try {
@@ -7445,7 +7488,8 @@ var dispatchSchema = z14.object({
7445
7488
  episode: z14.number().int().optional()
7446
7489
  });
7447
7490
  var credentialsSchema = z14.object({
7448
- credential: z14.string().startsWith("cabrn_")
7491
+ credential: z14.string().startsWith("cabrn_"),
7492
+ secrets: z14.record(z14.string(), z14.string()).optional()
7449
7493
  });
7450
7494
  function runnerEnv(input) {
7451
7495
  return {
@@ -7457,7 +7501,10 @@ function runnerEnv(input) {
7457
7501
  };
7458
7502
  }
7459
7503
  function runnerStdin(input) {
7460
- return `${JSON.stringify({ credential: input.credential })}
7504
+ return `${JSON.stringify({
7505
+ credential: input.credential,
7506
+ ...input.secrets ? { secrets: input.secrets } : {}
7507
+ })}
7461
7508
  `;
7462
7509
  }
7463
7510
  function encodeLauncherMessage(message) {
@@ -7482,6 +7529,29 @@ function selfRunnerCommand() {
7482
7529
  if (!script) throw new Error("runner: cannot tell which script started this companion");
7483
7530
  return [process.execPath, ...process.execArgv, script, "run-turn"];
7484
7531
  }
7532
+ function wrappedRunnerCommand(prefix, own) {
7533
+ return [...prefix ?? [], ...own];
7534
+ }
7535
+ var BOXED_ENV_DROPPED = [
7536
+ "ANTHROPIC_API_KEY",
7537
+ "CLAUDE_CODE_OAUTH_TOKEN",
7538
+ "CODEX_API_KEY",
7539
+ "GH_TOKEN",
7540
+ "GITHUB_TOKEN",
7541
+ "DBUS_SESSION_BUS_ADDRESS",
7542
+ "XDG_RUNTIME_DIR"
7543
+ ];
7544
+ var BOXED_ENV_DROPPED_PREFIX = "CABANE_GATE_";
7545
+ function runnerExecEnv(base, boxed) {
7546
+ const env = {};
7547
+ for (const [k, v] of Object.entries(base)) {
7548
+ if (v === void 0) continue;
7549
+ if (boxed && (BOXED_ENV_DROPPED.includes(k) || k.startsWith(BOXED_ENV_DROPPED_PREFIX)))
7550
+ continue;
7551
+ env[k] = v;
7552
+ }
7553
+ return env;
7554
+ }
7485
7555
  function healthSnapshot(store) {
7486
7556
  const out = {};
7487
7557
  for (const r of store?.reports() ?? []) {
@@ -7686,13 +7756,12 @@ async function runTurnInRunner(opts, launch, duties, payload, handleOpts) {
7686
7756
  return { ok: false, durationMs: durationMs2, reason: reason2 };
7687
7757
  }
7688
7758
  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
- }
7759
+ const boxed = launch.boxed === true;
7760
+ const env = runnerExecEnv(launch.env ?? process.env, boxed);
7693
7761
  const input = {
7694
7762
  baseUrl: opts.baseUrl,
7695
7763
  credential: minted.credential,
7764
+ ...boxed ? { secrets: launch.secrets?.() ?? {} } : {},
7696
7765
  dispatch: payload,
7697
7766
  turnId,
7698
7767
  resumed: handleOpts.resumed,
@@ -7764,7 +7833,7 @@ async function runTurnInRunner(opts, launch, duties, payload, handleOpts) {
7764
7833
  locks?.release(lockOwner(event.id));
7765
7834
  return;
7766
7835
  case "process_group":
7767
- groups.push(event.pgid);
7836
+ if (!boxed) groups.push(event.pgid);
7768
7837
  return;
7769
7838
  case "remote_session":
7770
7839
  remote.push({ serverUrl: event.serverUrl, sessionId: event.sessionId });
@@ -7785,7 +7854,8 @@ async function runTurnInRunner(opts, launch, duties, payload, handleOpts) {
7785
7854
  }
7786
7855
  if (result) return result;
7787
7856
  const durationMs = Date.now() - startedAt;
7788
- const reason = thrown !== void 0 ? `runner_failed: ${thrown}` : `runner_exited: ${code}`;
7857
+ const stopped = controller.signal.aborted && thrown === void 0;
7858
+ const reason = stopped ? "cancelled" : thrown !== void 0 ? `runner_failed: ${thrown}` : `runner_exited: ${code}`;
7789
7859
  await child?.reap(groups);
7790
7860
  const reaped = await (launch.reapTurn ?? reapTurnScopes)(turnId).catch((err) => ({
7791
7861
  failures: [err instanceof Error ? err.message : String(err)]
@@ -7800,8 +7870,7 @@ async function runTurnInRunner(opts, launch, duties, payload, handleOpts) {
7800
7870
  await opts.api.setActiveRun(opts.workspaceId, payload.conversationId, payload.agentId, {
7801
7871
  activeRunStartedAt: null,
7802
7872
  turnId,
7803
- outcome: "failed",
7804
- errorReason: reason.slice(0, ERROR_REASON_MAX),
7873
+ ...stopped ? { outcome: "interrupted" } : { outcome: "failed", errorReason: reason.slice(0, ERROR_REASON_MAX) },
7805
7874
  readThrough: payload.messageId
7806
7875
  });
7807
7876
  } catch (err) {
@@ -7819,7 +7888,8 @@ async function runTurnInRunner(opts, launch, duties, payload, handleOpts) {
7819
7888
  reason: "exited"
7820
7889
  });
7821
7890
  if (thrown !== void 0) throw new Error(thrown);
7822
- log.warn({ turnId, code }, "runner: exited without reporting a result");
7891
+ if (stopped) log.info({ turnId, code }, "runner: stopped before it could report");
7892
+ else log.warn({ turnId, code }, "runner: exited without reporting a result");
7823
7893
  duties.notifyEnd({ id: payload.messageId, ok: false, durationMs, reason });
7824
7894
  return { ok: false, durationMs, reason };
7825
7895
  }
@@ -7881,9 +7951,9 @@ function secretsPath() {
7881
7951
  return join18(cabaneDir(), "secrets.json");
7882
7952
  }
7883
7953
  var secretStoreSchema = z16.record(z16.string(), z16.string());
7884
- function loadSecretStore() {
7954
+ function loadSecretValues() {
7885
7955
  const path = secretsPath();
7886
- if (!existsSync10(path)) return makeStore({});
7956
+ if (!existsSync10(path)) return {};
7887
7957
  let raw;
7888
7958
  try {
7889
7959
  raw = readFileSync8(path, "utf8");
@@ -7892,7 +7962,7 @@ function loadSecretStore() {
7892
7962
  `couldn't read ${path}: ${err instanceof Error ? err.message : String(err)}`
7893
7963
  );
7894
7964
  }
7895
- if (raw.trim().length === 0) return makeStore({});
7965
+ if (raw.trim().length === 0) return {};
7896
7966
  let parsed;
7897
7967
  try {
7898
7968
  parsed = JSON.parse(raw);
@@ -7907,14 +7977,17 @@ function loadSecretStore() {
7907
7977
  `${path} must be a flat object mapping secret names to string values (e.g. { "GITHUB_TOKEN": "ghp_\u2026" }).`
7908
7978
  );
7909
7979
  }
7910
- return makeStore(result.data);
7980
+ return result.data;
7911
7981
  }
7912
7982
  function loadSecretStoreTolerant(onWarn) {
7983
+ return makeStore(loadSecretValuesTolerant(onWarn));
7984
+ }
7985
+ function loadSecretValuesTolerant(onWarn) {
7913
7986
  try {
7914
- return loadSecretStore();
7987
+ return loadSecretValues();
7915
7988
  } catch (err) {
7916
7989
  onWarn?.(err instanceof Error ? err.message : String(err));
7917
- return makeStore({});
7990
+ return {};
7918
7991
  }
7919
7992
  }
7920
7993
  function makeStore(map) {
@@ -7939,6 +8012,130 @@ function requiredSecretNames(mcpServers) {
7939
8012
  return [...names].sort();
7940
8013
  }
7941
8014
 
8015
+ // src/harness-usable.ts
8016
+ import { spawn as spawn4 } from "child_process";
8017
+
8018
+ // src/harness-versions.ts
8019
+ var EMPTY = { claudeCode: null, opencode: null, codex: null };
8020
+ function parseVersionToken(raw) {
8021
+ if (!raw) return null;
8022
+ const m = raw.match(/\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?/);
8023
+ return m ? m[0] : null;
8024
+ }
8025
+ async function probeOpencodeVersion(serverUrl, fetchImpl = fetch) {
8026
+ try {
8027
+ const base = serverUrl.endsWith("/") ? serverUrl.slice(0, -1) : serverUrl;
8028
+ const res = await fetchImpl(`${base}/global/health`, {
8029
+ headers: { accept: "application/json" }
8030
+ });
8031
+ if (!res.ok) return null;
8032
+ return extractOpencodeVersion(await res.json());
8033
+ } catch {
8034
+ return null;
8035
+ }
8036
+ }
8037
+ function extractOpencodeVersion(json) {
8038
+ if (!json || typeof json !== "object") return null;
8039
+ const obj = json;
8040
+ const candidates = [obj.version];
8041
+ for (const v of Object.values(obj)) {
8042
+ if (v && typeof v === "object") candidates.push(v.version);
8043
+ }
8044
+ for (const c of candidates) {
8045
+ if (typeof c === "string") {
8046
+ const parsed = parseVersionToken(c);
8047
+ if (parsed) return parsed;
8048
+ }
8049
+ }
8050
+ return null;
8051
+ }
8052
+ function emptyHarnessVersions() {
8053
+ return { ...EMPTY };
8054
+ }
8055
+
8056
+ // src/harness-usable.ts
8057
+ var API_KEY_VARIABLES = {
8058
+ "claude-code": ["ANTHROPIC_API_KEY", "CLAUDE_CODE_OAUTH_TOKEN"],
8059
+ codex: ["CODEX_API_KEY"]
8060
+ };
8061
+ function apiKeyVariable(runtime, env = process.env) {
8062
+ return API_KEY_VARIABLES[runtime].find((name) => !!env[name]?.trim()) ?? null;
8063
+ }
8064
+ var CHECK_TIMEOUT_MS = 5e3;
8065
+ async function checkHarnessUsable(runtime, deps = {}) {
8066
+ const env = deps.env ?? process.env;
8067
+ const resolveBundled = deps.resolveBundled ?? ((r) => resolveBundledBinary(r));
8068
+ const run = deps.run ?? runBounded;
8069
+ const resolution = resolveBundled(runtime);
8070
+ if (resolution.status !== "present" || !resolution.path) {
8071
+ return {
8072
+ status: "bundle_missing",
8073
+ bundled: resolution.status === "present" ? "executable_missing" : resolution.status
8074
+ };
8075
+ }
8076
+ const path = resolution.path;
8077
+ const version = run(path, ["--version"], env).then(
8078
+ (r) => r.code === 0 ? parseVersionToken(r.output) : null
8079
+ );
8080
+ const variable = apiKeyVariable(runtime, env);
8081
+ if (variable) return { status: "api_key", variable, version: await version };
8082
+ const statusArgs = runtime === "codex" ? ["login", "status"] : ["auth", "status", "--json"];
8083
+ const [answer, v] = await Promise.all([run(path, statusArgs, env), version]);
8084
+ if (answer.error) return { status: "check_failed", reason: answer.error };
8085
+ const signedIn = runtime === "codex" ? readCodexStatus(answer) : readClaudeStatus(answer);
8086
+ if (typeof signedIn === "string") return { status: "check_failed", reason: signedIn };
8087
+ return signedIn ? { status: "signed_in", version: v } : { status: "signed_out", version: v };
8088
+ }
8089
+ function readClaudeStatus(answer) {
8090
+ const start = answer.output.indexOf("{");
8091
+ const end = answer.output.lastIndexOf("}");
8092
+ if (start < 0 || end < start) return "unreadable";
8093
+ try {
8094
+ const parsed = JSON.parse(answer.output.slice(start, end + 1));
8095
+ const loggedIn = parsed?.loggedIn;
8096
+ return typeof loggedIn === "boolean" ? loggedIn : "unreadable";
8097
+ } catch {
8098
+ return "unreadable";
8099
+ }
8100
+ }
8101
+ function readCodexStatus(answer) {
8102
+ if (answer.code === 0) return true;
8103
+ if (/not logged in/i.test(answer.output)) return false;
8104
+ if (/config/i.test(answer.output)) return "config";
8105
+ return "unreadable";
8106
+ }
8107
+ function runBounded(path, args, env) {
8108
+ return new Promise((resolve) => {
8109
+ let settled = false;
8110
+ let out = "";
8111
+ const done = (result) => {
8112
+ if (settled) return;
8113
+ settled = true;
8114
+ clearTimeout(timer);
8115
+ resolve(result);
8116
+ };
8117
+ let child;
8118
+ try {
8119
+ child = spawn4(path, args, { stdio: ["ignore", "pipe", "pipe"], env });
8120
+ } catch {
8121
+ resolve({ code: null, output: "", error: "spawn" });
8122
+ return;
8123
+ }
8124
+ const capture = (chunk) => {
8125
+ if (out.length < 8192) out += chunk.toString();
8126
+ };
8127
+ child.stdout?.on("data", capture);
8128
+ child.stderr?.on("data", capture);
8129
+ const timer = setTimeout(() => {
8130
+ child.kill("SIGKILL");
8131
+ done({ code: null, output: out, error: "timeout" });
8132
+ }, CHECK_TIMEOUT_MS);
8133
+ timer.unref?.();
8134
+ child.once("error", () => done({ code: null, output: out, error: "spawn" }));
8135
+ child.once("close", (code) => done({ code, output: out }));
8136
+ });
8137
+ }
8138
+
7942
8139
  // src/transcript-writer.ts
7943
8140
  import { appendFileSync, chmodSync as chmodSync4, copyFileSync, mkdirSync as mkdirSync8, readdirSync, rmSync as rmSync6 } from "fs";
7944
8141
  import { basename as basename3, dirname as dirname7, join as join19 } from "path";
@@ -8083,7 +8280,7 @@ function hostReport() {
8083
8280
  }
8084
8281
 
8085
8282
  // src/providers/local-process.ts
8086
- import { spawn as spawn4 } from "child_process";
8283
+ import { spawn as spawn5 } from "child_process";
8087
8284
  import { constants as constants2 } from "os";
8088
8285
  var DEFAULT_KILL_GRACE_MS = 3e4;
8089
8286
  var DEFAULT_REAP_GRACE_MS = 3e3;
@@ -8094,7 +8291,7 @@ function exitCode(code, signal) {
8094
8291
  }
8095
8292
  function createLocalProcessProvider(opts = {}) {
8096
8293
  const killGraceMs = opts.killGraceMs ?? DEFAULT_KILL_GRACE_MS;
8097
- const spawnFn = opts.spawnFn ?? spawn4;
8294
+ const spawnFn = opts.spawnFn ?? spawn5;
8098
8295
  const reapGraceMs = opts.reapGraceMs ?? DEFAULT_REAP_GRACE_MS;
8099
8296
  const killFn = opts.killFn ?? ((pid, signal) => process.kill(pid, signal));
8100
8297
  const groupAlive = (pgid) => {
@@ -8296,44 +8493,6 @@ function checkReason(check) {
8296
8493
  }
8297
8494
  }
8298
8495
 
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
8496
  // src/harness-copy.ts
8338
8497
  import { homedir as homedir8 } from "os";
8339
8498
  import { join as join21 } from "path";
@@ -8369,90 +8528,6 @@ function spaced(sentence) {
8369
8528
  return sentence ? ` ${sentence}` : "";
8370
8529
  }
8371
8530
 
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
8531
  // src/harness-detect.ts
8457
8532
  var DEFAULT_OPENCODE_SERVER_URL = "http://127.0.0.1:4096";
8458
8533
  var DEFAULT_OPENCODE_PROBE_MS = 1e3;
@@ -10254,7 +10329,16 @@ var CompanionSupervisor = class {
10254
10329
  },
10255
10330
  runner: {
10256
10331
  provider: this.runnerProvider,
10257
- command: this.runnerCommand ?? selfRunnerCommand(),
10332
+ command: wrappedRunnerCommand(
10333
+ this.config.runner?.command,
10334
+ this.runnerCommand ?? selfRunnerCommand()
10335
+ ),
10336
+ ...this.config.runner?.command ? {
10337
+ boxed: true,
10338
+ secrets: () => loadSecretValuesTolerant(
10339
+ (m) => this.log.warn({ err: m }, "Could not read the secrets file for a boxed turn.")
10340
+ )
10341
+ } : {},
10258
10342
  ...this.runnerEnv ? { env: this.runnerEnv } : {},
10259
10343
  reportRunnerGone: (report) => this.reportRunnerGone(report),
10260
10344
  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.190",
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",