@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/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) {
@@ -5828,6 +5972,10 @@ var init_transport2 = __esm({
5828
5972
  });
5829
5973
 
5830
5974
  // packages/agent-runtime/src/codex/index.ts
5975
+ function isMissingRollout(err) {
5976
+ const message2 = err instanceof Error ? err.message : String(err);
5977
+ return /no rollout found for thread id/i.test(message2);
5978
+ }
5831
5979
  function createCodexAdapter(deps = {}) {
5832
5980
  const transport = deps.transport ?? (deps.enabled ? createSdkCodexTransport({ ...deps.apiKey ? { apiKey: deps.apiKey } : {} }) : null);
5833
5981
  return {
@@ -5846,8 +5994,8 @@ function createCodexAdapter(deps = {}) {
5846
5994
  }
5847
5995
  const directory = req.local.cwd ?? "";
5848
5996
  const decision = decideResume3(req.session, directory);
5849
- const resumeThreadId = "resume" in decision ? decision.resume : null;
5850
- const threadUsageBaseline = "resume" in decision ? decision.usageBaseline : null;
5997
+ let resumeThreadId = "resume" in decision ? decision.resume : null;
5998
+ let threadUsageBaseline = "resume" in decision ? decision.usageBaseline : null;
5851
5999
  if ("fresh" in decision && decision.reason === "cwd_mismatch") {
5852
6000
  deps.onWarn?.(
5853
6001
  "codex adapter: stored session directory no longer matches the current environment \u2014 starting a fresh thread (SJ527-analogue guard)",
@@ -5912,38 +6060,69 @@ function createCodexAdapter(deps = {}) {
5912
6060
  }
5913
6061
  if (signal.aborted) return;
5914
6062
  }
5915
- const spec = buildRunSpec2(req, resumeThreadId, threadStanding, images?.paths ?? []);
5916
- if (spec.model === null) {
5917
- deps.onWarn?.("Codex is using its default model because no model was resolved", {
5918
- model: req.config.model
5919
- });
6063
+ for (; ; ) {
6064
+ const spec = buildRunSpec2(req, resumeThreadId, threadStanding, images?.paths ?? []);
6065
+ if (spec.model === null) {
6066
+ deps.onWarn?.("Codex is using its default model because no model was resolved", {
6067
+ model: req.config.model
6068
+ });
6069
+ }
6070
+ let emitted = false;
6071
+ try {
6072
+ const result = await transport.run(spec, signal);
6073
+ if (signal.aborted) return;
6074
+ for await (const event of decodeCodexStream(result.events, {
6075
+ signal,
6076
+ cwd: req.local.cwd,
6077
+ resumedThreadId: resumeThreadId,
6078
+ degraded,
6079
+ // Which segments the thread carries once this turn lands. A segment sent is that one —
6080
+ // the later copy is what the model reads. One withheld is what the thread had. Read off
6081
+ // the spec, so the record can't disagree with what was sent.
6082
+ promptFingerprint: spec.preamble.core ? spec.fingerprints.core : threadStanding?.core ?? null,
6083
+ conversationFingerprint: spec.preamble.conversation ? spec.fingerprints.conversation : threadStanding?.conversation ?? null,
6084
+ compactions,
6085
+ // What the thread held before this turn, for the write at `thread.started`.
6086
+ heldStanding: {
6087
+ promptFingerprint: threadStanding?.core ?? null,
6088
+ conversationFingerprint: threadStanding?.conversation ?? null,
6089
+ compactions: threadStanding ? compactions : null
6090
+ },
6091
+ storedState: req.session,
6092
+ usageBaseline: threadUsageBaseline,
6093
+ // Codex reports no model in-stream, so record what the thread was started with (null on the
6094
+ // default token). Same for the reasoning effort.
6095
+ resolvedModel: spec.model,
6096
+ resolvedReasoningEffort: spec.modelReasoningEffort ?? null
6097
+ })) {
6098
+ emitted = true;
6099
+ yield event;
6100
+ }
6101
+ return;
6102
+ } catch (err) {
6103
+ if (resumeThreadId === null || emitted || signal.aborted || !isMissingRollout(err)) {
6104
+ throw err;
6105
+ }
6106
+ deps.onWarn?.(
6107
+ "codex adapter: the stored thread has no rollout on this host \u2014 starting a fresh thread",
6108
+ { threadId: resumeThreadId }
6109
+ );
6110
+ resumeThreadId = null;
6111
+ threadUsageBaseline = null;
6112
+ threadStanding = null;
6113
+ compactions = 0;
6114
+ req = { ...req, session: null };
6115
+ degraded = true;
6116
+ if (hooks?.fresh) {
6117
+ const recomposed = await hooks.fresh("session_start_failed");
6118
+ if (signal.aborted) return;
6119
+ if (recomposed) {
6120
+ req = { ...req, ...recomposed, session: null };
6121
+ degraded = false;
6122
+ }
6123
+ }
6124
+ }
5920
6125
  }
5921
- const result = await transport.run(spec, signal);
5922
- if (signal.aborted) return;
5923
- yield* decodeCodexStream(result.events, {
5924
- signal,
5925
- cwd: req.local.cwd,
5926
- resumedThreadId: resumeThreadId,
5927
- degraded,
5928
- // Which segments the thread carries once this turn lands. A segment sent is that one —
5929
- // the later copy is what the model reads. One withheld is what the thread had. Read off
5930
- // the spec, so the record can't disagree with what was sent.
5931
- promptFingerprint: spec.preamble.core ? spec.fingerprints.core : threadStanding?.core ?? null,
5932
- conversationFingerprint: spec.preamble.conversation ? spec.fingerprints.conversation : threadStanding?.conversation ?? null,
5933
- compactions,
5934
- // What the thread held before this turn, for the write at `thread.started`.
5935
- heldStanding: {
5936
- promptFingerprint: threadStanding?.core ?? null,
5937
- conversationFingerprint: threadStanding?.conversation ?? null,
5938
- compactions: threadStanding ? compactions : null
5939
- },
5940
- storedState: req.session,
5941
- usageBaseline: threadUsageBaseline,
5942
- // Codex reports no model in-stream, so record what the thread was started with (null on the
5943
- // default token). Same for the reasoning effort.
5944
- resolvedModel: spec.model,
5945
- resolvedReasoningEffort: spec.modelReasoningEffort ?? null
5946
- });
5947
6126
  } finally {
5948
6127
  if (images) {
5949
6128
  try {
@@ -7365,6 +7544,14 @@ function required(env, name) {
7365
7544
  if (!value) throw new Error(`runner: ${name} is not set`);
7366
7545
  return value;
7367
7546
  }
7547
+ function runnerPlace(env) {
7548
+ const workRoot2 = env[PLACE_ENV.workRoot];
7549
+ const transcriptDir = env[PLACE_ENV.transcriptDir];
7550
+ return {
7551
+ ...workRoot2 ? { workRoot: workRoot2 } : {},
7552
+ ...transcriptDir ? { transcriptDir } : {}
7553
+ };
7554
+ }
7368
7555
  function parseRunnerInput(env, stdin) {
7369
7556
  const dispatch = dispatchSchema.parse(JSON.parse(required(env, RUNNER_ENV.dispatch)));
7370
7557
  const spec = runnerSpecSchema.parse(JSON.parse(required(env, RUNNER_ENV.spec)));
@@ -7372,10 +7559,12 @@ function parseRunnerInput(env, stdin) {
7372
7559
  return {
7373
7560
  baseUrl: required(env, RUNNER_ENV.url),
7374
7561
  credential: credentials.credential,
7562
+ ...credentials.secrets ? { secrets: credentials.secrets } : {},
7375
7563
  dispatch,
7376
7564
  turnId: required(env, RUNNER_ENV.turnId),
7377
7565
  resumed: env[RUNNER_ENV.resumed] === "1",
7378
- spec
7566
+ spec,
7567
+ place: runnerPlace(env)
7379
7568
  };
7380
7569
  }
7381
7570
  function runnerEnv(input) {
@@ -7388,7 +7577,10 @@ function runnerEnv(input) {
7388
7577
  };
7389
7578
  }
7390
7579
  function runnerStdin(input) {
7391
- return `${JSON.stringify({ credential: input.credential })}
7580
+ return `${JSON.stringify({
7581
+ credential: input.credential,
7582
+ ...input.secrets ? { secrets: input.secrets } : {}
7583
+ })}
7392
7584
  `;
7393
7585
  }
7394
7586
  function encodeLauncherMessage(message2) {
@@ -7418,7 +7610,7 @@ function decodeRunnerEvent(line) {
7418
7610
  if (!value || typeof value !== "object" || !("cabaneRunner" in value)) return null;
7419
7611
  return value;
7420
7612
  }
7421
- var RUNNER_ENV, runtimeHealthSchema, runnerSpecSchema, dispatchSchema, credentialsSchema;
7613
+ var RUNNER_ENV, PLACE_ENV, runtimeHealthSchema, runnerSpecSchema, dispatchSchema, credentialsSchema;
7422
7614
  var init_protocol = __esm({
7423
7615
  "src/runner/protocol.ts"() {
7424
7616
  "use strict";
@@ -7437,6 +7629,10 @@ var init_protocol = __esm({
7437
7629
  // Everything else the turn is constructed from, as JSON: see `RunnerSpec`.
7438
7630
  spec: "CABANE_RUNNER_SPEC"
7439
7631
  };
7632
+ PLACE_ENV = {
7633
+ workRoot: "CABANE_WORK_ROOT",
7634
+ transcriptDir: "CABANE_TRANSCRIPT_DIR"
7635
+ };
7440
7636
  runtimeHealthSchema = z14.object({
7441
7637
  quotaState: z14.enum(["ok", "limited", "auth_expired", "unknown"]),
7442
7638
  limitedUntil: z14.string().nullable(),
@@ -7486,7 +7682,8 @@ var init_protocol = __esm({
7486
7682
  episode: z14.number().int().optional()
7487
7683
  });
7488
7684
  credentialsSchema = z14.object({
7489
- credential: z14.string().startsWith("cabrn_")
7685
+ credential: z14.string().startsWith("cabrn_"),
7686
+ secrets: z14.record(z14.string(), z14.string()).optional()
7490
7687
  });
7491
7688
  }
7492
7689
  });
@@ -7498,6 +7695,19 @@ function selfRunnerCommand() {
7498
7695
  if (!script) throw new Error("runner: cannot tell which script started this companion");
7499
7696
  return [process.execPath, ...process.execArgv, script, "run-turn"];
7500
7697
  }
7698
+ function wrappedRunnerCommand(prefix, own) {
7699
+ return [...prefix ?? [], ...own];
7700
+ }
7701
+ function runnerExecEnv(base, boxed) {
7702
+ const env = {};
7703
+ for (const [k, v] of Object.entries(base)) {
7704
+ if (v === void 0) continue;
7705
+ if (boxed && (BOXED_ENV_DROPPED.includes(k) || k.startsWith(BOXED_ENV_DROPPED_PREFIX)))
7706
+ continue;
7707
+ env[k] = v;
7708
+ }
7709
+ return env;
7710
+ }
7501
7711
  function healthSnapshot(store) {
7502
7712
  const out = {};
7503
7713
  for (const r of store?.reports() ?? []) {
@@ -7694,13 +7904,12 @@ async function runTurnInRunner(opts, launch, duties, payload, handleOpts) {
7694
7904
  return { ok: false, durationMs: durationMs2, reason: reason2 };
7695
7905
  }
7696
7906
  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
- }
7907
+ const boxed = launch.boxed === true;
7908
+ const env = runnerExecEnv(launch.env ?? process.env, boxed);
7701
7909
  const input = {
7702
7910
  baseUrl: opts.baseUrl,
7703
7911
  credential: minted.credential,
7912
+ ...boxed ? { secrets: launch.secrets?.() ?? {} } : {},
7704
7913
  dispatch: payload,
7705
7914
  turnId,
7706
7915
  resumed: handleOpts.resumed,
@@ -7772,7 +7981,7 @@ async function runTurnInRunner(opts, launch, duties, payload, handleOpts) {
7772
7981
  locks?.release(lockOwner(event.id));
7773
7982
  return;
7774
7983
  case "process_group":
7775
- groups.push(event.pgid);
7984
+ if (!boxed) groups.push(event.pgid);
7776
7985
  return;
7777
7986
  case "remote_session":
7778
7987
  remote.push({ serverUrl: event.serverUrl, sessionId: event.sessionId });
@@ -7793,7 +8002,8 @@ async function runTurnInRunner(opts, launch, duties, payload, handleOpts) {
7793
8002
  }
7794
8003
  if (result) return result;
7795
8004
  const durationMs = Date.now() - startedAt;
7796
- const reason = thrown !== void 0 ? `runner_failed: ${thrown}` : `runner_exited: ${code}`;
8005
+ const stopped = controller.signal.aborted && thrown === void 0;
8006
+ const reason = stopped ? "cancelled" : thrown !== void 0 ? `runner_failed: ${thrown}` : `runner_exited: ${code}`;
7797
8007
  await child?.reap(groups);
7798
8008
  const reaped = await (launch.reapTurn ?? reapTurnScopes)(turnId).catch((err) => ({
7799
8009
  failures: [err instanceof Error ? err.message : String(err)]
@@ -7808,8 +8018,7 @@ async function runTurnInRunner(opts, launch, duties, payload, handleOpts) {
7808
8018
  await opts.api.setActiveRun(opts.workspaceId, payload.conversationId, payload.agentId, {
7809
8019
  activeRunStartedAt: null,
7810
8020
  turnId,
7811
- outcome: "failed",
7812
- errorReason: reason.slice(0, ERROR_REASON_MAX),
8021
+ ...stopped ? { outcome: "interrupted" } : { outcome: "failed", errorReason: reason.slice(0, ERROR_REASON_MAX) },
7813
8022
  readThrough: payload.messageId
7814
8023
  });
7815
8024
  } catch (err) {
@@ -7827,11 +8036,12 @@ async function runTurnInRunner(opts, launch, duties, payload, handleOpts) {
7827
8036
  reason: "exited"
7828
8037
  });
7829
8038
  if (thrown !== void 0) throw new Error(thrown);
7830
- log2.warn({ turnId, code }, "runner: exited without reporting a result");
8039
+ if (stopped) log2.info({ turnId, code }, "runner: stopped before it could report");
8040
+ else log2.warn({ turnId, code }, "runner: exited without reporting a result");
7831
8041
  duties.notifyEnd({ id: payload.messageId, ok: false, durationMs, reason });
7832
8042
  return { ok: false, durationMs, reason };
7833
8043
  }
7834
- var PINO_LEVELS;
8044
+ var BOXED_ENV_DROPPED, BOXED_ENV_DROPPED_PREFIX, PINO_LEVELS;
7835
8045
  var init_launch = __esm({
7836
8046
  "src/runner/launch.ts"() {
7837
8047
  "use strict";
@@ -7842,6 +8052,16 @@ var init_launch = __esm({
7842
8052
  init_boot_id();
7843
8053
  init_turn_containment();
7844
8054
  init_protocol();
8055
+ BOXED_ENV_DROPPED = [
8056
+ "ANTHROPIC_API_KEY",
8057
+ "CLAUDE_CODE_OAUTH_TOKEN",
8058
+ "CODEX_API_KEY",
8059
+ "GH_TOKEN",
8060
+ "GITHUB_TOKEN",
8061
+ "DBUS_SESSION_BUS_ADDRESS",
8062
+ "XDG_RUNTIME_DIR"
8063
+ ];
8064
+ BOXED_ENV_DROPPED_PREFIX = "CABANE_GATE_";
7845
8065
  PINO_LEVELS = {
7846
8066
  10: "trace",
7847
8067
  20: "debug",
@@ -8456,9 +8676,9 @@ import { z as z16 } from "zod";
8456
8676
  function secretsPath() {
8457
8677
  return join19(cabaneDir(), "secrets.json");
8458
8678
  }
8459
- function loadSecretStore() {
8679
+ function loadSecretValues() {
8460
8680
  const path = secretsPath();
8461
- if (!existsSync10(path)) return makeStore({});
8681
+ if (!existsSync10(path)) return {};
8462
8682
  let raw;
8463
8683
  try {
8464
8684
  raw = readFileSync8(path, "utf8");
@@ -8467,7 +8687,7 @@ function loadSecretStore() {
8467
8687
  `couldn't read ${path}: ${err instanceof Error ? err.message : String(err)}`
8468
8688
  );
8469
8689
  }
8470
- if (raw.trim().length === 0) return makeStore({});
8690
+ if (raw.trim().length === 0) return {};
8471
8691
  let parsed;
8472
8692
  try {
8473
8693
  parsed = JSON.parse(raw);
@@ -8482,14 +8702,17 @@ function loadSecretStore() {
8482
8702
  `${path} must be a flat object mapping secret names to string values (e.g. { "GITHUB_TOKEN": "ghp_\u2026" }).`
8483
8703
  );
8484
8704
  }
8485
- return makeStore(result.data);
8705
+ return result.data;
8486
8706
  }
8487
8707
  function loadSecretStoreTolerant(onWarn) {
8708
+ return makeStore(loadSecretValuesTolerant(onWarn));
8709
+ }
8710
+ function loadSecretValuesTolerant(onWarn) {
8488
8711
  try {
8489
- return loadSecretStore();
8712
+ return loadSecretValues();
8490
8713
  } catch (err) {
8491
8714
  onWarn?.(err instanceof Error ? err.message : String(err));
8492
- return makeStore({});
8715
+ return {};
8493
8716
  }
8494
8717
  }
8495
8718
  function makeStore(map) {
@@ -8581,6 +8804,31 @@ var init_secrets = __esm({
8581
8804
  }
8582
8805
  });
8583
8806
 
8807
+ // src/turn-env.ts
8808
+ function harnessLoginEnv(store, runtime) {
8809
+ const names = [
8810
+ ...runtime in API_KEY_VARIABLES ? API_KEY_VARIABLES[runtime] : [],
8811
+ GITHUB_TOKEN_NAME
8812
+ ];
8813
+ const env = {};
8814
+ for (const name of names) {
8815
+ const value = store.get(name);
8816
+ if (value) env[name] = value;
8817
+ }
8818
+ return env;
8819
+ }
8820
+ function turnIdentityEnv(ids) {
8821
+ return { CABANE_CONVERSATION_ID: ids.conversationId, CABANE_AGENT_ID: ids.agentId };
8822
+ }
8823
+ var GITHUB_TOKEN_NAME;
8824
+ var init_turn_env = __esm({
8825
+ "src/turn-env.ts"() {
8826
+ "use strict";
8827
+ init_harness_usable();
8828
+ GITHUB_TOKEN_NAME = "GH_TOKEN";
8829
+ }
8830
+ });
8831
+
8584
8832
  // src/transcript-writer.ts
8585
8833
  import { appendFileSync, chmodSync as chmodSync4, copyFileSync, mkdirSync as mkdirSync8, readdirSync, rmSync as rmSync6 } from "fs";
8586
8834
  import { basename as basename3, dirname as dirname7, join as join20 } from "path";
@@ -8971,6 +9219,7 @@ var init_turn_execution = __esm({
8971
9219
  init_prepare_failure();
8972
9220
  init_prepared();
8973
9221
  init_secrets();
9222
+ init_turn_env();
8974
9223
  init_turn_control_tools();
8975
9224
  init_transcript_writer();
8976
9225
  init_turn_containment();
@@ -9038,6 +9287,7 @@ var init_turn_execution = __esm({
9038
9287
  // The harness's secret environment for this turn. Only ever handed to the
9039
9288
  // adapter; never logged, never merged into `process.env`.
9040
9289
  secretEnv = {};
9290
+ secretStore;
9041
9291
  effectiveCwd;
9042
9292
  hookEnv;
9043
9293
  turnEnv;
@@ -9326,7 +9576,8 @@ var init_turn_execution = __esm({
9326
9576
  throw this.concluded(reason, reason);
9327
9577
  }
9328
9578
  this.resolvedMcpServers = resolvedMcpServers;
9329
- this.secretEnv = secretEnv;
9579
+ this.secretEnv = { ...secretEnv, ...harnessLoginEnv(secretStore, this.turnContext.runtime) };
9580
+ this.secretStore = secretStore;
9330
9581
  }
9331
9582
  async prepareEnvironment() {
9332
9583
  const { payload, workspaceId, turnId, turnLog } = this;
@@ -9477,6 +9728,10 @@ var init_turn_execution = __esm({
9477
9728
  }
9478
9729
  const turnEnv = {
9479
9730
  ...hookEnv,
9731
+ ...turnIdentityEnv({
9732
+ conversationId: this.payload.conversationId,
9733
+ agentId: this.payload.agentId
9734
+ }),
9480
9735
  CABANE_COMPANION_HOME: agentCompanionHome()
9481
9736
  };
9482
9737
  this.codexLocal = this.turnContext.runtime === "codex" ? { configSetsSandboxMode: await readCodexConfigSetsSandboxMode() } : void 0;
@@ -10551,8 +10806,14 @@ function runnerTurnOptions(input, log2, emit, extras = {}) {
10551
10806
  agentUsername: spec.agentUsername,
10552
10807
  credential: input.credential,
10553
10808
  local: spec.local,
10554
- workRoot: spec.workRoot,
10809
+ // The place the runner stands in names its own root and transcript
10810
+ // directory when it has them (`PLACE_ENV`); the launcher's host-computed
10811
+ // values are what a runner on the person's own machine gets.
10812
+ workRoot: input.place?.workRoot ?? spec.workRoot,
10555
10813
  runConfig: spec.runConfig,
10814
+ // The launcher's copy of the device's secrets, when it sent one: a runner
10815
+ // in a box has no `secrets.json` to read.
10816
+ ...input.secrets ? { secretStore: makeStore(input.secrets) } : {},
10556
10817
  log: log2,
10557
10818
  ...queryFn ? { queryFn } : {},
10558
10819
  ...spec.opencodeServerUrl ? { opencodeServerUrl: spec.opencodeServerUrl } : {},
@@ -10562,7 +10823,7 @@ function runnerTurnOptions(input, log2, emit, extras = {}) {
10562
10823
  ...spec.totalTimeoutMs !== void 0 ? { totalTimeoutMs: spec.totalTimeoutMs } : {},
10563
10824
  ...spec.leaseRenewalMs !== void 0 ? { leaseRenewalMs: spec.leaseRenewalMs } : {},
10564
10825
  ...spec.preparingRowDelayMs !== void 0 ? { preparingRowDelayMs: spec.preparingRowDelayMs } : {},
10565
- ...spec.transcripts ? { transcriptDir: transcriptsDir() } : {},
10826
+ ...spec.transcripts ? { transcriptDir: input.place?.transcriptDir ?? transcriptsDir() } : {},
10566
10827
  connectorHealth,
10567
10828
  inRunner: {
10568
10829
  bootId: spec.bootId,
@@ -10697,6 +10958,7 @@ var init_run_turn = __esm({
10697
10958
  init_exec_shim();
10698
10959
  init_launcher_channel();
10699
10960
  init_opencode_lock();
10961
+ init_secrets();
10700
10962
  init_transcript_writer();
10701
10963
  init_turn_execution();
10702
10964
  init_protocol();
@@ -11134,133 +11396,8 @@ function spaced(sentence) {
11134
11396
 
11135
11397
  // src/harness-detect.ts
11136
11398
  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
11399
+ init_harness_usable();
11400
+ init_harness_versions();
11264
11401
  var DEFAULT_OPENCODE_SERVER_URL = "http://127.0.0.1:4096";
11265
11402
  var DEFAULT_OPENCODE_PROBE_MS = 1e3;
11266
11403
  var CONFIGURED_OPENCODE_PROBE_MS = 4e3;
@@ -12985,6 +13122,10 @@ function runtimeIssuesFor(gates) {
12985
13122
  return issues;
12986
13123
  }
12987
13124
 
13125
+ // src/supervisor.ts
13126
+ init_harness_versions();
13127
+ init_harness_usable();
13128
+
12988
13129
  // src/opencode-models.ts
12989
13130
  var OPENCODE_RUNTIME = "opencode";
12990
13131
  function mapOpencodeProviders(json) {
@@ -14473,7 +14614,16 @@ var CompanionSupervisor = class {
14473
14614
  },
14474
14615
  runner: {
14475
14616
  provider: this.runnerProvider,
14476
- command: this.runnerCommand ?? selfRunnerCommand(),
14617
+ command: wrappedRunnerCommand(
14618
+ this.config.runner?.command,
14619
+ this.runnerCommand ?? selfRunnerCommand()
14620
+ ),
14621
+ ...this.config.runner?.command ? {
14622
+ boxed: true,
14623
+ secrets: () => loadSecretValuesTolerant(
14624
+ (m) => this.log.warn({ err: m }, "Could not read the secrets file for a boxed turn.")
14625
+ )
14626
+ } : {},
14477
14627
  ...this.runnerEnv ? { env: this.runnerEnv } : {},
14478
14628
  reportRunnerGone: (report) => this.reportRunnerGone(report),
14479
14629
  connectorHealth: this.connectorHealth,
@@ -17109,7 +17259,7 @@ init_errors();
17109
17259
  init_src();
17110
17260
  init_transcript_writer();
17111
17261
  import { existsSync as existsSync19, readFileSync as readFileSync17, readdirSync as readdirSync6 } from "fs";
17112
- import { isAbsolute as isAbsolute2, join as join31 } from "path";
17262
+ import { basename as basename4, isAbsolute as isAbsolute2, join as join31 } from "path";
17113
17263
  async function transcript(opts = {}) {
17114
17264
  const dir2 = transcriptsDir();
17115
17265
  if (opts.follow) {
@@ -17232,13 +17382,28 @@ async function followTranscripts(dir2) {
17232
17382
  follower.tick();
17233
17383
  });
17234
17384
  }
17235
- function listFiles(dir2) {
17385
+ function listTranscriptFiles(dir2) {
17386
+ let entries;
17236
17387
  try {
17237
- return readdirSync6(dir2).filter((f) => f.endsWith(".jsonl")).sort().reverse();
17388
+ entries = readdirSync6(dir2, { withFileTypes: true });
17238
17389
  } catch {
17239
17390
  return [];
17240
17391
  }
17392
+ const files = [];
17393
+ for (const entry of entries) {
17394
+ if (entry.isFile() && entry.name.endsWith(".jsonl")) files.push(entry.name);
17395
+ if (!entry.isDirectory() || entry.name === "anomalies") continue;
17396
+ let inner;
17397
+ try {
17398
+ inner = readdirSync6(join31(dir2, entry.name));
17399
+ } catch {
17400
+ continue;
17401
+ }
17402
+ for (const name of inner) if (name.endsWith(".jsonl")) files.push(`${entry.name}/${name}`);
17403
+ }
17404
+ return files.sort((a, b) => basename4(a) < basename4(b) ? 1 : basename4(a) > basename4(b) ? -1 : 0);
17241
17405
  }
17406
+ var listFiles = listTranscriptFiles;
17242
17407
  function printList(dir2) {
17243
17408
  const files = listFiles(dir2);
17244
17409
  if (files.length === 0) {
@@ -17285,6 +17450,7 @@ function peek(path) {
17285
17450
  function resolveTarget(dir2, target) {
17286
17451
  if (isAbsolute2(target) || target.includes("/")) {
17287
17452
  if (existsSync19(target)) return target;
17453
+ if (!isAbsolute2(target) && existsSync19(join31(dir2, target))) return join31(dir2, target);
17288
17454
  throw new CompanionError(`no transcript at ${target}.`);
17289
17455
  }
17290
17456
  const exact = join31(dir2, target);