@cabane/companion 0.6.187 → 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/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;
@@ -2344,14 +2352,19 @@ var DeviceApi = class {
2344
2352
  return parsed;
2345
2353
  }
2346
2354
  // Pull the agents this device should run, across all the owner's workspaces.
2347
- getAssignments() {
2348
- return this.request("GET", "/api/companion/assignments");
2355
+ getAssignments(signal) {
2356
+ return this.request("GET", "/api/companion/assignments", void 0, requestSignal(signal));
2349
2357
  }
2350
2358
  // Ask again for the credential of an agent assigned to this device, when the saved copy is
2351
2359
  // gone. The previous credential stops working; any agent not assigned here is a 404, and a
2352
2360
  // second ask inside a minute a 429.
2353
- reissueAgentCredential(agentId) {
2354
- return this.request("POST", `/api/companion/agents/${encodeURIComponent(agentId)}/credential`);
2361
+ reissueAgentCredential(agentId, signal) {
2362
+ return this.request(
2363
+ "POST",
2364
+ `/api/companion/agents/${encodeURIComponent(agentId)}/credential`,
2365
+ void 0,
2366
+ requestSignal(signal, 3e4)
2367
+ );
2355
2368
  }
2356
2369
  getRuns(cursor) {
2357
2370
  const query = new URLSearchParams({ limit: "100", order: "asc" });
@@ -2391,6 +2404,10 @@ function errorMessage2(status, body) {
2391
2404
  if (typeof body === "string" && body.length > 0) return `${status} ${body.slice(0, 200)}`;
2392
2405
  return `${status} error`;
2393
2406
  }
2407
+ function requestSignal(signal, timeoutMs = 5e3) {
2408
+ const deadline = AbortSignal.timeout(timeoutMs);
2409
+ return signal ? AbortSignal.any([signal, deadline]) : deadline;
2410
+ }
2394
2411
 
2395
2412
  // src/credentials.ts
2396
2413
  import {
@@ -7436,7 +7453,8 @@ var dispatchSchema = z14.object({
7436
7453
  episode: z14.number().int().optional()
7437
7454
  });
7438
7455
  var credentialsSchema = z14.object({
7439
- credential: z14.string().startsWith("cabrn_")
7456
+ credential: z14.string().startsWith("cabrn_"),
7457
+ secrets: z14.record(z14.string(), z14.string()).optional()
7440
7458
  });
7441
7459
  function runnerEnv(input) {
7442
7460
  return {
@@ -7448,7 +7466,10 @@ function runnerEnv(input) {
7448
7466
  };
7449
7467
  }
7450
7468
  function runnerStdin(input) {
7451
- return `${JSON.stringify({ credential: input.credential })}
7469
+ return `${JSON.stringify({
7470
+ credential: input.credential,
7471
+ ...input.secrets ? { secrets: input.secrets } : {}
7472
+ })}
7452
7473
  `;
7453
7474
  }
7454
7475
  function encodeLauncherMessage(message) {
@@ -7473,6 +7494,29 @@ function selfRunnerCommand() {
7473
7494
  if (!script) throw new Error("runner: cannot tell which script started this companion");
7474
7495
  return [process.execPath, ...process.execArgv, script, "run-turn"];
7475
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
+ }
7476
7520
  function healthSnapshot(store) {
7477
7521
  const out = {};
7478
7522
  for (const r of store?.reports() ?? []) {
@@ -7677,13 +7721,12 @@ async function runTurnInRunner(opts, launch, duties, payload, handleOpts) {
7677
7721
  return { ok: false, durationMs: durationMs2, reason: reason2 };
7678
7722
  }
7679
7723
  try {
7680
- const env = {};
7681
- for (const [k, v] of Object.entries(launch.env ?? process.env)) {
7682
- if (v !== void 0) env[k] = v;
7683
- }
7724
+ const boxed = launch.boxed === true;
7725
+ const env = runnerExecEnv(launch.env ?? process.env, boxed);
7684
7726
  const input = {
7685
7727
  baseUrl: opts.baseUrl,
7686
7728
  credential: minted.credential,
7729
+ ...boxed ? { secrets: launch.secrets?.() ?? {} } : {},
7687
7730
  dispatch: payload,
7688
7731
  turnId,
7689
7732
  resumed: handleOpts.resumed,
@@ -7755,7 +7798,7 @@ async function runTurnInRunner(opts, launch, duties, payload, handleOpts) {
7755
7798
  locks?.release(lockOwner(event.id));
7756
7799
  return;
7757
7800
  case "process_group":
7758
- groups.push(event.pgid);
7801
+ if (!boxed) groups.push(event.pgid);
7759
7802
  return;
7760
7803
  case "remote_session":
7761
7804
  remote.push({ serverUrl: event.serverUrl, sessionId: event.sessionId });
@@ -7776,7 +7819,8 @@ async function runTurnInRunner(opts, launch, duties, payload, handleOpts) {
7776
7819
  }
7777
7820
  if (result) return result;
7778
7821
  const durationMs = Date.now() - startedAt;
7779
- 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}`;
7780
7824
  await child?.reap(groups);
7781
7825
  const reaped = await (launch.reapTurn ?? reapTurnScopes)(turnId).catch((err) => ({
7782
7826
  failures: [err instanceof Error ? err.message : String(err)]
@@ -7791,8 +7835,7 @@ async function runTurnInRunner(opts, launch, duties, payload, handleOpts) {
7791
7835
  await opts.api.setActiveRun(opts.workspaceId, payload.conversationId, payload.agentId, {
7792
7836
  activeRunStartedAt: null,
7793
7837
  turnId,
7794
- outcome: "failed",
7795
- errorReason: reason.slice(0, ERROR_REASON_MAX),
7838
+ ...stopped ? { outcome: "interrupted" } : { outcome: "failed", errorReason: reason.slice(0, ERROR_REASON_MAX) },
7796
7839
  readThrough: payload.messageId
7797
7840
  });
7798
7841
  } catch (err) {
@@ -7810,7 +7853,8 @@ async function runTurnInRunner(opts, launch, duties, payload, handleOpts) {
7810
7853
  reason: "exited"
7811
7854
  });
7812
7855
  if (thrown !== void 0) throw new Error(thrown);
7813
- 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");
7814
7858
  duties.notifyEnd({ id: payload.messageId, ok: false, durationMs, reason });
7815
7859
  return { ok: false, durationMs, reason };
7816
7860
  }
@@ -7872,9 +7916,9 @@ function secretsPath() {
7872
7916
  return join18(cabaneDir(), "secrets.json");
7873
7917
  }
7874
7918
  var secretStoreSchema = z16.record(z16.string(), z16.string());
7875
- function loadSecretStore() {
7919
+ function loadSecretValues() {
7876
7920
  const path = secretsPath();
7877
- if (!existsSync10(path)) return makeStore({});
7921
+ if (!existsSync10(path)) return {};
7878
7922
  let raw;
7879
7923
  try {
7880
7924
  raw = readFileSync8(path, "utf8");
@@ -7883,7 +7927,7 @@ function loadSecretStore() {
7883
7927
  `couldn't read ${path}: ${err instanceof Error ? err.message : String(err)}`
7884
7928
  );
7885
7929
  }
7886
- if (raw.trim().length === 0) return makeStore({});
7930
+ if (raw.trim().length === 0) return {};
7887
7931
  let parsed;
7888
7932
  try {
7889
7933
  parsed = JSON.parse(raw);
@@ -7898,14 +7942,17 @@ function loadSecretStore() {
7898
7942
  `${path} must be a flat object mapping secret names to string values (e.g. { "GITHUB_TOKEN": "ghp_\u2026" }).`
7899
7943
  );
7900
7944
  }
7901
- return makeStore(result.data);
7945
+ return result.data;
7902
7946
  }
7903
7947
  function loadSecretStoreTolerant(onWarn) {
7948
+ return makeStore(loadSecretValuesTolerant(onWarn));
7949
+ }
7950
+ function loadSecretValuesTolerant(onWarn) {
7904
7951
  try {
7905
- return loadSecretStore();
7952
+ return loadSecretValues();
7906
7953
  } catch (err) {
7907
7954
  onWarn?.(err instanceof Error ? err.message : String(err));
7908
- return makeStore({});
7955
+ return {};
7909
7956
  }
7910
7957
  }
7911
7958
  function makeStore(map) {
@@ -7930,6 +7977,130 @@ function requiredSecretNames(mcpServers) {
7930
7977
  return [...names].sort();
7931
7978
  }
7932
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
+
7933
8104
  // src/transcript-writer.ts
7934
8105
  import { appendFileSync, chmodSync as chmodSync4, copyFileSync, mkdirSync as mkdirSync8, readdirSync, rmSync as rmSync6 } from "fs";
7935
8106
  import { basename as basename3, dirname as dirname7, join as join19 } from "path";
@@ -8074,7 +8245,7 @@ function hostReport() {
8074
8245
  }
8075
8246
 
8076
8247
  // src/providers/local-process.ts
8077
- import { spawn as spawn4 } from "child_process";
8248
+ import { spawn as spawn5 } from "child_process";
8078
8249
  import { constants as constants2 } from "os";
8079
8250
  var DEFAULT_KILL_GRACE_MS = 3e4;
8080
8251
  var DEFAULT_REAP_GRACE_MS = 3e3;
@@ -8085,7 +8256,7 @@ function exitCode(code, signal) {
8085
8256
  }
8086
8257
  function createLocalProcessProvider(opts = {}) {
8087
8258
  const killGraceMs = opts.killGraceMs ?? DEFAULT_KILL_GRACE_MS;
8088
- const spawnFn = opts.spawnFn ?? spawn4;
8259
+ const spawnFn = opts.spawnFn ?? spawn5;
8089
8260
  const reapGraceMs = opts.reapGraceMs ?? DEFAULT_REAP_GRACE_MS;
8090
8261
  const killFn = opts.killFn ?? ((pid, signal) => process.kill(pid, signal));
8091
8262
  const groupAlive = (pgid) => {
@@ -8287,44 +8458,6 @@ function checkReason(check) {
8287
8458
  }
8288
8459
  }
8289
8460
 
8290
- // src/harness-versions.ts
8291
- var EMPTY = { claudeCode: null, opencode: null, codex: null };
8292
- function parseVersionToken(raw) {
8293
- if (!raw) return null;
8294
- const m = raw.match(/\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?/);
8295
- return m ? m[0] : null;
8296
- }
8297
- async function probeOpencodeVersion(serverUrl, fetchImpl = fetch) {
8298
- try {
8299
- const base = serverUrl.endsWith("/") ? serverUrl.slice(0, -1) : serverUrl;
8300
- const res = await fetchImpl(`${base}/global/health`, {
8301
- headers: { accept: "application/json" }
8302
- });
8303
- if (!res.ok) return null;
8304
- return extractOpencodeVersion(await res.json());
8305
- } catch {
8306
- return null;
8307
- }
8308
- }
8309
- function extractOpencodeVersion(json) {
8310
- if (!json || typeof json !== "object") return null;
8311
- const obj = json;
8312
- const candidates = [obj.version];
8313
- for (const v of Object.values(obj)) {
8314
- if (v && typeof v === "object") candidates.push(v.version);
8315
- }
8316
- for (const c of candidates) {
8317
- if (typeof c === "string") {
8318
- const parsed = parseVersionToken(c);
8319
- if (parsed) return parsed;
8320
- }
8321
- }
8322
- return null;
8323
- }
8324
- function emptyHarnessVersions() {
8325
- return { ...EMPTY };
8326
- }
8327
-
8328
8461
  // src/harness-copy.ts
8329
8462
  import { homedir as homedir8 } from "os";
8330
8463
  import { join as join21 } from "path";
@@ -8360,90 +8493,6 @@ function spaced(sentence) {
8360
8493
  return sentence ? ` ${sentence}` : "";
8361
8494
  }
8362
8495
 
8363
- // src/harness-usable.ts
8364
- import { spawn as spawn5 } from "child_process";
8365
- var API_KEY_VARIABLES = {
8366
- "claude-code": ["ANTHROPIC_API_KEY", "CLAUDE_CODE_OAUTH_TOKEN"],
8367
- codex: ["CODEX_API_KEY"]
8368
- };
8369
- function apiKeyVariable(runtime, env = process.env) {
8370
- return API_KEY_VARIABLES[runtime].find((name) => !!env[name]?.trim()) ?? null;
8371
- }
8372
- var CHECK_TIMEOUT_MS = 5e3;
8373
- async function checkHarnessUsable(runtime, deps = {}) {
8374
- const env = deps.env ?? process.env;
8375
- const resolveBundled = deps.resolveBundled ?? ((r) => resolveBundledBinary(r));
8376
- const run = deps.run ?? runBounded;
8377
- const resolution = resolveBundled(runtime);
8378
- if (resolution.status !== "present" || !resolution.path) {
8379
- return {
8380
- status: "bundle_missing",
8381
- bundled: resolution.status === "present" ? "executable_missing" : resolution.status
8382
- };
8383
- }
8384
- const path = resolution.path;
8385
- const version = run(path, ["--version"], env).then(
8386
- (r) => r.code === 0 ? parseVersionToken(r.output) : null
8387
- );
8388
- const variable = apiKeyVariable(runtime, env);
8389
- if (variable) return { status: "api_key", variable, version: await version };
8390
- const statusArgs = runtime === "codex" ? ["login", "status"] : ["auth", "status", "--json"];
8391
- const [answer, v] = await Promise.all([run(path, statusArgs, env), version]);
8392
- if (answer.error) return { status: "check_failed", reason: answer.error };
8393
- const signedIn = runtime === "codex" ? readCodexStatus(answer) : readClaudeStatus(answer);
8394
- if (typeof signedIn === "string") return { status: "check_failed", reason: signedIn };
8395
- return signedIn ? { status: "signed_in", version: v } : { status: "signed_out", version: v };
8396
- }
8397
- function readClaudeStatus(answer) {
8398
- const start = answer.output.indexOf("{");
8399
- const end = answer.output.lastIndexOf("}");
8400
- if (start < 0 || end < start) return "unreadable";
8401
- try {
8402
- const parsed = JSON.parse(answer.output.slice(start, end + 1));
8403
- const loggedIn = parsed?.loggedIn;
8404
- return typeof loggedIn === "boolean" ? loggedIn : "unreadable";
8405
- } catch {
8406
- return "unreadable";
8407
- }
8408
- }
8409
- function readCodexStatus(answer) {
8410
- if (answer.code === 0) return true;
8411
- if (/not logged in/i.test(answer.output)) return false;
8412
- if (/config/i.test(answer.output)) return "config";
8413
- return "unreadable";
8414
- }
8415
- function runBounded(path, args, env) {
8416
- return new Promise((resolve) => {
8417
- let settled = false;
8418
- let out = "";
8419
- const done = (result) => {
8420
- if (settled) return;
8421
- settled = true;
8422
- clearTimeout(timer);
8423
- resolve(result);
8424
- };
8425
- let child;
8426
- try {
8427
- child = spawn5(path, args, { stdio: ["ignore", "pipe", "pipe"], env });
8428
- } catch {
8429
- resolve({ code: null, output: "", error: "spawn" });
8430
- return;
8431
- }
8432
- const capture = (chunk) => {
8433
- if (out.length < 8192) out += chunk.toString();
8434
- };
8435
- child.stdout?.on("data", capture);
8436
- child.stderr?.on("data", capture);
8437
- const timer = setTimeout(() => {
8438
- child.kill("SIGKILL");
8439
- done({ code: null, output: out, error: "timeout" });
8440
- }, CHECK_TIMEOUT_MS);
8441
- timer.unref?.();
8442
- child.once("error", () => done({ code: null, output: out, error: "spawn" }));
8443
- child.once("close", (code) => done({ code, output: out }));
8444
- });
8445
- }
8446
-
8447
8496
  // src/harness-detect.ts
8448
8497
  var DEFAULT_OPENCODE_SERVER_URL = "http://127.0.0.1:4096";
8449
8498
  var DEFAULT_OPENCODE_PROBE_MS = 1e3;
@@ -9623,6 +9672,7 @@ var CompanionSupervisor = class {
9623
9672
  pollTimer = null;
9624
9673
  refreshing = false;
9625
9674
  stopped = false;
9675
+ reconciliationAbort = new AbortController();
9626
9676
  // The version-skew warning is logged once, not every heartbeat.
9627
9677
  versionSkewWarned = false;
9628
9678
  // This device's id, captured from the heartbeat / assignments response. The SSE
@@ -9668,6 +9718,7 @@ var CompanionSupervisor = class {
9668
9718
  // heartbeat + poll loops. A companion with no device token (logged out) does
9669
9719
  // nothing but say so.
9670
9720
  async start() {
9721
+ if (this.stopped) return;
9671
9722
  this.log.info(
9672
9723
  { protocolVersion: TURN_PROTOCOL_VERSION, version: COMPANION_VERSION },
9673
9724
  `Companion ${COMPANION_VERSION} starting on ${this.config.deviceLabel ?? hostname2()}`
@@ -9752,6 +9803,7 @@ var CompanionSupervisor = class {
9752
9803
  });
9753
9804
  this.updater.start();
9754
9805
  await this.refreshAssignments();
9806
+ if (this.stopped) return;
9755
9807
  this.deviceSub.start();
9756
9808
  this.heartbeatTimer = setInterval(() => this.kickHeartbeat(), HEARTBEAT_INTERVAL_MS);
9757
9809
  this.heartbeatTimer.unref?.();
@@ -9864,10 +9916,12 @@ var CompanionSupervisor = class {
9864
9916
  let items;
9865
9917
  let device;
9866
9918
  try {
9867
- const resp = await this.deviceApi.getAssignments();
9919
+ const resp = await this.deviceApi.getAssignments(this.reconciliationAbort.signal);
9920
+ if (this.stopped) return;
9868
9921
  items = resp.assignments;
9869
9922
  device = resp.device;
9870
9923
  } catch (err) {
9924
+ if (this.stopped) return;
9871
9925
  this.log.error(
9872
9926
  { ...apiErrorLogFields(err) },
9873
9927
  "Couldn't load the assigned agents. Check that this device is active in Cabane; trying again shortly."
@@ -9886,6 +9940,7 @@ var CompanionSupervisor = class {
9886
9940
  if (it.credential) setCredential(device.id, it.agentId, it.credential);
9887
9941
  }
9888
9942
  items = await this.restoreMissingCredentials(device.id, items);
9943
+ if (this.stopped) return;
9889
9944
  this.uncredentialed = new Set(
9890
9945
  items.filter((it) => !it.credential && !getCredential(device.id, it.agentId)).map((it) => `${it.workspaceId}|${it.agentId}`)
9891
9946
  );
@@ -9898,6 +9953,7 @@ var CompanionSupervisor = class {
9898
9953
  for (const wsId of [...this.workspaces.keys()]) {
9899
9954
  if (!byWorkspace.has(wsId)) await this.removeWorkspace(wsId);
9900
9955
  }
9956
+ if (this.stopped) return;
9901
9957
  for (const [wsId, list] of byWorkspace) this.reconcileWorkspace(device.id, wsId, list);
9902
9958
  const pruned = pruneCredentials(
9903
9959
  device.id,
@@ -9929,6 +9985,7 @@ var CompanionSupervisor = class {
9929
9985
  async restoreMissingCredentials(deviceId, items) {
9930
9986
  const restored = [];
9931
9987
  for (const it of items) {
9988
+ if (this.stopped) return restored;
9932
9989
  if (it.credential || getCredential(deviceId, it.agentId)) {
9933
9990
  restored.push(it);
9934
9991
  continue;
@@ -9940,7 +9997,11 @@ var CompanionSupervisor = class {
9940
9997
  continue;
9941
9998
  }
9942
9999
  try {
9943
- const { credential } = await this.deviceApi.reissueAgentCredential(it.agentId);
10000
+ const { credential } = await this.deviceApi.reissueAgentCredential(
10001
+ it.agentId,
10002
+ this.reconciliationAbort.signal
10003
+ );
10004
+ if (this.stopped) return restored;
9944
10005
  setCredential(deviceId, it.agentId, credential);
9945
10006
  this.log.info(
9946
10007
  { agentId: it.agentId, agentName: it.agentDisplayName },
@@ -10233,7 +10294,16 @@ var CompanionSupervisor = class {
10233
10294
  },
10234
10295
  runner: {
10235
10296
  provider: this.runnerProvider,
10236
- 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
+ } : {},
10237
10307
  ...this.runnerEnv ? { env: this.runnerEnv } : {},
10238
10308
  reportRunnerGone: (report) => this.reportRunnerGone(report),
10239
10309
  connectorHealth: this.connectorHealth,
@@ -10815,6 +10885,7 @@ var CompanionSupervisor = class {
10815
10885
  // ---- control actions ----
10816
10886
  async shutdown() {
10817
10887
  this.stopped = true;
10888
+ this.reconciliationAbort.abort();
10818
10889
  this.updater?.stop();
10819
10890
  if (this.heartbeatTimer) clearInterval(this.heartbeatTimer);
10820
10891
  if (this.detectTimer) clearInterval(this.detectTimer);
@@ -11090,33 +11161,46 @@ async function createCompanionRuntime(opts = {}) {
11090
11161
  });
11091
11162
  let restartForUpdate = async () => {
11092
11163
  };
11093
- const supervisor = new CompanionSupervisor({
11094
- config: cfg,
11095
- log,
11096
- hub,
11097
- ...opts.checkHarness ? { checkHarness: opts.checkHarness } : {},
11098
- ...embeddedRunner({
11099
- electron: Boolean(process.versions.electron),
11100
- execPath: process.execPath,
11101
- moduleUrl: import.meta.url,
11102
- env: process.env
11103
- }) ?? {},
11104
- restartForUpdate: () => restartForUpdate()
11105
- });
11106
- await supervisor.start();
11164
+ let supervisor;
11107
11165
  let control = null;
11108
11166
  let dashboard = null;
11167
+ let stopped = false;
11168
+ const stop = async () => {
11169
+ if (stopped) return;
11170
+ stopped = true;
11171
+ clearRuntimeStateIfOurs(instanceId);
11172
+ process.removeListener("exit", clearRuntimeOnExit);
11173
+ await Promise.allSettled([
11174
+ ...supervisor ? [supervisor.shutdown()] : [],
11175
+ ...control ? [control.close()] : [],
11176
+ ...dashboard ? [dashboard.close()] : []
11177
+ ]);
11178
+ };
11109
11179
  try {
11180
+ supervisor = new CompanionSupervisor({
11181
+ config: cfg,
11182
+ log,
11183
+ hub,
11184
+ ...opts.checkHarness ? { checkHarness: opts.checkHarness } : {},
11185
+ ...embeddedRunner({
11186
+ electron: Boolean(process.versions.electron),
11187
+ execPath: process.execPath,
11188
+ moduleUrl: import.meta.url,
11189
+ env: process.env
11190
+ }) ?? {},
11191
+ restartForUpdate: () => restartForUpdate()
11192
+ });
11193
+ const active2 = supervisor;
11110
11194
  control = await startControlServer({
11111
11195
  status: () => ({
11112
11196
  ...hub.statusJson(),
11113
- ...supervisor.lastHeartbeat(),
11114
- detection: supervisor.currentDetection()
11197
+ ...active2.lastHeartbeat(),
11198
+ detection: active2.currentDetection()
11115
11199
  }),
11116
- detect: () => supervisor.redetect(),
11117
- allow: (runtime) => supervisor.allowHarness(runtime),
11118
- update: (auto) => supervisor.update(auto),
11119
- stop: () => void supervisor.requestStop()
11200
+ detect: () => active2.redetect(),
11201
+ allow: (runtime) => active2.allowHarness(runtime),
11202
+ update: (auto) => active2.update(auto),
11203
+ stop: () => void stop().then(() => active2.requestStop())
11120
11204
  });
11121
11205
  if (opts.dashboard) {
11122
11206
  const preferredPort = opts.port ?? cfg.dashboardPort;
@@ -11127,6 +11211,7 @@ async function createCompanionRuntime(opts = {}) {
11127
11211
  });
11128
11212
  hub.setDashboardUrl(dashboard.url);
11129
11213
  }
11214
+ if (stopped) throw new Error("The companion was stopped during startup.");
11130
11215
  writeRuntimeState({
11131
11216
  pid: process.pid,
11132
11217
  socket: control.path,
@@ -11144,7 +11229,7 @@ async function createCompanionRuntime(opts = {}) {
11144
11229
  clearRuntimeStateIfOurs(instanceId);
11145
11230
  process.removeListener("exit", clearRuntimeOnExit);
11146
11231
  await Promise.allSettled([
11147
- supervisor.shutdown(),
11232
+ ...supervisor ? [supervisor.shutdown()] : [],
11148
11233
  ...control ? [control.close()] : [],
11149
11234
  ...dashboard ? [dashboard.close()] : []
11150
11235
  ]);
@@ -11156,22 +11241,17 @@ async function createCompanionRuntime(opts = {}) {
11156
11241
  });
11157
11242
  throw err;
11158
11243
  }
11159
- let stopped = false;
11160
- const stop = async () => {
11161
- if (stopped) return;
11162
- stopped = true;
11163
- clearRuntimeStateIfOurs(instanceId);
11164
- process.removeListener("exit", clearRuntimeOnExit);
11165
- try {
11166
- await supervisor.shutdown();
11167
- await closeSurfaces(control, dashboard);
11168
- } catch {
11169
- }
11170
- };
11244
+ const active = supervisor;
11171
11245
  restartForUpdate = async () => {
11172
11246
  await stop();
11173
11247
  process.exit(0);
11174
11248
  };
11249
+ void active.start().catch(async (err) => {
11250
+ log.error({ err }, "Couldn't initialize the companion.");
11251
+ recordCrash({ reason: String(err), origin: "startup", at: (/* @__PURE__ */ new Date()).toISOString() });
11252
+ await stop();
11253
+ process.exitCode = 1;
11254
+ });
11175
11255
  return {
11176
11256
  ok: true,
11177
11257
  runtime: {
@@ -11179,16 +11259,12 @@ async function createCompanionRuntime(opts = {}) {
11179
11259
  socketPath: control.path,
11180
11260
  config: cfg,
11181
11261
  stop,
11182
- heartbeatNow: () => supervisor.heartbeatNow(),
11262
+ heartbeatNow: () => active.heartbeatNow(),
11183
11263
  harnesses: () => hub.statusJson().harnesses ?? [],
11184
- detect: () => supervisor.redetect()
11264
+ detect: () => active.redetect()
11185
11265
  }
11186
11266
  };
11187
11267
  }
11188
- async function closeSurfaces(control, dashboard) {
11189
- await control.close();
11190
- if (dashboard) await dashboard.close();
11191
- }
11192
11268
  export {
11193
11269
  createCompanionRuntime,
11194
11270
  embeddedRunner,