@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/cli.js +489 -243
- package/dist/launch.js +9 -1
- package/dist/pairing-config.js +9 -1
- package/dist/runtime.js +264 -188
- package/package.json +1 -1
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(
|
|
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({
|
|
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
|
|
7681
|
-
|
|
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
|
|
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.
|
|
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
|
|
7919
|
+
function loadSecretValues() {
|
|
7876
7920
|
const path = secretsPath();
|
|
7877
|
-
if (!existsSync10(path)) return
|
|
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
|
|
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
|
|
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
|
|
7952
|
+
return loadSecretValues();
|
|
7906
7953
|
} catch (err) {
|
|
7907
7954
|
onWarn?.(err instanceof Error ? err.message : String(err));
|
|
7908
|
-
return
|
|
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
|
|
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 ??
|
|
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(
|
|
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:
|
|
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
|
-
|
|
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
|
-
...
|
|
11114
|
-
detection:
|
|
11197
|
+
...active2.lastHeartbeat(),
|
|
11198
|
+
detection: active2.currentDetection()
|
|
11115
11199
|
}),
|
|
11116
|
-
detect: () =>
|
|
11117
|
-
allow: (runtime) =>
|
|
11118
|
-
update: (auto) =>
|
|
11119
|
-
stop: () => void
|
|
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
|
-
|
|
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: () =>
|
|
11262
|
+
heartbeatNow: () => active.heartbeatNow(),
|
|
11183
11263
|
harnesses: () => hub.statusJson().harnesses ?? [],
|
|
11184
|
-
detect: () =>
|
|
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,
|