@tablation/crew 0.105.0 → 0.106.0
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 +1857 -1540
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -1698,9 +1698,9 @@ var keychainSource = {
|
|
|
1698
1698
|
write(s, token, c) {
|
|
1699
1699
|
try {
|
|
1700
1700
|
if (process.platform === "darwin") {
|
|
1701
|
-
const
|
|
1701
|
+
const q2 = (v) => `"${v.replace(/(["\\])/g, "\\$1")}"`;
|
|
1702
1702
|
execFileSync("security", ["-i"], {
|
|
1703
|
-
input: `add-generic-password -U -s ${
|
|
1703
|
+
input: `add-generic-password -U -s ${q2(keychainService(s))} -a ${q2(c.host)} -w ${q2(token)}
|
|
1704
1704
|
`,
|
|
1705
1705
|
stdio: ["pipe", "ignore", "pipe"]
|
|
1706
1706
|
});
|
|
@@ -2719,7 +2719,22 @@ function parseShipRunner(v) {
|
|
|
2719
2719
|
if (v === "host" || v === "sandbox") return v;
|
|
2720
2720
|
throw new ConfigError(`ship.runner must be "host" or "sandbox", got ${JSON.stringify(v)}`);
|
|
2721
2721
|
}
|
|
2722
|
-
var SANDBOX_KEYS = [
|
|
2722
|
+
var SANDBOX_KEYS = [
|
|
2723
|
+
"bin",
|
|
2724
|
+
"image",
|
|
2725
|
+
"policy",
|
|
2726
|
+
"cacheVolume",
|
|
2727
|
+
"mirrorDir",
|
|
2728
|
+
"workDir",
|
|
2729
|
+
"agentBin",
|
|
2730
|
+
"forgeTokenVar",
|
|
2731
|
+
"engine",
|
|
2732
|
+
"cacheWorkspace",
|
|
2733
|
+
"cacheImage",
|
|
2734
|
+
"depsImage",
|
|
2735
|
+
"depsCommand",
|
|
2736
|
+
"depsLockfile"
|
|
2737
|
+
];
|
|
2723
2738
|
function parseShipSandbox(v) {
|
|
2724
2739
|
if (v === void 0 || v === null) return void 0;
|
|
2725
2740
|
if (typeof v !== "object" || Array.isArray(v)) throw new ConfigError("ship.sandbox must be a map of settings");
|
|
@@ -5897,16 +5912,16 @@ function roleCapWarnings(ship) {
|
|
|
5897
5912
|
|
|
5898
5913
|
// src/selftest.ts
|
|
5899
5914
|
import { spawn as spawn5 } from "child_process";
|
|
5900
|
-
import { existsSync as
|
|
5915
|
+
import { existsSync as existsSync25, mkdirSync as mkdirSync14, mkdtempSync as mkdtempSync2, readFileSync as readFileSync22, rmSync as rmSync14, writeFileSync as writeFileSync16 } from "fs";
|
|
5901
5916
|
import { tmpdir as tmpdir4 } from "os";
|
|
5902
|
-
import { join as
|
|
5917
|
+
import { join as join27 } from "path";
|
|
5903
5918
|
|
|
5904
5919
|
// src/agent.ts
|
|
5905
|
-
import { spawnSync as
|
|
5906
|
-
import { readFileSync as
|
|
5920
|
+
import { spawnSync as spawnSync6 } from "child_process";
|
|
5921
|
+
import { readFileSync as readFileSync21, existsSync as existsSync24, statSync as statSync7, mkdirSync as mkdirSync13, writeFileSync as writeFileSync15, createWriteStream, openSync as openSync3, closeSync as closeSync3, readSync } from "fs";
|
|
5907
5922
|
import { StringDecoder } from "string_decoder";
|
|
5908
5923
|
import { finished } from "stream/promises";
|
|
5909
|
-
import { join as
|
|
5924
|
+
import { join as join26, dirname as dirname8, basename as basename8 } from "path";
|
|
5910
5925
|
import { createHash as createHash6 } from "crypto";
|
|
5911
5926
|
|
|
5912
5927
|
// src/state.ts
|
|
@@ -7061,9 +7076,9 @@ async function addRunToTicket(target, t, recordId, run3) {
|
|
|
7061
7076
|
}
|
|
7062
7077
|
}
|
|
7063
7078
|
var queueDir = (stateDir) => join17(stateDir, "pending-agent-logs");
|
|
7064
|
-
function queueAgentRun(stateDir, name,
|
|
7079
|
+
function queueAgentRun(stateDir, name, q2) {
|
|
7065
7080
|
mkdirSync11(queueDir(stateDir), { recursive: true });
|
|
7066
|
-
writeFileSync12(join17(queueDir(stateDir), `${name}.json`), JSON.stringify(
|
|
7081
|
+
writeFileSync12(join17(queueDir(stateDir), `${name}.json`), JSON.stringify(q2));
|
|
7067
7082
|
}
|
|
7068
7083
|
async function replayQueuedAgentRuns(stateDir, route2, keyFor, log, now = /* @__PURE__ */ new Date()) {
|
|
7069
7084
|
const dir = queueDir(stateDir);
|
|
@@ -7071,23 +7086,23 @@ async function replayQueuedAgentRuns(stateDir, route2, keyFor, log, now = /* @__
|
|
|
7071
7086
|
let sent = 0;
|
|
7072
7087
|
for (const f of readdirSync6(dir).filter((n) => n.endsWith(".json")).sort()) {
|
|
7073
7088
|
const path = join17(dir, f);
|
|
7074
|
-
let
|
|
7089
|
+
let q2;
|
|
7075
7090
|
try {
|
|
7076
|
-
|
|
7091
|
+
q2 = JSON.parse(readFileSync16(path, "utf8"));
|
|
7077
7092
|
} catch {
|
|
7078
7093
|
continue;
|
|
7079
7094
|
}
|
|
7080
|
-
if (
|
|
7095
|
+
if (q2.version !== 1 || q2.route !== route2 || now.toISOString() < q2.nextAttemptAt) continue;
|
|
7081
7096
|
try {
|
|
7082
|
-
const apiKey = await keyFor(
|
|
7097
|
+
const apiKey = await keyFor(q2.role);
|
|
7083
7098
|
if (!apiKey) continue;
|
|
7084
|
-
const sentRun = await reportAgentRun({ ...
|
|
7099
|
+
const sentRun = await reportAgentRun({ ...q2.target, apiKey }, q2.report);
|
|
7085
7100
|
if (sentRun.counterWarning) log.warn(sentRun.counterWarning);
|
|
7086
7101
|
if (sentRun.counterRetry) queueTicketCounters(stateDir, f.replace(/\.json$/, ""), {
|
|
7087
7102
|
version: 1,
|
|
7088
|
-
route:
|
|
7089
|
-
role:
|
|
7090
|
-
target:
|
|
7103
|
+
route: q2.route,
|
|
7104
|
+
role: q2.role,
|
|
7105
|
+
target: q2.target,
|
|
7091
7106
|
write: sentRun.counterRetry,
|
|
7092
7107
|
attempts: 0,
|
|
7093
7108
|
nextAttemptAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
@@ -7101,22 +7116,22 @@ async function replayQueuedAgentRuns(stateDir, route2, keyFor, log, now = /* @__
|
|
|
7101
7116
|
log.warn(`dropped a queued Agent Log report (${f}): ${e.message}`);
|
|
7102
7117
|
continue;
|
|
7103
7118
|
}
|
|
7104
|
-
const attempts =
|
|
7119
|
+
const attempts = q2.attempts + 1;
|
|
7105
7120
|
writeFileSync12(path, JSON.stringify({
|
|
7106
|
-
...
|
|
7121
|
+
...q2,
|
|
7107
7122
|
attempts,
|
|
7108
7123
|
warned: true,
|
|
7109
7124
|
nextAttemptAt: new Date(now.getTime() + sleepBackoff(attempts)).toISOString()
|
|
7110
7125
|
}));
|
|
7111
|
-
if (!
|
|
7126
|
+
if (!q2.warned) log.warn(`a queued Agent Log report could not be sent yet (${e.message}); will retry`);
|
|
7112
7127
|
}
|
|
7113
7128
|
}
|
|
7114
7129
|
return sent;
|
|
7115
7130
|
}
|
|
7116
7131
|
var countersDir = (stateDir) => join17(stateDir, "pending-ticket-counters");
|
|
7117
|
-
function queueTicketCounters(stateDir, name,
|
|
7132
|
+
function queueTicketCounters(stateDir, name, q2) {
|
|
7118
7133
|
mkdirSync11(countersDir(stateDir), { recursive: true });
|
|
7119
|
-
writeFileSync12(join17(countersDir(stateDir), `${name}.json`), JSON.stringify(
|
|
7134
|
+
writeFileSync12(join17(countersDir(stateDir), `${name}.json`), JSON.stringify(q2));
|
|
7120
7135
|
}
|
|
7121
7136
|
async function replayQueuedTicketCounters(stateDir, route2, keyFor, log, now = /* @__PURE__ */ new Date()) {
|
|
7122
7137
|
const dir = countersDir(stateDir);
|
|
@@ -7124,34 +7139,34 @@ async function replayQueuedTicketCounters(stateDir, route2, keyFor, log, now = /
|
|
|
7124
7139
|
let sent = 0;
|
|
7125
7140
|
for (const f of readdirSync6(dir).filter((n) => n.endsWith(".json")).sort()) {
|
|
7126
7141
|
const path = join17(dir, f);
|
|
7127
|
-
let
|
|
7142
|
+
let q2;
|
|
7128
7143
|
try {
|
|
7129
|
-
|
|
7144
|
+
q2 = JSON.parse(readFileSync16(path, "utf8"));
|
|
7130
7145
|
} catch {
|
|
7131
7146
|
continue;
|
|
7132
7147
|
}
|
|
7133
|
-
if (
|
|
7148
|
+
if (q2.version !== 1 || q2.route !== route2 || now.toISOString() < q2.nextAttemptAt) continue;
|
|
7134
7149
|
try {
|
|
7135
|
-
const apiKey = await keyFor(
|
|
7150
|
+
const apiKey = await keyFor(q2.role);
|
|
7136
7151
|
if (!apiKey) continue;
|
|
7137
|
-
await writeCounters({ ...
|
|
7152
|
+
await writeCounters({ ...q2.target, apiKey }, q2.write);
|
|
7138
7153
|
rmSync11(path, { force: true });
|
|
7139
7154
|
sent += 1;
|
|
7140
|
-
log.emit(`added a queued run's spend to ${
|
|
7155
|
+
log.emit(`added a queued run's spend to ${q2.write.ticketReference ?? q2.write.recordId} (${f})`);
|
|
7141
7156
|
} catch (e) {
|
|
7142
7157
|
if (!isRetryable(e)) {
|
|
7143
7158
|
rmSync11(path, { force: true });
|
|
7144
7159
|
log.warn(`dropped a queued ticket counter update (${f}): ${e.message}`);
|
|
7145
7160
|
continue;
|
|
7146
7161
|
}
|
|
7147
|
-
const attempts =
|
|
7162
|
+
const attempts = q2.attempts + 1;
|
|
7148
7163
|
writeFileSync12(path, JSON.stringify({
|
|
7149
|
-
...
|
|
7164
|
+
...q2,
|
|
7150
7165
|
attempts,
|
|
7151
7166
|
warned: true,
|
|
7152
7167
|
nextAttemptAt: new Date(now.getTime() + sleepBackoff(attempts)).toISOString()
|
|
7153
7168
|
}));
|
|
7154
|
-
if (!
|
|
7169
|
+
if (!q2.warned) log.warn(`a queued ticket counter update could not be sent yet (${e.message}); will retry`);
|
|
7155
7170
|
}
|
|
7156
7171
|
}
|
|
7157
7172
|
return sent;
|
|
@@ -9936,11 +9951,11 @@ function skillMarkerWarnings(promptsDir, personaPrompts) {
|
|
|
9936
9951
|
}
|
|
9937
9952
|
|
|
9938
9953
|
// src/runner.ts
|
|
9939
|
-
import { spawn as spawn3, execFile, execFileSync as execFileSync6 } from "child_process";
|
|
9954
|
+
import { spawn as spawn3, execFile as execFile2, execFileSync as execFileSync6 } from "child_process";
|
|
9940
9955
|
import { mkdtempSync, writeFileSync as writeFileSync14, rmSync as rmSync13 } from "fs";
|
|
9941
9956
|
import { tmpdir as tmpdir3 } from "os";
|
|
9942
|
-
import { basename as basename7, join as
|
|
9943
|
-
import { promisify } from "util";
|
|
9957
|
+
import { basename as basename7, join as join25 } from "path";
|
|
9958
|
+
import { promisify as promisify2 } from "util";
|
|
9944
9959
|
|
|
9945
9960
|
// src/run-limit.ts
|
|
9946
9961
|
var DEFAULT_MAX_RUN_MINUTES = 240;
|
|
@@ -9991,1216 +10006,1712 @@ function runKilledComment(b) {
|
|
|
9991
10006
|
The agent run on this ticket was stopped at its wall-clock limit of ${b.limitMinutes} minutes before it handed on${b.branch ? `; its branch is \`${b.branch}\`` : ""}. ${tree} The worktree is left as it was. The next run on this seat resumes from it rather than starting over.`;
|
|
9992
10007
|
}
|
|
9993
10008
|
|
|
9994
|
-
// src/
|
|
9995
|
-
|
|
9996
|
-
|
|
9997
|
-
|
|
9998
|
-
|
|
9999
|
-
|
|
10009
|
+
// src/sandbox-cache.ts
|
|
10010
|
+
import { execFile } from "child_process";
|
|
10011
|
+
import { promisify } from "util";
|
|
10012
|
+
|
|
10013
|
+
// src/deprovision-host.ts
|
|
10014
|
+
import { spawnSync as spawnSync5 } from "child_process";
|
|
10015
|
+
import { existsSync as existsSync23, readdirSync as readdirSync11 } from "fs";
|
|
10016
|
+
|
|
10017
|
+
// src/interrupted.ts
|
|
10018
|
+
import { existsSync as existsSync21, readdirSync as readdirSync9, readFileSync as readFileSync20 } from "fs";
|
|
10019
|
+
import { join as join23 } from "path";
|
|
10020
|
+
var INTERRUPTED_MARKER = calloutHeader("NOTE", "Interrupted");
|
|
10021
|
+
function interruptedComment(b) {
|
|
10022
|
+
const tree = b.dirty === null ? "The worktree could not be checked." : b.dirty ? "The worktree has uncommitted changes \u2014 they are this seat's own, from that run." : "The worktree is clean.";
|
|
10023
|
+
return `${INTERRUPTED_MARKER}
|
|
10024
|
+
|
|
10025
|
+
The agent run on this ticket was cut short (${b.reason}) before it handed on${b.branch ? `; its branch is \`${b.branch}\`` : ""}. ${tree} The next run on this seat resumes from the worktree rather than starting over.`;
|
|
10026
|
+
}
|
|
10027
|
+
var handled = /* @__PURE__ */ new Set();
|
|
10028
|
+
var pidAlive = (pid) => {
|
|
10029
|
+
try {
|
|
10030
|
+
process.kill(pid, 0);
|
|
10031
|
+
return true;
|
|
10032
|
+
} catch (e) {
|
|
10033
|
+
return e.code === "EPERM";
|
|
10034
|
+
}
|
|
10000
10035
|
};
|
|
10001
|
-
|
|
10002
|
-
|
|
10003
|
-
|
|
10004
|
-
|
|
10005
|
-
|
|
10006
|
-
|
|
10007
|
-
|
|
10008
|
-
env: o.env,
|
|
10009
|
-
stdio: ["pipe", o.streamFd, "inherit"],
|
|
10010
|
-
detached: true
|
|
10011
|
-
});
|
|
10012
|
-
return { child, handle: { runner: "host", pid: child.pid } };
|
|
10036
|
+
function runFinished(streamPath) {
|
|
10037
|
+
const path = `${streamPath}.meta.json`;
|
|
10038
|
+
if (!existsSync21(path)) return false;
|
|
10039
|
+
try {
|
|
10040
|
+
return JSON.parse(readFileSync20(path, "utf8")).running !== true;
|
|
10041
|
+
} catch {
|
|
10042
|
+
return true;
|
|
10013
10043
|
}
|
|
10014
|
-
|
|
10015
|
-
|
|
10044
|
+
}
|
|
10045
|
+
function orphanedStreams(o) {
|
|
10046
|
+
const dir = join23(o.stateDir, "streams");
|
|
10047
|
+
if (!existsSync21(dir)) return [];
|
|
10048
|
+
const ownerAlive = o.isOwnerAlive ?? pidAlive;
|
|
10049
|
+
const out = [];
|
|
10050
|
+
for (const name of readdirSync9(dir)) {
|
|
10051
|
+
if (!name.endsWith(".jsonl") || name.endsWith(".events.jsonl")) continue;
|
|
10052
|
+
const streamPath = join23(dir, name);
|
|
10053
|
+
if (runFinished(streamPath)) continue;
|
|
10054
|
+
const sidecar = readRunSidecar(streamPath);
|
|
10055
|
+
if (!sidecar || sidecar.route !== o.routeLabel) continue;
|
|
10056
|
+
if (ownerAlive(sidecar.ownerPid)) continue;
|
|
10057
|
+
out.push({ streamPath, sidecar });
|
|
10016
10058
|
}
|
|
10017
|
-
|
|
10018
|
-
|
|
10019
|
-
|
|
10020
|
-
|
|
10021
|
-
|
|
10022
|
-
|
|
10023
|
-
|
|
10059
|
+
return out;
|
|
10060
|
+
}
|
|
10061
|
+
async function reconcileRuns(o) {
|
|
10062
|
+
let adopted = 0;
|
|
10063
|
+
let interrupted = 0;
|
|
10064
|
+
for (const { streamPath, sidecar } of orphanedStreams(o)) {
|
|
10065
|
+
if (handled.has(streamPath)) continue;
|
|
10066
|
+
handled.add(streamPath);
|
|
10067
|
+
const isAlive = o.isAlive ?? runIsAlive;
|
|
10068
|
+
const lock = isAlive(sidecar) ? o.state.acquireRun(sidecar.role, o.maxConcurrentAgents, o.roleConcurrency?.[sidecar.role] ?? 1) : null;
|
|
10069
|
+
const ticketLock = isAlive(sidecar) && sidecar.ticketRecordId ? o.state.acquireTicket(sidecar.ticketRecordId) : null;
|
|
10070
|
+
const work = (async () => {
|
|
10071
|
+
try {
|
|
10072
|
+
const result = await reattachRun(streamPath, sidecar, o.emit, {
|
|
10073
|
+
agentLogKey: await o.agentLogKey?.(sidecar.role).catch(() => void 0),
|
|
10074
|
+
isAlive: o.isAlive,
|
|
10075
|
+
pollMs: o.pollMs,
|
|
10076
|
+
onAdopted: () => o.emit.emit(`adopted ${sidecar.role} run (pid ${sidecar.pid}) left by a previous crew process`, { step: "agent", role: sidecar.role })
|
|
10077
|
+
});
|
|
10078
|
+
if (result.adopted) adopted++;
|
|
10079
|
+
else interrupted++;
|
|
10080
|
+
try {
|
|
10081
|
+
await o.afterRun?.(sidecar, result);
|
|
10082
|
+
} catch (e) {
|
|
10083
|
+
o.emit.warn(`could not follow up on ${sidecar.role} run ${streamPath}: ${e.message}`, { step: "agent" });
|
|
10084
|
+
}
|
|
10085
|
+
} catch (e) {
|
|
10086
|
+
o.emit.warn(`could not reattach ${sidecar.role} run ${streamPath}: ${e.message}`, { step: "agent" });
|
|
10087
|
+
} finally {
|
|
10088
|
+
if (lock?.ok) lock.release();
|
|
10089
|
+
if (ticketLock?.ok) ticketLock.release();
|
|
10090
|
+
}
|
|
10091
|
+
})();
|
|
10092
|
+
if (isAlive(sidecar)) {
|
|
10093
|
+
if (o.inFlight) {
|
|
10094
|
+
o.inFlight.add(work);
|
|
10095
|
+
void work.finally(() => o.inFlight.delete(work));
|
|
10096
|
+
}
|
|
10097
|
+
} else {
|
|
10098
|
+
await work;
|
|
10024
10099
|
}
|
|
10025
10100
|
}
|
|
10026
|
-
|
|
10027
|
-
|
|
10028
|
-
|
|
10029
|
-
|
|
10101
|
+
return { adopted, interrupted };
|
|
10102
|
+
}
|
|
10103
|
+
|
|
10104
|
+
// src/live-runs.ts
|
|
10105
|
+
import { existsSync as existsSync22, readdirSync as readdirSync10 } from "fs";
|
|
10106
|
+
import { join as join24 } from "path";
|
|
10107
|
+
function liveRuns(stateDir, isAlive = runIsAlive) {
|
|
10108
|
+
const dir = join24(stateDir, "streams");
|
|
10109
|
+
if (!existsSync22(dir)) return [];
|
|
10110
|
+
const out = [];
|
|
10111
|
+
for (const name of readdirSync10(dir)) {
|
|
10112
|
+
if (!name.endsWith(".jsonl") || name.endsWith(".events.jsonl")) continue;
|
|
10113
|
+
const streamPath = join24(dir, name);
|
|
10114
|
+
if (runFinished(streamPath)) continue;
|
|
10115
|
+
const sidecar = readRunSidecar(streamPath);
|
|
10116
|
+
if (!sidecar || !isAlive(sidecar)) continue;
|
|
10117
|
+
out.push({ ...sidecar, streamPath });
|
|
10030
10118
|
}
|
|
10031
|
-
|
|
10032
|
-
function shQuote2(s) {
|
|
10033
|
-
return `'${s.replace(/'/g, `'\\''`)}'`;
|
|
10119
|
+
return out.sort((a, b) => a.spawnedAt.localeCompare(b.spawnedAt));
|
|
10034
10120
|
}
|
|
10035
|
-
function
|
|
10036
|
-
|
|
10037
|
-
return `crew-${base.slice(0, 36)}-${now.toString(36)}`.replace(/-+/g, "-");
|
|
10121
|
+
function siblingRuns(runs, ownTicketRecordId) {
|
|
10122
|
+
return runs.filter((r) => !ownTicketRecordId || r.ticketRecordId !== ownTicketRecordId);
|
|
10038
10123
|
}
|
|
10039
|
-
|
|
10040
|
-
|
|
10041
|
-
|
|
10042
|
-
|
|
10043
|
-
|
|
10044
|
-
|
|
10045
|
-
|
|
10046
|
-
|
|
10047
|
-
}
|
|
10048
|
-
os(args, o = {}) {
|
|
10049
|
-
return execFileP(this.cfg.bin, args, { env: o.env ?? process.env, maxBuffer: 16 * 1024 * 1024 });
|
|
10050
|
-
}
|
|
10051
|
-
async start(plan, o) {
|
|
10052
|
-
const origin = this.originOf(plan.cwd);
|
|
10053
|
-
if (!origin) throw new Error(`cannot find the origin remote of ${plan.cwd}, so there is nothing to clone into the sandbox`);
|
|
10054
|
-
const repoName = basename7(origin).replace(/\.git$/, "");
|
|
10055
|
-
const name = sandboxName(plan);
|
|
10056
|
-
const { cfg } = this;
|
|
10057
|
-
const secretKeys = Object.keys(plan.setEnv);
|
|
10058
|
-
if (cfg.forgeTokenVar && o.env[cfg.forgeTokenVar] !== void 0 && !secretKeys.includes(cfg.forgeTokenVar)) secretKeys.push(cfg.forgeTokenVar);
|
|
10059
|
-
const createEnv = { ...process.env, ...plan.setEnv };
|
|
10060
|
-
if (cfg.forgeTokenVar && o.env[cfg.forgeTokenVar] !== void 0) createEnv[cfg.forgeTokenVar] = o.env[cfg.forgeTokenVar];
|
|
10061
|
-
const create2 = ["sandbox", "create", "--name", name, "--detach"];
|
|
10062
|
-
if (cfg.policy) create2.push("--policy", cfg.policy);
|
|
10063
|
-
if (cfg.image) create2.push("--from", cfg.image);
|
|
10064
|
-
if (cfg.cacheVolume) {
|
|
10065
|
-
create2.push("--driver-config-json", JSON.stringify({
|
|
10066
|
-
docker: { mounts: [{ type: "volume", source: cfg.cacheVolume, target: cfg.mirrorDir, read_only: true }] }
|
|
10067
|
-
}));
|
|
10068
|
-
}
|
|
10069
|
-
for (const k of secretKeys) create2.push("--env-from", k);
|
|
10070
|
-
const maxSeconds = Math.max(60, Math.ceil((plan.maxRunMinutes ?? 0) * 60) + 600);
|
|
10071
|
-
create2.push("--", "sleep", String(plan.maxRunMinutes ? maxSeconds : 86400));
|
|
10072
|
-
const t0 = Date.now();
|
|
10073
|
-
try {
|
|
10074
|
-
await this.os(create2, { env: createEnv });
|
|
10075
|
-
} catch (e) {
|
|
10076
|
-
await this.deleteQuietly(name);
|
|
10077
|
-
throw new Error(`openshell could not create the sandbox: ${describeExecError(e)}`);
|
|
10078
|
-
}
|
|
10079
|
-
o.emit.emit(`sandbox_created: ${name}`, { role: plan.role, data: { event: "sandbox_created", sandbox: name, ms: Date.now() - t0 } });
|
|
10080
|
-
const handle = { runner: "sandbox", sandbox: name, sandboxBin: cfg.bin };
|
|
10124
|
+
async function settleRunEnd(o) {
|
|
10125
|
+
const { tracker, emit } = o;
|
|
10126
|
+
const all = o.runs ?? liveRuns(o.stateDir);
|
|
10127
|
+
const slug2 = routeSlug(o.route.route);
|
|
10128
|
+
const seatSiblings = siblingRuns(all.filter((r) => r.role === o.role && r.route === slug2), o.ownTicketRecordId);
|
|
10129
|
+
const shipSiblings = siblingRuns(all, o.ownTicketRecordId);
|
|
10130
|
+
const newest = (rs) => rs[rs.length - 1];
|
|
10131
|
+
if (o.memberId) {
|
|
10081
10132
|
try {
|
|
10082
|
-
|
|
10083
|
-
|
|
10084
|
-
`git clone --quiet --shared ${shQuote2(`${cfg.mirrorDir}/${repoName}.git`)} ${shQuote2(cfg.workDir)}`,
|
|
10085
|
-
`cd ${shQuote2(cfg.workDir)}`,
|
|
10086
|
-
`git remote set-url origin ${shQuote2(origin)}`,
|
|
10087
|
-
`git checkout --quiet -b ${shQuote2(branch)}`
|
|
10088
|
-
].join(" && ");
|
|
10089
|
-
await this.os(["sandbox", "exec", "-n", name, "--no-login-shell", "--", "sh", "-c", clone]);
|
|
10090
|
-
const harness = cfg.agentBin ?? basename7(plan.bin);
|
|
10091
|
-
const viaUpload = Buffer.byteLength(plan.prompt) > SANDBOX_STDIN_LIMIT;
|
|
10092
|
-
if (viaUpload) {
|
|
10093
|
-
const tmp = mkdtempSync(join23(tmpdir3(), "crew-prompt-"));
|
|
10094
|
-
try {
|
|
10095
|
-
const file = join23(tmp, "prompt.txt");
|
|
10096
|
-
writeFileSync14(file, plan.prompt, { mode: 384 });
|
|
10097
|
-
await this.os(["sandbox", "upload", name, file, SANDBOX_PROMPT_PATH]);
|
|
10098
|
-
} finally {
|
|
10099
|
-
rmSync13(tmp, { recursive: true, force: true });
|
|
10100
|
-
}
|
|
10101
|
-
}
|
|
10102
|
-
const command2 = viaUpload ? ["sh", "-c", `exec ${[harness, ...plan.args].map(shQuote2).join(" ")} < ${SANDBOX_PROMPT_PATH}`] : [harness, ...plan.args];
|
|
10103
|
-
const child = spawn3(cfg.bin, ["sandbox", "exec", "-n", name, "--no-login-shell", "--workdir", cfg.workDir, "--", ...command2], {
|
|
10104
|
-
env: process.env,
|
|
10105
|
-
stdio: ["pipe", o.streamFd, "inherit"],
|
|
10106
|
-
detached: true
|
|
10107
|
-
});
|
|
10108
|
-
return { child, handle: { ...handle, pid: child.pid }, ...viaUpload ? { promptDelivered: true } : {} };
|
|
10133
|
+
if (seatSiblings.length) await tracker.setCrewStatus(o.memberId, "working", newest(seatSiblings).ticketRecordId);
|
|
10134
|
+
else await tracker.setCrewStatus(o.memberId, "idle");
|
|
10109
10135
|
} catch (e) {
|
|
10110
|
-
|
|
10111
|
-
throw new Error(`could not prepare the sandbox ${name}: ${describeExecError(e)}`);
|
|
10136
|
+
emit.warn(`could not clear crew status: ${e.message}`, { step: "agent" });
|
|
10112
10137
|
}
|
|
10113
10138
|
}
|
|
10114
|
-
|
|
10115
|
-
if (
|
|
10116
|
-
|
|
10117
|
-
|
|
10118
|
-
|
|
10119
|
-
|
|
10120
|
-
}
|
|
10121
|
-
reattach(meta) {
|
|
10122
|
-
return { runner: "sandbox", pid: meta.pid, sandbox: meta.sandbox, sandboxBin: meta.sandboxBin };
|
|
10123
|
-
}
|
|
10124
|
-
async release(handle, emit) {
|
|
10125
|
-
if (!handle.sandbox) return;
|
|
10126
|
-
const t0 = Date.now();
|
|
10127
|
-
await this.deleteQuietly(handle.sandbox, handle.sandboxBin);
|
|
10128
|
-
emit.emit(`sandbox_deleted: ${handle.sandbox}`, { data: { event: "sandbox_deleted", sandbox: handle.sandbox, ms: Date.now() - t0 } });
|
|
10129
|
-
}
|
|
10130
|
-
async deleteQuietly(name, bin = this.cfg.bin) {
|
|
10131
|
-
try {
|
|
10132
|
-
await execFileP(bin, ["sandbox", "delete", name], { timeout: 6e4 });
|
|
10133
|
-
} catch {
|
|
10139
|
+
try {
|
|
10140
|
+
if (shipSiblings.length) {
|
|
10141
|
+
const n = newest(shipSiblings);
|
|
10142
|
+
await tracker.beatEngaged(o.shipName, n.routeName ?? n.route, n.ticketRecordId, n.spawnedAt);
|
|
10143
|
+
} else {
|
|
10144
|
+
await tracker.beatIdle(o.shipName);
|
|
10134
10145
|
}
|
|
10146
|
+
} catch (e) {
|
|
10147
|
+
emit.warn(`could not beat ship idle: ${e.message}`, { step: "agent" });
|
|
10135
10148
|
}
|
|
10136
|
-
};
|
|
10137
|
-
function describeExecError(e) {
|
|
10138
|
-
const err3 = e;
|
|
10139
|
-
return (err3.stderr?.trim() || err3.message || String(e)).split("\n").slice(-3).join(" / ");
|
|
10140
10149
|
}
|
|
10141
|
-
function
|
|
10150
|
+
function describeRun(run3, route2, seatIds, worktreeOf = (d, n) => {
|
|
10142
10151
|
try {
|
|
10143
|
-
return
|
|
10152
|
+
return worktreeForNumber(d, n)?.path ?? null;
|
|
10144
10153
|
} catch {
|
|
10145
10154
|
return null;
|
|
10146
10155
|
}
|
|
10156
|
+
}) {
|
|
10157
|
+
const ownRoute = run3.route === routeSlug(route2.route);
|
|
10158
|
+
const repoDir = run3.repo ? reposOf(route2).find((t) => t.name === run3.repo)?.dir : void 0;
|
|
10159
|
+
const num = run3.ticket?.replace(/^\D+/, "") ?? "";
|
|
10160
|
+
return {
|
|
10161
|
+
role: run3.role,
|
|
10162
|
+
seat: ownRoute ? seatIds[run3.role] ?? null : null,
|
|
10163
|
+
ticket: run3.ticket,
|
|
10164
|
+
ticketRecordId: run3.ticketRecordId,
|
|
10165
|
+
repo: run3.repo ?? null,
|
|
10166
|
+
worktree: repoDir && num ? worktreeOf(repoDir, num) : null,
|
|
10167
|
+
stream: run3.streamPath,
|
|
10168
|
+
pid: run3.pid,
|
|
10169
|
+
startedAt: run3.spawnedAt,
|
|
10170
|
+
deadline: run3.deadline ?? null
|
|
10171
|
+
};
|
|
10147
10172
|
}
|
|
10148
|
-
|
|
10149
|
-
|
|
10173
|
+
|
|
10174
|
+
// src/seat-keys.ts
|
|
10175
|
+
import { hostFromUrl as hostFromUrl2 } from "@tablation/client";
|
|
10176
|
+
var SEAT_KEY_ROLES = ROLE_NAMES.filter((r) => r !== "pair");
|
|
10177
|
+
function keyIsAdmin(route2) {
|
|
10178
|
+
const role = route2.resolved?.role;
|
|
10179
|
+
return role === void 0 || role === "WORKSPACE_ADMIN" || role === "PLATFORM_ADMIN";
|
|
10150
10180
|
}
|
|
10151
|
-
|
|
10152
|
-
|
|
10181
|
+
var MEMBER_NO_ROW = "has no linked Agents row \u2014 a workspace admin's `crew agents sync` creates the persona rows; run it here afterwards to link this ship's seats";
|
|
10182
|
+
var MEMBER_NO_KEY = 'no agent key for this seat \u2014 run `crew agents sync` (needs the "Mint agent keys" capability on your role)';
|
|
10183
|
+
var FORBIDDEN_LINE = 'seat keys: not minted \u2014 your role does not grant "Mint agent keys"; ask a workspace admin to add it to your project role (or run sync with an admin key)';
|
|
10184
|
+
function seatKeySlug(workspaceSlug, agentId) {
|
|
10185
|
+
return `${workspaceSlug}#agent:${agentId}`;
|
|
10153
10186
|
}
|
|
10154
|
-
function
|
|
10155
|
-
const
|
|
10156
|
-
|
|
10187
|
+
function slugFor(route2, agentId) {
|
|
10188
|
+
const [workspaceSlug] = splitRoute(route2.route);
|
|
10189
|
+
return seatKeySlug(workspaceSlug, agentId);
|
|
10190
|
+
}
|
|
10191
|
+
function seatAgentId(route2, role) {
|
|
10192
|
+
return route2.resolved?.agentPersonas?.[role]?.agentId;
|
|
10193
|
+
}
|
|
10194
|
+
async function resolveReleaseKey(route2, getStore = getSessionStore) {
|
|
10195
|
+
const ship = (reason) => ({ kind: "ship", apiKey: resolveApiKey(route2), reason });
|
|
10196
|
+
const agentId = route2.resolved?.releaseAgentId;
|
|
10197
|
+
if (!agentId) return ship(keyIsAdmin(route2) ? "release seat has no linked Agents row (run `crew agents sync`)" : `release seat ${MEMBER_NO_ROW}`);
|
|
10157
10198
|
try {
|
|
10158
|
-
|
|
10199
|
+
const session = await getStore().get(hostFromUrl2(route2.baseUrl), slugFor(route2, agentId));
|
|
10200
|
+
if (session?.apiKey) return { kind: "agent", apiKey: session.apiKey, agentId };
|
|
10201
|
+
return ship(keyIsAdmin(route2) ? "no agent key stored for the release seat (run `crew agents sync`)" : MEMBER_NO_KEY);
|
|
10159
10202
|
} catch (e) {
|
|
10160
|
-
|
|
10203
|
+
const reason = describeReadError(e);
|
|
10204
|
+
return { kind: "ship", apiKey: resolveApiKey(route2), reason, storeError: reason };
|
|
10161
10205
|
}
|
|
10206
|
+
}
|
|
10207
|
+
async function resolveSeatKey(route2, role, getStore = getSessionStore) {
|
|
10208
|
+
const ship = (reason) => ({ kind: "ship", apiKey: resolveApiKey(route2), reason });
|
|
10209
|
+
if (!SEAT_KEY_ROLES.includes(role)) return ship(`${role} always writes as the ship`);
|
|
10210
|
+
const agentId = seatAgentId(route2, role);
|
|
10211
|
+
if (!agentId) return ship(keyIsAdmin(route2) ? "seat has no linked Agents row (run `crew agents sync`)" : `seat ${MEMBER_NO_ROW}`);
|
|
10162
10212
|
try {
|
|
10163
|
-
|
|
10213
|
+
const session = await getStore().get(hostFromUrl2(route2.baseUrl), slugFor(route2, agentId));
|
|
10214
|
+
if (session?.apiKey) return { kind: "agent", apiKey: session.apiKey, agentId };
|
|
10215
|
+
return ship(keyIsAdmin(route2) ? "no agent key stored for this seat (run `crew agents sync`)" : MEMBER_NO_KEY);
|
|
10164
10216
|
} catch (e) {
|
|
10165
|
-
const
|
|
10166
|
-
return {
|
|
10167
|
-
ok: false,
|
|
10168
|
-
detail: `${version}: the gateway does not answer (${(err3.stderr || err3.message).trim().split("\n").filter(Boolean).slice(0, 2).join(" / ")}). Start it; on Docker Desktop its docker section needs supervisor_network_mode = "bridge", and allow_driver_config = true for the cache volume mount.`
|
|
10169
|
-
};
|
|
10217
|
+
const reason = describeReadError(e);
|
|
10218
|
+
return { kind: "ship", apiKey: resolveApiKey(route2), reason, storeError: reason };
|
|
10170
10219
|
}
|
|
10171
|
-
return { ok: true, detail: version };
|
|
10172
10220
|
}
|
|
10173
|
-
|
|
10174
|
-
|
|
10175
|
-
|
|
10176
|
-
|
|
10177
|
-
|
|
10178
|
-
|
|
10179
|
-
|
|
10180
|
-
|
|
10181
|
-
return
|
|
10221
|
+
function safeStderrLine(e) {
|
|
10222
|
+
const err3 = e;
|
|
10223
|
+
const raw = typeof err3?.stderr === "string" && err3.stderr.trim() ? err3.stderr : typeof err3?.message === "string" ? err3.message : "";
|
|
10224
|
+
for (const line of raw.split("\n")) {
|
|
10225
|
+
const t = line.trim();
|
|
10226
|
+
if (!t || /^Command failed:/i.test(t) || /(^|\s)-w(\s|$)/.test(t)) continue;
|
|
10227
|
+
return t.slice(0, 120);
|
|
10228
|
+
}
|
|
10229
|
+
return void 0;
|
|
10182
10230
|
}
|
|
10183
|
-
|
|
10184
|
-
|
|
10185
|
-
|
|
10186
|
-
|
|
10187
|
-
|
|
10231
|
+
function describeReadError(e) {
|
|
10232
|
+
const fsErr = e;
|
|
10233
|
+
if (typeof fsErr?.path === "string" && typeof fsErr.code === "string") return `key file store unreadable (${fsErr.code})`;
|
|
10234
|
+
const code = e?.code;
|
|
10235
|
+
const line = safeStderrLine(e);
|
|
10236
|
+
const what = typeof code === "number" ? `security exited ${code}` : typeof code === "string" ? `security failed: ${code}` : void 0;
|
|
10237
|
+
const detail = [what, line].filter(Boolean).join(what && line ? ": " : "");
|
|
10238
|
+
return detail ? `OS keychain unavailable (${detail})` : "OS keychain unavailable";
|
|
10188
10239
|
}
|
|
10189
|
-
|
|
10190
|
-
|
|
10191
|
-
|
|
10192
|
-
}
|
|
10193
|
-
|
|
10194
|
-
|
|
10195
|
-
|
|
10196
|
-
|
|
10197
|
-
|
|
10198
|
-
|
|
10199
|
-
|
|
10240
|
+
function keychainExitCode(reason) {
|
|
10241
|
+
const m = /security exited (\d+)/.exec(reason);
|
|
10242
|
+
return m ? Number(m[1]) : void 0;
|
|
10243
|
+
}
|
|
10244
|
+
function describeStoreError(e) {
|
|
10245
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
10246
|
+
if (/User interaction is not allowed/i.test(msg)) {
|
|
10247
|
+
return "keychain unavailable (User interaction is not allowed \u2014 run from a logged-in session, or `security unlock-keychain` first)";
|
|
10248
|
+
}
|
|
10249
|
+
return "keychain unavailable (could not write to the OS keychain)";
|
|
10250
|
+
}
|
|
10251
|
+
async function mintMissingSeatKeys(route2, o) {
|
|
10252
|
+
const out = [];
|
|
10253
|
+
if (!route2.resolved) return out;
|
|
10254
|
+
const resolved = route2.resolved;
|
|
10255
|
+
const doFetch = o.fetchImpl ?? fetch;
|
|
10256
|
+
const targets = [
|
|
10257
|
+
...SEAT_KEY_ROLES.filter((r) => resolved.seats[r]).map((role) => ({ role, agentId: seatAgentId(route2, role) })),
|
|
10258
|
+
...resolved.releaseSeat ? [{ role: "release", agentId: resolved.releaseAgentId }] : []
|
|
10259
|
+
];
|
|
10260
|
+
let store;
|
|
10200
10261
|
try {
|
|
10201
|
-
|
|
10202
|
-
} catch {
|
|
10203
|
-
return
|
|
10262
|
+
store = (o.getStore ?? getSessionStore)();
|
|
10263
|
+
} catch (e) {
|
|
10264
|
+
return targets.map(({ role }) => ({
|
|
10265
|
+
role,
|
|
10266
|
+
action: "failed",
|
|
10267
|
+
detail: describeStoreError(e)
|
|
10268
|
+
}));
|
|
10204
10269
|
}
|
|
10205
|
-
|
|
10206
|
-
|
|
10207
|
-
|
|
10208
|
-
|
|
10270
|
+
const host = hostFromUrl2(route2.baseUrl);
|
|
10271
|
+
const base = route2.baseUrl.replace(/\/+$/, "");
|
|
10272
|
+
if (!o.dryRun) {
|
|
10273
|
+
const probeSlug = seatKeySlug(splitRoute(route2.route)[0], "_probe");
|
|
10209
10274
|
try {
|
|
10210
|
-
|
|
10211
|
-
|
|
10275
|
+
await store.set(host, probeSlug, {
|
|
10276
|
+
apiKey: "probe",
|
|
10277
|
+
workspaceId: resolved.workspaceId,
|
|
10278
|
+
identityId: "_probe",
|
|
10279
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
10280
|
+
});
|
|
10281
|
+
await store.clear(host, probeSlug);
|
|
10282
|
+
} catch (e) {
|
|
10283
|
+
const detail = `${describeStoreError(e)}; no keys minted`;
|
|
10284
|
+
return targets.map(({ role, agentId }) => agentId ? { role, action: "failed", detail } : { role, action: "no_agent" });
|
|
10285
|
+
}
|
|
10286
|
+
}
|
|
10287
|
+
for (const { role, agentId } of targets) {
|
|
10288
|
+
if (!agentId) {
|
|
10289
|
+
out.push({ role, action: "no_agent" });
|
|
10212
10290
|
continue;
|
|
10213
10291
|
}
|
|
10214
|
-
|
|
10215
|
-
|
|
10216
|
-
|
|
10217
|
-
|
|
10218
|
-
|
|
10219
|
-
|
|
10292
|
+
const slug2 = slugFor(route2, agentId);
|
|
10293
|
+
try {
|
|
10294
|
+
if ((await store.get(host, slug2))?.apiKey) {
|
|
10295
|
+
out.push({ role, action: "already_stored" });
|
|
10296
|
+
continue;
|
|
10297
|
+
}
|
|
10298
|
+
if (o.dryRun) {
|
|
10299
|
+
out.push({ role, action: "minted", detail: "dry run" });
|
|
10300
|
+
continue;
|
|
10301
|
+
}
|
|
10302
|
+
const res = await doFetch(`${base}/api/workspaces/${resolved.workspaceId}/agents/${agentId}/api-keys`, {
|
|
10303
|
+
method: "POST",
|
|
10304
|
+
headers: {
|
|
10305
|
+
Authorization: `Bearer ${resolveApiKey(route2)}`,
|
|
10306
|
+
"Content-Type": "application/json",
|
|
10307
|
+
"User-Agent": o.userAgent ?? "Mozilla/5.0 TablationCrewAgent/1.0"
|
|
10308
|
+
},
|
|
10309
|
+
body: JSON.stringify({ name: `crew ${o.shipName ?? "ship"} ${role}` })
|
|
10310
|
+
});
|
|
10311
|
+
if (res.status === 403) {
|
|
10312
|
+
out.push({ role, action: "forbidden" });
|
|
10313
|
+
break;
|
|
10314
|
+
}
|
|
10315
|
+
if (!res.ok) {
|
|
10316
|
+
out.push({ role, action: "failed", detail: `${res.status} ${res.statusText}` });
|
|
10317
|
+
continue;
|
|
10318
|
+
}
|
|
10319
|
+
const body = await res.json();
|
|
10320
|
+
if (!body.key) {
|
|
10321
|
+
out.push({ role, action: "failed", detail: "response carried no key" });
|
|
10322
|
+
continue;
|
|
10323
|
+
}
|
|
10324
|
+
try {
|
|
10325
|
+
const session = {
|
|
10326
|
+
apiKey: body.key,
|
|
10327
|
+
workspaceId: resolved.workspaceId,
|
|
10328
|
+
identityId: agentId,
|
|
10329
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
10330
|
+
...body.id ? { keyId: body.id } : {}
|
|
10331
|
+
};
|
|
10332
|
+
await store.set(host, slug2, session);
|
|
10333
|
+
} catch (e) {
|
|
10334
|
+
let revoked = false;
|
|
10335
|
+
if (body.id) {
|
|
10336
|
+
try {
|
|
10337
|
+
const del2 = await doFetch(`${base}/api/workspaces/${resolved.workspaceId}/agents/${agentId}/api-keys/${body.id}`, {
|
|
10338
|
+
method: "DELETE",
|
|
10339
|
+
headers: { Authorization: `Bearer ${resolveApiKey(route2)}`, "User-Agent": o.userAgent ?? "Mozilla/5.0 TablationCrewAgent/1.0" }
|
|
10340
|
+
});
|
|
10341
|
+
revoked = del2.ok;
|
|
10342
|
+
} catch {
|
|
10343
|
+
revoked = false;
|
|
10344
|
+
}
|
|
10345
|
+
}
|
|
10346
|
+
out.push({
|
|
10347
|
+
role,
|
|
10348
|
+
action: "failed",
|
|
10349
|
+
detail: revoked ? `minted but could not be stored (${describeStoreError(e)}); revoked` : `a key for ${role} is live on the server and not stored (${describeStoreError(e)}) \u2014 revoke it from that seat's Agents row`
|
|
10350
|
+
});
|
|
10351
|
+
continue;
|
|
10352
|
+
}
|
|
10353
|
+
out.push({ role, action: "minted" });
|
|
10354
|
+
} catch (e) {
|
|
10355
|
+
out.push({ role, action: "failed", detail: /Command failed|security|sk_|-w /i.test(e.message) ? describeStoreError(e) : e.message });
|
|
10220
10356
|
}
|
|
10221
10357
|
}
|
|
10222
|
-
return
|
|
10358
|
+
return out;
|
|
10223
10359
|
}
|
|
10224
|
-
|
|
10225
|
-
|
|
10226
|
-
|
|
10227
|
-
|
|
10228
|
-
|
|
10229
|
-
|
|
10230
|
-
|
|
10231
|
-
|
|
10232
|
-
|
|
10233
|
-
|
|
10234
|
-
|
|
10235
|
-
|
|
10236
|
-
let digest;
|
|
10237
|
-
let ageSeconds;
|
|
10238
|
-
if (existsSync21(digestPath2)) {
|
|
10239
|
-
ageSeconds = Math.floor((now() - statSync7(digestPath2).mtimeMs) / 1e3);
|
|
10240
|
-
if (ageSeconds <= DIGEST_MAX_AGE_SECONDS) digest = readFileSync20(digestPath2, "utf8");
|
|
10360
|
+
function describeMintOutcome(o) {
|
|
10361
|
+
switch (o.action) {
|
|
10362
|
+
case "minted":
|
|
10363
|
+
return `${o.role}: agent key minted${o.detail ? ` (${o.detail})` : ""}`;
|
|
10364
|
+
case "already_stored":
|
|
10365
|
+
return `${o.role}: agent key already stored`;
|
|
10366
|
+
case "no_agent":
|
|
10367
|
+
return `${o.role}: no linked Agents row \u2014 writes as the ship until \`crew agents sync\` links one`;
|
|
10368
|
+
case "forbidden":
|
|
10369
|
+
return FORBIDDEN_LINE;
|
|
10370
|
+
case "failed":
|
|
10371
|
+
return `${o.role}: could not mint an agent key (${o.detail ?? "unknown"}) \u2014 writes as the ship`;
|
|
10241
10372
|
}
|
|
10242
|
-
|
|
10243
|
-
|
|
10244
|
-
const
|
|
10245
|
-
|
|
10246
|
-
const
|
|
10247
|
-
|
|
10248
|
-
|
|
10249
|
-
|
|
10250
|
-
|
|
10251
|
-
|
|
10252
|
-
|
|
10253
|
-
|
|
10254
|
-
|
|
10255
|
-
|
|
10373
|
+
}
|
|
10374
|
+
async function revokeSeatKeys(route2, o) {
|
|
10375
|
+
const out = [];
|
|
10376
|
+
if (!route2.resolved) return out;
|
|
10377
|
+
const resolved = route2.resolved;
|
|
10378
|
+
const doFetch = o.fetchImpl ?? fetch;
|
|
10379
|
+
const targets = [
|
|
10380
|
+
...SEAT_KEY_ROLES.filter((r) => resolved.seats[r]).map((role) => ({ role, agentId: seatAgentId(route2, role) })),
|
|
10381
|
+
...resolved.releaseSeat ? [{ role: "release", agentId: resolved.releaseAgentId }] : []
|
|
10382
|
+
];
|
|
10383
|
+
let store;
|
|
10384
|
+
try {
|
|
10385
|
+
store = (o.getStore ?? getSessionStore)();
|
|
10386
|
+
} catch (e) {
|
|
10387
|
+
return targets.map(({ role }) => ({ role, action: "failed", detail: describeStoreError(e) }));
|
|
10256
10388
|
}
|
|
10257
|
-
const
|
|
10258
|
-
|
|
10259
|
-
|
|
10260
|
-
|
|
10261
|
-
|
|
10262
|
-
|
|
10263
|
-
});
|
|
10264
|
-
const promptSha = createHash6("sha256").update(expanded.text).digest("hex");
|
|
10265
|
-
const streamsDir = join24(o.stateDir, "streams");
|
|
10266
|
-
const base = `${routeLabel}-${o.role}-${o.cycle}${o.runId ? `-${o.runId}` : ""}`;
|
|
10267
|
-
const seat = resolveSeat(o.ship, o.route, o.role, o.agentPersona ?? (o.agentModel ? { model: o.agentModel } : {}), o.repo);
|
|
10268
|
-
const { model } = seat;
|
|
10269
|
-
let toolchain = seat.toolchain;
|
|
10270
|
-
if (toolchain.auth === "workspace") {
|
|
10271
|
-
if (!o.apiKey || o.seatKind === "ship") {
|
|
10272
|
-
throw new WorkspaceAuthRefused(`${o.role}: toolchain ${toolchain.name} has auth: workspace, which needs the seat's own agent key \u2014 this seat would fall back to the ship key (run \`crew agents sync\`)`);
|
|
10389
|
+
const host = hostFromUrl2(route2.baseUrl);
|
|
10390
|
+
const base = route2.baseUrl.replace(/\/+$/, "");
|
|
10391
|
+
for (const { role, agentId } of targets) {
|
|
10392
|
+
if (!agentId) {
|
|
10393
|
+
out.push({ role, action: "no_agent" });
|
|
10394
|
+
continue;
|
|
10273
10395
|
}
|
|
10274
|
-
|
|
10275
|
-
|
|
10396
|
+
const slug2 = slugFor(route2, agentId);
|
|
10397
|
+
try {
|
|
10398
|
+
const session = await store.get(host, slug2);
|
|
10399
|
+
if (!session?.apiKey) {
|
|
10400
|
+
out.push({ role, action: "not_stored" });
|
|
10401
|
+
continue;
|
|
10402
|
+
}
|
|
10403
|
+
if (!session.keyId) {
|
|
10404
|
+
out.push({ role, action: "failed", detail: "no key id was recorded when this key was minted \u2014 revoke it from the seat's Agents row" });
|
|
10405
|
+
continue;
|
|
10406
|
+
}
|
|
10407
|
+
if (o.dryRun) {
|
|
10408
|
+
out.push({ role, action: "revoked", detail: "dry run" });
|
|
10409
|
+
continue;
|
|
10410
|
+
}
|
|
10411
|
+
const del2 = await doFetch(`${base}/api/workspaces/${resolved.workspaceId}/agents/${agentId}/api-keys/${session.keyId}`, {
|
|
10412
|
+
method: "DELETE",
|
|
10413
|
+
headers: { Authorization: `Bearer ${resolveApiKey(route2)}`, "User-Agent": o.userAgent ?? "Mozilla/5.0 TablationCrewAgent/1.0" }
|
|
10414
|
+
});
|
|
10415
|
+
if (!del2.ok && del2.status !== 404) {
|
|
10416
|
+
out.push({ role, action: "failed", detail: `${del2.status} ${del2.statusText}` });
|
|
10417
|
+
continue;
|
|
10418
|
+
}
|
|
10419
|
+
await store.clear(host, slug2);
|
|
10420
|
+
out.push({ role, action: "revoked", detail: del2.ok ? void 0 : "server already had no such key" });
|
|
10421
|
+
} catch (e) {
|
|
10422
|
+
out.push({ role, action: "failed", detail: /Command failed|security|sk_|-w /i.test(e.message) ? describeStoreError(e) : e.message });
|
|
10276
10423
|
}
|
|
10277
|
-
toolchain = { ...toolchain, inference: { baseUrl: inferenceBaseUrl(o.route.baseUrl, o.route.resolved.workspaceId), token: o.apiKey } };
|
|
10278
10424
|
}
|
|
10279
|
-
|
|
10280
|
-
const invocation = adapter.invocation(toolchain, {
|
|
10281
|
-
role: o.role,
|
|
10282
|
-
model,
|
|
10283
|
-
// Skipped when there's no key to send: a route `crew connect` hasn't
|
|
10284
|
-
// resolved yet has nothing to authenticate the MCP connection with either.
|
|
10285
|
-
mcp: o.apiKey ? { url: `${o.route.baseUrl}/api/mcp`, keyVar: API_KEY_VAR } : void 0
|
|
10286
|
-
});
|
|
10287
|
-
const columns = resolveContract(o.route.contract).columns;
|
|
10288
|
-
const agentLog = o.route.resolved && o.apiKey ? {
|
|
10289
|
-
baseUrl: o.route.baseUrl,
|
|
10290
|
-
workspaceId: o.route.resolved.workspaceId,
|
|
10291
|
-
apiKey: o.apiKey,
|
|
10292
|
-
userAgent: o.ship.userAgent,
|
|
10293
|
-
agentId: o.resolvedAgentId,
|
|
10294
|
-
...o.route.resolved.models?.issues ? { tickets: {
|
|
10295
|
-
issuesModelId: o.route.resolved.models.issues,
|
|
10296
|
-
keyColumn: columns.key,
|
|
10297
|
-
tagColumn: columns.tag,
|
|
10298
|
-
counters: {
|
|
10299
|
-
agentRuns: columns.agentRuns,
|
|
10300
|
-
tokensIn: columns.tokensIn,
|
|
10301
|
-
tokensOut: columns.tokensOut,
|
|
10302
|
-
cacheReadTokens: columns.cacheReadTokens,
|
|
10303
|
-
cacheWriteTokens: columns.cacheWriteTokens,
|
|
10304
|
-
costUsd: columns.costUsd
|
|
10305
|
-
}
|
|
10306
|
-
} } : {}
|
|
10307
|
-
} : void 0;
|
|
10308
|
-
return {
|
|
10309
|
-
role: o.role,
|
|
10310
|
-
route: routeLabel,
|
|
10311
|
-
// The route's directory, which is a STARTING POINT and not the
|
|
10312
|
-
// ticket's repo: the session picks its own working directory even when
|
|
10313
|
-
// the runner already selected a ticket (`o.repo` is known then, and
|
|
10314
|
-
// only feeds the per-repo toolchain choice). Where an area spans
|
|
10315
|
-
// several repos this is whichever one `repos` happened to list first
|
|
10316
|
-
// (config.ts), and the digest's `repo` column is what actually places
|
|
10317
|
-
// the work. Kept deliberately rather than guessed at (ISSUE-349).
|
|
10318
|
-
cwd: o.route.dir,
|
|
10319
|
-
bin: invocation.bin,
|
|
10320
|
-
args: invocation.args,
|
|
10321
|
-
toolchain: toolchain.name,
|
|
10322
|
-
harness: toolchain.harness,
|
|
10323
|
-
workspaceAuth: toolchain.auth === "workspace",
|
|
10324
|
-
provider: toolchainProvider(toolchain),
|
|
10325
|
-
maxRunMinutes: resolveMaxRunMinutes(o.route.maxRunMinutes, toolchain.maxRunMinutes),
|
|
10326
|
-
...o.ship.runner === "sandbox" ? { runner: "sandbox", ...o.ship.sandbox ? { sandboxConfig: o.ship.sandbox } : {} } : {},
|
|
10327
|
-
resolution: { toolchain: seat.toolchainWhy, model: seat.modelWhy },
|
|
10328
|
-
warnings: [...seat.warnings ?? [], ...expanded.warnings],
|
|
10329
|
-
prompt,
|
|
10330
|
-
promptBytes: Buffer.byteLength(prompt, "utf8"),
|
|
10331
|
-
unsetEnv: adapter.unsetEnv,
|
|
10332
|
-
setEnv: {
|
|
10333
|
-
...o.apiKey ? { [API_KEY_VAR]: o.apiKey } : {},
|
|
10334
|
-
...adapter.runEnv(toolchain),
|
|
10335
|
-
// The operator's global SessionStart hook (pair-context-hook.sh) injects
|
|
10336
|
-
// the interactive Pair persona's prompt for any session it can't tell
|
|
10337
|
-
// apart from a person's own terminal. Every headless lane run this
|
|
10338
|
-
// planner produces carries a real ticket brief already (`prompt`
|
|
10339
|
-
// above) — it must never be replaced by the Pair persona with no
|
|
10340
|
-
// ticket attached, which silently no-ops and strands the ticket
|
|
10341
|
-
// in_progress (ISSUE-490, ISSUE-591). Setting this is what lets that
|
|
10342
|
-
// hook tell the two apart; it has nothing to do with the run itself.
|
|
10343
|
-
[CREW_LANE_ROLE_VAR]: o.role,
|
|
10344
|
-
// CREW-2783: the ship's forge credential — git helper config plus the
|
|
10345
|
-
// token as an env var for `gh` and friends. Empty without `forges:`.
|
|
10346
|
-
...forgeRunEnv(o.ship.forges, { crewCommand: crewCommandLine(), configFile: process.env.CREW_CONFIG, stateDir: o.stateDir })
|
|
10347
|
-
},
|
|
10348
|
-
digestAttached: digest !== void 0,
|
|
10349
|
-
digestAgeSeconds: ageSeconds,
|
|
10350
|
-
model,
|
|
10351
|
-
streamPath: join24(streamsDir, `${base}.jsonl`),
|
|
10352
|
-
eventsPath: join24(streamsDir, `${base}.events.jsonl`),
|
|
10353
|
-
ticket: o.ticket,
|
|
10354
|
-
ticketRecordId: o.ticketRecordId ?? null,
|
|
10355
|
-
repo: o.repo ?? null,
|
|
10356
|
-
routeName: o.route.route,
|
|
10357
|
-
agentLog,
|
|
10358
|
-
promptVersion: o.route.resolved?.agentPersonas?.[o.role]?.historyId,
|
|
10359
|
-
promptSha
|
|
10360
|
-
};
|
|
10425
|
+
return out;
|
|
10361
10426
|
}
|
|
10362
|
-
function
|
|
10363
|
-
|
|
10364
|
-
|
|
10365
|
-
|
|
10366
|
-
|
|
10367
|
-
|
|
10368
|
-
|
|
10369
|
-
|
|
10370
|
-
|
|
10371
|
-
|
|
10372
|
-
|
|
10373
|
-
`unset: ${p.unsetEnv.join(" ")}`,
|
|
10374
|
-
// The VALUE is deliberately absent: --dry-run output is pasted into
|
|
10375
|
-
// tickets and chat.
|
|
10376
|
-
`set: ${Object.keys(p.setEnv).join(" ") || "(nothing)"}`,
|
|
10377
|
-
`stream: ${p.streamPath}`
|
|
10378
|
-
].join("\n");
|
|
10427
|
+
function describeRevokeOutcome(o) {
|
|
10428
|
+
switch (o.action) {
|
|
10429
|
+
case "revoked":
|
|
10430
|
+
return `${o.role}: agent key revoked and cleared from the keychain${o.detail ? ` (${o.detail})` : ""}`;
|
|
10431
|
+
case "not_stored":
|
|
10432
|
+
return `${o.role}: no agent key stored on this ship`;
|
|
10433
|
+
case "no_agent":
|
|
10434
|
+
return `${o.role}: no linked Agents row`;
|
|
10435
|
+
case "failed":
|
|
10436
|
+
return `${o.role}: could not revoke the agent key (${o.detail ?? "unknown"}) \u2014 left in the keychain`;
|
|
10437
|
+
}
|
|
10379
10438
|
}
|
|
10380
|
-
|
|
10381
|
-
|
|
10439
|
+
async function canMintAgentKeys(route2, o = {}) {
|
|
10440
|
+
const resolved = route2.resolved;
|
|
10441
|
+
if (!resolved) return void 0;
|
|
10442
|
+
const agentId = seatAgentId(route2, "dev") ?? SEAT_KEY_ROLES.map((r) => seatAgentId(route2, r)).find(Boolean);
|
|
10443
|
+
if (!agentId) return void 0;
|
|
10382
10444
|
try {
|
|
10383
|
-
const
|
|
10384
|
-
|
|
10445
|
+
const res = await (o.fetchImpl ?? fetch)(`${route2.baseUrl.replace(/\/+$/, "")}/api/workspaces/${resolved.workspaceId}/agents/${agentId}/api-keys`, {
|
|
10446
|
+
headers: { Authorization: `Bearer ${resolveApiKey(route2)}`, "User-Agent": o.userAgent ?? "Mozilla/5.0 TablationCrewAgent/1.0" }
|
|
10447
|
+
});
|
|
10448
|
+
if (res.ok) return true;
|
|
10449
|
+
return res.status === 403 ? false : void 0;
|
|
10385
10450
|
} catch {
|
|
10386
|
-
return
|
|
10451
|
+
return void 0;
|
|
10387
10452
|
}
|
|
10388
10453
|
}
|
|
10389
|
-
|
|
10390
|
-
|
|
10391
|
-
|
|
10392
|
-
|
|
10393
|
-
|
|
10394
|
-
|
|
10395
|
-
const
|
|
10396
|
-
|
|
10397
|
-
|
|
10398
|
-
return Math.abs(started - Date.parse(sc.spawnedAt)) < 15e3;
|
|
10454
|
+
|
|
10455
|
+
// src/deprovision-host.ts
|
|
10456
|
+
var DOCKER_LABEL_OWNER = "crew.owner=crew";
|
|
10457
|
+
var DOCKER_LABEL_CACHE = "crew.kind=cache";
|
|
10458
|
+
var DOCKER_LABEL_REPO_LAYER = "crew.layer=repo";
|
|
10459
|
+
function docker(args) {
|
|
10460
|
+
const res = spawnSync5("docker", args, { encoding: "utf8" });
|
|
10461
|
+
if (res.error || res.status !== 0) throw new Error(`docker ${args[0]} ${args[1] ?? ""} failed: ${(res.stderr || String(res.error ?? "")).trim().split("\n").pop()}`);
|
|
10462
|
+
return res.stdout;
|
|
10399
10463
|
}
|
|
10400
|
-
|
|
10464
|
+
var lines = (s) => s.split("\n").map((l) => l.trim()).filter(Boolean);
|
|
10465
|
+
function labeledDocker() {
|
|
10466
|
+
if (!dockerAvailable()) return { volumes: [], images: [] };
|
|
10401
10467
|
return {
|
|
10402
|
-
|
|
10403
|
-
|
|
10404
|
-
ticket: plan.ticket ?? null,
|
|
10405
|
-
model: plan.model,
|
|
10406
|
-
toolchain: plan.toolchain ?? LEGACY_TOOLCHAIN_HARNESS,
|
|
10407
|
-
harness: adapterHarness,
|
|
10408
|
-
provider: plan.provider ?? LEGACY_TOOLCHAIN_PROVIDER,
|
|
10409
|
-
eventsPath: plan.eventsPath,
|
|
10410
|
-
streamPath: plan.streamPath,
|
|
10411
|
-
agentLog: plan.agentLog ? withoutKey(plan.agentLog) : void 0,
|
|
10412
|
-
promptVersion: plan.promptVersion,
|
|
10413
|
-
promptSha: plan.promptSha
|
|
10468
|
+
volumes: lines(docker(["volume", "ls", "-q", "--filter", `label=${DOCKER_LABEL_OWNER}`, "--filter", `label=${DOCKER_LABEL_CACHE}`])),
|
|
10469
|
+
images: lines(docker(["image", "ls", "-q", "--filter", `label=${DOCKER_LABEL_OWNER}`, "--filter", `label=${DOCKER_LABEL_REPO_LAYER}`]))
|
|
10414
10470
|
};
|
|
10415
10471
|
}
|
|
10416
|
-
|
|
10417
|
-
const
|
|
10418
|
-
const
|
|
10419
|
-
|
|
10420
|
-
|
|
10421
|
-
|
|
10422
|
-
|
|
10423
|
-
|
|
10424
|
-
|
|
10425
|
-
|
|
10426
|
-
|
|
10427
|
-
|
|
10428
|
-
|
|
10429
|
-
|
|
10430
|
-
|
|
10431
|
-
|
|
10432
|
-
|
|
10433
|
-
|
|
10434
|
-
|
|
10435
|
-
}
|
|
10436
|
-
|
|
10437
|
-
|
|
10438
|
-
|
|
10439
|
-
|
|
10440
|
-
|
|
10441
|
-
|
|
10442
|
-
|
|
10443
|
-
|
|
10444
|
-
|
|
10445
|
-
|
|
10446
|
-
|
|
10447
|
-
|
|
10448
|
-
|
|
10449
|
-
|
|
10450
|
-
|
|
10451
|
-
|
|
10452
|
-
|
|
10453
|
-
|
|
10472
|
+
function realDeprovisionDeps(cfg, emit, crewHome, getStore) {
|
|
10473
|
+
const stateDir = cfg.ship.stateDir;
|
|
10474
|
+
const forgeHosts = () => Object.keys(cfg.ship.forges ?? {});
|
|
10475
|
+
const routeFor = (label) => cfg.routes.find((r) => routeSlug(r.route) === label);
|
|
10476
|
+
const sandboxes = () => dockerAvailable() ? listPassengerContainers().map((c) => c.name) : [];
|
|
10477
|
+
const unitPaths = () => {
|
|
10478
|
+
const host = hostPlatform();
|
|
10479
|
+
if (host === "windows") return [];
|
|
10480
|
+
const ctx = installContext();
|
|
10481
|
+
const systemd = detectSystemd();
|
|
10482
|
+
return ["run", "release", "passengers"].flatMap((job) => planInstall(cfg.ship, crewHome, host, systemd, job, ctx).unitPaths).filter((p) => existsSync23(p));
|
|
10483
|
+
};
|
|
10484
|
+
return {
|
|
10485
|
+
liveRuns: () => liveRuns(stateDir).map((r) => ({ role: r.role, route: r.route, pid: r.pid, ticketRecordId: r.ticketRecordId ?? void 0, streamPath: r.streamPath })),
|
|
10486
|
+
killRun: (run3) => killRunGroup(run3.pid),
|
|
10487
|
+
recordKill(run3) {
|
|
10488
|
+
emit.emit(`run_killed: ${run3.role} run on ${run3.route} stopped by crew deprovision`, {
|
|
10489
|
+
step: "agent",
|
|
10490
|
+
role: run3.role,
|
|
10491
|
+
data: { event: "run_killed", reason: "deprovision", pid: run3.pid }
|
|
10492
|
+
});
|
|
10493
|
+
},
|
|
10494
|
+
async handBack(run3) {
|
|
10495
|
+
const route2 = routeFor(run3.route);
|
|
10496
|
+
if (!run3.ticketRecordId || !route2) return;
|
|
10497
|
+
const memberId = route2.resolved?.seats[run3.role];
|
|
10498
|
+
if (!memberId) return;
|
|
10499
|
+
const identity = await resolveSeatKey(route2, run3.role, getStore);
|
|
10500
|
+
const tracker = new Tracker(route2, cfg.ship, { apiKey: identity.apiKey });
|
|
10501
|
+
const t = await tracker.ticket(run3.ticketRecordId);
|
|
10502
|
+
const target = repoTargetFor(route2, t.repo_id);
|
|
10503
|
+
const num = String(t[tracker.contract.columns.key] ?? "").replace(/^\D+/, "");
|
|
10504
|
+
const wt = target && num ? worktreeForNumber(target.dir, num) : null;
|
|
10505
|
+
let dirty = null;
|
|
10506
|
+
try {
|
|
10507
|
+
if (wt) dirty = status(wt.path).length > 0;
|
|
10508
|
+
} catch {
|
|
10509
|
+
}
|
|
10510
|
+
await tracker.postEvent(run3.ticketRecordId, interruptedComment({
|
|
10511
|
+
branch: wt?.branch ?? null,
|
|
10512
|
+
dirty,
|
|
10513
|
+
reason: "the host was released and wiped"
|
|
10514
|
+
}), memberId, "interrupted");
|
|
10515
|
+
},
|
|
10516
|
+
sandboxes,
|
|
10517
|
+
removeSandbox: (name) => {
|
|
10518
|
+
stopContainer(name);
|
|
10519
|
+
},
|
|
10520
|
+
async clearShipRows() {
|
|
10521
|
+
const out = [];
|
|
10522
|
+
for (const r of cfg.routes.filter((x) => x.resolved?.shipsModelId)) {
|
|
10523
|
+
try {
|
|
10524
|
+
const found = await new Tracker(r, cfg.ship).clearForRelease(cfg.ship.name);
|
|
10525
|
+
out.push({ route: r.route, ok: true, detail: found ? "cleared engaged, tunnel_status, relay_url" : "no Ships row for this ship" });
|
|
10526
|
+
} catch (e) {
|
|
10527
|
+
out.push({ route: r.route, ok: false, detail: e.message });
|
|
10528
|
+
}
|
|
10529
|
+
}
|
|
10530
|
+
return out;
|
|
10531
|
+
},
|
|
10532
|
+
forgeHosts,
|
|
10533
|
+
eraseForgeToken: (host) => eraseForgeToken(cfg.ship.forges, host, stateDir),
|
|
10534
|
+
labeled: labeledDocker,
|
|
10535
|
+
removeVolume: (name) => {
|
|
10536
|
+
docker(["volume", "rm", "-f", name]);
|
|
10537
|
+
},
|
|
10538
|
+
removeImage: (name) => {
|
|
10539
|
+
docker(["image", "rm", "-f", name]);
|
|
10540
|
+
},
|
|
10541
|
+
remaining() {
|
|
10542
|
+
const left = [];
|
|
10543
|
+
if (existsSync23(cfg.configFile)) left.push(`config ${cfg.configFile}`);
|
|
10544
|
+
for (const h of forgeHosts()) if (hasForgeToken(cfg.ship.forges, h, stateDir)) left.push(`forge token for ${h}`);
|
|
10545
|
+
try {
|
|
10546
|
+
for (const k of readdirSync11(keysDir(stateDir))) left.push(`seat key ${k}`);
|
|
10547
|
+
} catch {
|
|
10548
|
+
}
|
|
10549
|
+
for (const s of sandboxes()) left.push(`sandbox ${s}`);
|
|
10550
|
+
try {
|
|
10551
|
+
const { volumes, images } = labeledDocker();
|
|
10552
|
+
for (const v of volumes) left.push(`volume ${v}`);
|
|
10553
|
+
for (const i of images) left.push(`image ${i}`);
|
|
10554
|
+
} catch (e) {
|
|
10555
|
+
left.push(`docker could not be checked (${e.message})`);
|
|
10556
|
+
}
|
|
10557
|
+
for (const u of unitPaths()) left.push(`unit ${u}`);
|
|
10558
|
+
if (loadedCrewLabels().length) left.push(`loaded units ${loadedCrewLabels().join(", ")}`);
|
|
10559
|
+
return left;
|
|
10454
10560
|
}
|
|
10561
|
+
};
|
|
10562
|
+
}
|
|
10563
|
+
|
|
10564
|
+
// src/sandbox-cache.ts
|
|
10565
|
+
var execFileP = promisify(execFile);
|
|
10566
|
+
var realExec2 = (bin, args, o) => execFileP(bin, args, { env: o?.env ?? process.env, maxBuffer: 16 * 1024 * 1024 });
|
|
10567
|
+
var OPENSHELL_ATTACHABLE_LABEL = "openshell.ai/sandbox-attachable=true";
|
|
10568
|
+
var OPENSHELL_WORKSPACE_LABEL = "openshell.ai/sandbox-attachable-workspace";
|
|
10569
|
+
var VOLUME_ROOT = "/cache";
|
|
10570
|
+
var DEFAULT_CACHE_SETTINGS = {
|
|
10571
|
+
engine: "docker",
|
|
10572
|
+
workspace: "crew",
|
|
10573
|
+
helperImage: "alpine/git:latest",
|
|
10574
|
+
depsLockfile: "pnpm-lock.yaml"
|
|
10575
|
+
};
|
|
10576
|
+
function parseRemote(url) {
|
|
10577
|
+
const u = url.trim();
|
|
10578
|
+
const m = /^[a-z+]+:\/\/(?:[^@/]*@)?([^/:]+)(?::\d+)?\/(.+?)(?:\.git)?\/?$/i.exec(u) ?? /^(?:[\w.-]+@)?([\w.-]+):(?!\/\/)(.+?)(?:\.git)?\/?$/.exec(u);
|
|
10579
|
+
if (!m) return null;
|
|
10580
|
+
return { host: normalizeForgeHost(m[1]) || DEFAULT_FORGE_HOST, path: m[2].replace(/^\/+/, "") };
|
|
10581
|
+
}
|
|
10582
|
+
function mirrorRelPath(r) {
|
|
10583
|
+
return `mirrors/${r.host}/${r.path}.git`;
|
|
10584
|
+
}
|
|
10585
|
+
function depsRelDir(r) {
|
|
10586
|
+
return `cache/deps/${r.host}/${r.path}`;
|
|
10587
|
+
}
|
|
10588
|
+
var q = (s) => `'${s.replace(/'/g, `'\\''`)}'`;
|
|
10589
|
+
function cloneScript(o) {
|
|
10590
|
+
const mirror = `${o.mirrorDir}/${mirrorRelPath(o.repo)}`;
|
|
10591
|
+
const depsRoot = `${o.mirrorDir}/${depsRelDir(o.repo)}`;
|
|
10592
|
+
return [
|
|
10593
|
+
`git clone --quiet --shared ${q(mirror)} ${q(o.workDir)}`,
|
|
10594
|
+
`cd ${q(o.workDir)}`,
|
|
10595
|
+
`git remote set-url origin ${q(o.origin)}`,
|
|
10596
|
+
`git checkout --quiet -b ${q(o.branch)}`,
|
|
10597
|
+
// A missing lockfile or tree is not an error: the run installs as it would without a cache.
|
|
10598
|
+
`{ if [ -f ${q(o.lockfile)} ]; then h=$(sha256sum ${q(o.lockfile)} | cut -c1-16); d=${q(depsRoot)}/$h; [ -d "$d" ] && cp -R -P "$d"/. . ; fi; true; }`
|
|
10599
|
+
].join(" && ");
|
|
10600
|
+
}
|
|
10601
|
+
var REFRESH_SCRIPT = `
|
|
10602
|
+
set -e
|
|
10603
|
+
m=${VOLUME_ROOT}/"$1"; url="$2"
|
|
10604
|
+
export GIT_TERMINAL_PROMPT=0
|
|
10605
|
+
git config --global --add safe.directory '*'
|
|
10606
|
+
if [ -n "$CREW_FORGE_TOKEN" ]; then
|
|
10607
|
+
H="Authorization: Basic $(printf '%s:%s' "$CREW_FORGE_USER" "$CREW_FORGE_TOKEN" | base64 | tr -d '\\n')"
|
|
10608
|
+
git() { command git -c "http.extraHeader=$H" "$@"; }
|
|
10609
|
+
fi
|
|
10610
|
+
if [ -d "$m" ]; then
|
|
10611
|
+
git -C "$m" fetch --prune --atomic --quiet origin
|
|
10612
|
+
else
|
|
10613
|
+
mkdir -p "$(dirname "$m")"; rm -rf "$m.tmp"
|
|
10614
|
+
git clone --mirror --quiet "$url" "$m.tmp"
|
|
10615
|
+
git -C "$m.tmp" config gc.auto 0
|
|
10616
|
+
mv "$m.tmp" "$m"
|
|
10617
|
+
fi
|
|
10618
|
+
date -u +%Y-%m-%dT%H:%M:%SZ > "$m/crew-last-refresh"
|
|
10619
|
+
`;
|
|
10620
|
+
var DEPS_SCRIPT = `
|
|
10621
|
+
set -e
|
|
10622
|
+
m=${VOLUME_ROOT}/"$1"; droot=${VOLUME_ROOT}/"$2"; lock="$3"
|
|
10623
|
+
export GIT_TERMINAL_PROMPT=0
|
|
10624
|
+
git config --global --add safe.directory '*'
|
|
10625
|
+
git -C "$m" cat-file -e HEAD:"$lock" 2>/dev/null || exit 0
|
|
10626
|
+
h=$(git -C "$m" show HEAD:"$lock" | sha256sum | cut -c1-16)
|
|
10627
|
+
[ -d "$droot/$h" ] && exit 0
|
|
10628
|
+
w=$(mktemp -d); trap 'rm -rf "$w"' EXIT
|
|
10629
|
+
git clone --quiet --shared "$m" "$w/src"
|
|
10630
|
+
cd "$w/src"
|
|
10631
|
+
sh -c "$CREW_DEPS_COMMAND"
|
|
10632
|
+
mkdir -p "$droot/$h.tmp"
|
|
10633
|
+
find . -name node_modules -type d -prune | while read -r p; do
|
|
10634
|
+
mkdir -p "$droot/$h.tmp/$(dirname "$p")"; cp -R -P "$p" "$droot/$h.tmp/$p"
|
|
10635
|
+
done
|
|
10636
|
+
rm -rf "$droot/$h"; mv "$droot/$h.tmp" "$droot/$h"
|
|
10637
|
+
`;
|
|
10638
|
+
var SandboxCache = class {
|
|
10639
|
+
s;
|
|
10640
|
+
exec;
|
|
10641
|
+
constructor(s, exec = realExec2) {
|
|
10642
|
+
this.s = s;
|
|
10643
|
+
this.exec = exec;
|
|
10644
|
+
}
|
|
10645
|
+
engine(args, env) {
|
|
10646
|
+
return this.exec(this.s.engine, args, { env });
|
|
10647
|
+
}
|
|
10648
|
+
/** Creates the volume with the labels OpenShell and `crew deprovision` look for, if it is not there. */
|
|
10649
|
+
async ensureVolume() {
|
|
10455
10650
|
try {
|
|
10456
|
-
|
|
10457
|
-
|
|
10458
|
-
|
|
10459
|
-
const n = readSync(fd, chunk, 0, chunk.length, this.offset);
|
|
10460
|
-
if (n <= 0) break;
|
|
10461
|
-
this.offset += n;
|
|
10462
|
-
text2 += this.decoder.write(chunk.subarray(0, n));
|
|
10463
|
-
}
|
|
10464
|
-
return text2;
|
|
10465
|
-
} finally {
|
|
10466
|
-
closeSync3(fd);
|
|
10651
|
+
await this.engine(["volume", "inspect", this.s.volume]);
|
|
10652
|
+
return false;
|
|
10653
|
+
} catch {
|
|
10467
10654
|
}
|
|
10655
|
+
await this.engine([
|
|
10656
|
+
"volume",
|
|
10657
|
+
"create",
|
|
10658
|
+
"--label",
|
|
10659
|
+
OPENSHELL_ATTACHABLE_LABEL,
|
|
10660
|
+
"--label",
|
|
10661
|
+
`${OPENSHELL_WORKSPACE_LABEL}=${this.s.workspace}`,
|
|
10662
|
+
"--label",
|
|
10663
|
+
DOCKER_LABEL_OWNER,
|
|
10664
|
+
"--label",
|
|
10665
|
+
DOCKER_LABEL_CACHE,
|
|
10666
|
+
this.s.volume
|
|
10667
|
+
]);
|
|
10668
|
+
return true;
|
|
10669
|
+
}
|
|
10670
|
+
helper(image, script, args, env = {}) {
|
|
10671
|
+
const flags = Object.keys(env).flatMap((k) => ["-e", k]);
|
|
10672
|
+
return this.engine(["run", "--rm", "--label", DOCKER_LABEL_OWNER, "-v", `${this.s.volume}:${VOLUME_ROOT}`, ...flags, "--entrypoint", "sh", image, "-c", script, "sh", ...args], { ...process.env, ...env });
|
|
10468
10673
|
}
|
|
10469
|
-
};
|
|
10470
|
-
var TAIL_POLL_MS = 200;
|
|
10471
|
-
var RunObserver = class {
|
|
10472
|
-
cycles = [];
|
|
10473
|
-
attribution = new TicketAttributionTracker();
|
|
10474
|
-
/** Hand-off writes and their outcomes, to tell a run whose hand-off never landed (CREW-2363). */
|
|
10475
|
-
handoff = new HandoffTranscriptTracker();
|
|
10476
|
-
result;
|
|
10477
|
-
/** Prefix tokens of the first assistant call that carried usage; kept once. */
|
|
10478
|
-
prefixTokens;
|
|
10479
|
-
/** Set once the stream carries the harness's final result (turn count or cost). */
|
|
10480
|
-
sawResult = false;
|
|
10481
|
-
muted = false;
|
|
10482
10674
|
/**
|
|
10483
|
-
*
|
|
10484
|
-
*
|
|
10675
|
+
* Refreshes every mirror (and, where configured, its dependency tree). One
|
|
10676
|
+
* repository failing does not stop the others; the caller raises attention
|
|
10677
|
+
* for the failures and the runs keep using the previous mirror state.
|
|
10485
10678
|
*/
|
|
10486
|
-
|
|
10487
|
-
|
|
10488
|
-
|
|
10489
|
-
|
|
10490
|
-
|
|
10491
|
-
|
|
10492
|
-
|
|
10493
|
-
|
|
10494
|
-
|
|
10495
|
-
|
|
10496
|
-
|
|
10497
|
-
|
|
10498
|
-
|
|
10499
|
-
|
|
10500
|
-
|
|
10501
|
-
|
|
10502
|
-
|
|
10503
|
-
|
|
10504
|
-
|
|
10505
|
-
|
|
10506
|
-
|
|
10507
|
-
|
|
10679
|
+
async refresh(repos) {
|
|
10680
|
+
await this.ensureVolume();
|
|
10681
|
+
const out = [];
|
|
10682
|
+
for (const r of repos) {
|
|
10683
|
+
const env = r.token ? { CREW_FORGE_TOKEN: r.token, CREW_FORGE_USER: r.username ?? DEFAULT_FORGE_USERNAME } : {};
|
|
10684
|
+
try {
|
|
10685
|
+
await this.helper(this.s.helperImage, REFRESH_SCRIPT, [mirrorRelPath(r), forgeDescriptorFor(r.host).httpsUrl(r.host, r.path)], env);
|
|
10686
|
+
} catch (e) {
|
|
10687
|
+
out.push({ repo: r, ok: false, detail: describe(e) });
|
|
10688
|
+
continue;
|
|
10689
|
+
}
|
|
10690
|
+
if (this.s.depsCommand && this.s.depsImage) {
|
|
10691
|
+
try {
|
|
10692
|
+
await this.helper(this.s.depsImage, DEPS_SCRIPT, [mirrorRelPath(r), depsRelDir(r), this.s.depsLockfile], { CREW_DEPS_COMMAND: this.s.depsCommand });
|
|
10693
|
+
} catch (e) {
|
|
10694
|
+
out.push({ repo: r, ok: false, detail: `dependency tree: ${describe(e)}` });
|
|
10695
|
+
continue;
|
|
10696
|
+
}
|
|
10697
|
+
}
|
|
10698
|
+
out.push({ repo: r, ok: true });
|
|
10699
|
+
}
|
|
10700
|
+
return out;
|
|
10508
10701
|
}
|
|
10509
|
-
|
|
10510
|
-
|
|
10511
|
-
this.buf = "";
|
|
10702
|
+
/** Mirrors in the volume with their last refresh; empty when the volume does not exist. */
|
|
10703
|
+
async list() {
|
|
10512
10704
|
try {
|
|
10513
|
-
this.
|
|
10705
|
+
await this.engine(["volume", "inspect", this.s.volume]);
|
|
10514
10706
|
} catch {
|
|
10707
|
+
return [];
|
|
10708
|
+
}
|
|
10709
|
+
const script = `cd ${VOLUME_ROOT}/mirrors 2>/dev/null || exit 0; find . -name '*.git' -type d -prune | while read -r d; do printf '%s\\t%s\\n' "\${d#./}" "$(cat "$d/crew-last-refresh" 2>/dev/null)"; done`;
|
|
10710
|
+
const { stdout } = await this.helper(this.s.helperImage, script, []);
|
|
10711
|
+
const rows = [];
|
|
10712
|
+
for (const line of stdout.split("\n").filter(Boolean)) {
|
|
10713
|
+
const [rel, when] = line.split(" ");
|
|
10714
|
+
const m = /^([^/]+)\/(.+)\.git$/.exec(rel ?? "");
|
|
10715
|
+
if (m) rows.push({ repo: { host: m[1], path: m[2] }, lastRefresh: when?.trim() || null });
|
|
10515
10716
|
}
|
|
10717
|
+
return rows;
|
|
10516
10718
|
}
|
|
10517
|
-
|
|
10518
|
-
|
|
10519
|
-
|
|
10520
|
-
|
|
10521
|
-
|
|
10522
|
-
|
|
10523
|
-
return;
|
|
10719
|
+
/** Drops mirrors (and their dependency trees) for repositories not in `keep`. Returns what was dropped. */
|
|
10720
|
+
async prune(keep) {
|
|
10721
|
+
const have = new Set(keep.map((r) => `${r.host}/${r.path}`));
|
|
10722
|
+
const drop = (await this.list()).map((m) => m.repo).filter((r) => !have.has(`${r.host}/${r.path}`));
|
|
10723
|
+
for (const r of drop) {
|
|
10724
|
+
await this.helper(this.s.helperImage, `rm -rf ${VOLUME_ROOT}/${q(mirrorRelPath(r))} ${VOLUME_ROOT}/${q(depsRelDir(r))}`, []);
|
|
10524
10725
|
}
|
|
10726
|
+
return drop;
|
|
10727
|
+
}
|
|
10728
|
+
/** Recreates the volume empty; the next pass refreshes every mirror. */
|
|
10729
|
+
async reset() {
|
|
10525
10730
|
try {
|
|
10526
|
-
|
|
10527
|
-
|
|
10528
|
-
|
|
10529
|
-
if (got.numTurns !== void 0 || got.costUsd !== void 0) this.sawResult = true;
|
|
10530
|
-
}
|
|
10531
|
-
this.handoff.feed(parsed);
|
|
10532
|
-
this.consume(parsed, this.mapper.map(parsed));
|
|
10533
|
-
} catch {
|
|
10731
|
+
await this.engine(["volume", "rm", "--force", this.s.volume]);
|
|
10732
|
+
} catch (e) {
|
|
10733
|
+
throw new Error(`could not remove volume ${this.s.volume}: ${describe(e)}`);
|
|
10534
10734
|
}
|
|
10735
|
+
await this.ensureVolume();
|
|
10535
10736
|
}
|
|
10536
|
-
|
|
10537
|
-
|
|
10538
|
-
|
|
10539
|
-
|
|
10540
|
-
|
|
10541
|
-
|
|
10542
|
-
|
|
10543
|
-
|
|
10544
|
-
|
|
10545
|
-
|
|
10546
|
-
|
|
10547
|
-
|
|
10548
|
-
|
|
10549
|
-
|
|
10550
|
-
|
|
10551
|
-
|
|
10552
|
-
|
|
10553
|
-
|
|
10554
|
-
|
|
10555
|
-
|
|
10556
|
-
|
|
10557
|
-
|
|
10558
|
-
|
|
10559
|
-
|
|
10560
|
-
|
|
10561
|
-
|
|
10562
|
-
|
|
10563
|
-
|
|
10564
|
-
|
|
10565
|
-
|
|
10566
|
-
|
|
10567
|
-
|
|
10568
|
-
|
|
10569
|
-
|
|
10570
|
-
|
|
10571
|
-
level: "info",
|
|
10572
|
-
message: ev.kind === "tool" ? `${ev.tool ?? "tool"}${ev.target ? ` ${ev.target}` : ""}` : ev.kind,
|
|
10573
|
-
role: plan.role,
|
|
10574
|
-
...this.liveTicket ? { ticket: this.liveTicket } : {},
|
|
10575
|
-
data: { kind: ev.kind, ...ev.text ? { text: ev.text } : {}, ...ev.tool ? { tool: ev.tool } : {}, ...ev.target ? { target: ev.target } : {} }
|
|
10576
|
-
};
|
|
10577
|
-
if (!this.muted) this.eventsSink?.write(`${JSON.stringify(e)}
|
|
10578
|
-
`);
|
|
10579
|
-
if ((ev.kind === "thought" || ev.kind === "text") && ev.text) {
|
|
10580
|
-
turnText += `${ev.text}
|
|
10581
|
-
`;
|
|
10582
|
-
const cycleIndex = this.cycles.length;
|
|
10583
|
-
this.cycles.push({
|
|
10584
|
-
cycleIndex,
|
|
10585
|
-
occurredAt: e.at,
|
|
10586
|
-
thinking: ev.text,
|
|
10587
|
-
// A conversational `text` turn is tagged 'assistant' so it's
|
|
10588
|
-
// distinguishable from a thinking block once both share this
|
|
10589
|
-
// buffer (ISSUE-594) — matching the precedent
|
|
10590
|
-
// `agent-chat.service.ts`'s logChatRun (ISSUE-504) already set
|
|
10591
|
-
// for the same column. A `thought` cycle stays untagged, same
|
|
10592
|
-
// as before this change.
|
|
10593
|
-
...ev.kind === "text" ? { role: "assistant" } : {}
|
|
10594
|
-
});
|
|
10595
|
-
if (!this.muted) {
|
|
10596
|
-
this.eventsSink?.write(`${JSON.stringify({
|
|
10597
|
-
...e,
|
|
10598
|
-
message: "cycle",
|
|
10599
|
-
data: { kind: "cycle", cycleIndex, text: ev.text }
|
|
10600
|
-
})}
|
|
10601
|
-
`);
|
|
10602
|
-
}
|
|
10737
|
+
};
|
|
10738
|
+
function describe(e) {
|
|
10739
|
+
const err3 = e;
|
|
10740
|
+
return (err3.stderr?.trim() || err3.message || String(e)).split("\n").slice(-3).join(" / ");
|
|
10741
|
+
}
|
|
10742
|
+
function cacheSettingsOf(cfg) {
|
|
10743
|
+
if (!cfg?.cacheVolume) return null;
|
|
10744
|
+
return {
|
|
10745
|
+
engine: cfg.engine ?? DEFAULT_CACHE_SETTINGS.engine,
|
|
10746
|
+
volume: cfg.cacheVolume,
|
|
10747
|
+
workspace: cfg.cacheWorkspace ?? DEFAULT_CACHE_SETTINGS.workspace,
|
|
10748
|
+
helperImage: cfg.cacheImage ?? DEFAULT_CACHE_SETTINGS.helperImage,
|
|
10749
|
+
depsImage: cfg.depsImage ?? cfg.image,
|
|
10750
|
+
depsCommand: cfg.depsCommand,
|
|
10751
|
+
depsLockfile: cfg.depsLockfile ?? DEFAULT_CACHE_SETTINGS.depsLockfile
|
|
10752
|
+
};
|
|
10753
|
+
}
|
|
10754
|
+
var sandboxCacheKey = (volume, r) => `sandbox_cache:${volume}:${r.host}/${r.path}`;
|
|
10755
|
+
var CACHE_REFRESH_INTERVAL_MS = 6e4;
|
|
10756
|
+
function makeCachePass(o) {
|
|
10757
|
+
const now = o.now ?? Date.now;
|
|
10758
|
+
let last = -Infinity;
|
|
10759
|
+
return async () => {
|
|
10760
|
+
if (now() - last < CACHE_REFRESH_INTERVAL_MS) return [];
|
|
10761
|
+
last = now();
|
|
10762
|
+
const all = [];
|
|
10763
|
+
for (const r of o.repos()) {
|
|
10764
|
+
let release;
|
|
10765
|
+
try {
|
|
10766
|
+
release = o.lock(r.dir);
|
|
10767
|
+
all.push(...await o.cache.refresh([r]));
|
|
10768
|
+
} catch (e) {
|
|
10769
|
+
all.push({ repo: r, ok: false, detail: describe(e) });
|
|
10770
|
+
} finally {
|
|
10771
|
+
release?.();
|
|
10603
10772
|
}
|
|
10604
10773
|
}
|
|
10605
|
-
|
|
10606
|
-
|
|
10607
|
-
|
|
10608
|
-
|
|
10774
|
+
for (const out of all) {
|
|
10775
|
+
const key = sandboxCacheKey(o.volume, out.repo);
|
|
10776
|
+
if (out.ok) o.ok(key);
|
|
10777
|
+
else await o.fail(key, `sandbox cache: ${out.repo.host}/${out.repo.path} did not refresh (${out.detail}); runs use the previous mirror. Resumes automatically when a later pass refreshes it`);
|
|
10778
|
+
}
|
|
10779
|
+
return all;
|
|
10780
|
+
};
|
|
10781
|
+
}
|
|
10782
|
+
|
|
10783
|
+
// src/runner.ts
|
|
10784
|
+
var execFileP2 = promisify2(execFile2);
|
|
10785
|
+
var DEFAULT_SANDBOX_CONFIG = {
|
|
10786
|
+
bin: "openshell",
|
|
10787
|
+
mirrorDir: "/cache",
|
|
10788
|
+
workDir: "/sandbox/work"
|
|
10609
10789
|
};
|
|
10610
|
-
|
|
10611
|
-
|
|
10612
|
-
|
|
10613
|
-
|
|
10614
|
-
|
|
10615
|
-
|
|
10616
|
-
|
|
10617
|
-
|
|
10618
|
-
|
|
10619
|
-
|
|
10620
|
-
|
|
10621
|
-
|
|
10622
|
-
exitCode: null,
|
|
10623
|
-
running: true,
|
|
10624
|
-
...extra
|
|
10625
|
-
}));
|
|
10626
|
-
} catch {
|
|
10790
|
+
var SANDBOX_STDIN_LIMIT = 4 * 1024 * 1024 - 64 * 1024;
|
|
10791
|
+
var SANDBOX_PROMPT_PATH = "/sandbox/prompt.txt";
|
|
10792
|
+
var HostRunner = class {
|
|
10793
|
+
kind = "host";
|
|
10794
|
+
start(plan, o) {
|
|
10795
|
+
const child = spawn3(plan.bin, plan.args, {
|
|
10796
|
+
cwd: plan.cwd,
|
|
10797
|
+
env: o.env,
|
|
10798
|
+
stdio: ["pipe", o.streamFd, "inherit"],
|
|
10799
|
+
detached: true
|
|
10800
|
+
});
|
|
10801
|
+
return { child, handle: { runner: "host", pid: child.pid } };
|
|
10627
10802
|
}
|
|
10628
|
-
|
|
10629
|
-
|
|
10630
|
-
let moved = false;
|
|
10631
|
-
if (hintMoved) {
|
|
10632
|
-
try {
|
|
10633
|
-
moved = await hintMoved();
|
|
10634
|
-
} catch {
|
|
10635
|
-
}
|
|
10803
|
+
async kill(handle) {
|
|
10804
|
+
if (handle.pid !== void 0) await killRunGroup(handle.pid);
|
|
10636
10805
|
}
|
|
10637
|
-
|
|
10638
|
-
|
|
10639
|
-
const { result } = obs;
|
|
10640
|
-
const pending = end.interrupted ? null : obs.handoff.pending();
|
|
10641
|
-
if (pending) {
|
|
10806
|
+
isAlive(handle) {
|
|
10807
|
+
if (handle.pid === void 0) return false;
|
|
10642
10808
|
try {
|
|
10643
|
-
|
|
10644
|
-
|
|
10645
|
-
...pending,
|
|
10646
|
-
version: 1,
|
|
10647
|
-
route: id.route,
|
|
10648
|
-
role: id.role,
|
|
10649
|
-
...attributedTicket ? { ticket: attributedTicket } : {},
|
|
10650
|
-
streamPath: id.streamPath,
|
|
10651
|
-
createdAt: now.toISOString(),
|
|
10652
|
-
attempts: 0,
|
|
10653
|
-
nextAttemptAt: now.toISOString()
|
|
10654
|
-
});
|
|
10809
|
+
process.kill(handle.pid, 0);
|
|
10810
|
+
return true;
|
|
10655
10811
|
} catch (e) {
|
|
10656
|
-
|
|
10812
|
+
return e.code === "EPERM";
|
|
10657
10813
|
}
|
|
10658
10814
|
}
|
|
10659
|
-
|
|
10660
|
-
|
|
10661
|
-
attributedTicket: attributedTicket ?? null,
|
|
10662
|
-
attributedBy,
|
|
10663
|
-
running: void 0,
|
|
10664
|
-
exitCode: end.code,
|
|
10665
|
-
...end.interrupted ? { interrupted: end.interrupted } : {},
|
|
10666
|
-
...pending ? { handoff: "pending" } : {},
|
|
10667
|
-
sessionId: result?.sessionId ?? null,
|
|
10668
|
-
numTurns: result?.numTurns ?? null,
|
|
10669
|
-
prefixTokens: obs.prefixTokens ?? null,
|
|
10670
|
-
totalCostUsd: result?.costUsd ?? null
|
|
10671
|
-
});
|
|
10672
|
-
} catch {
|
|
10815
|
+
reattach(meta) {
|
|
10816
|
+
return { runner: "host", pid: meta.pid };
|
|
10673
10817
|
}
|
|
10674
|
-
|
|
10675
|
-
|
|
10676
|
-
|
|
10677
|
-
|
|
10678
|
-
|
|
10679
|
-
|
|
10680
|
-
|
|
10681
|
-
|
|
10682
|
-
|
|
10683
|
-
|
|
10684
|
-
|
|
10685
|
-
|
|
10686
|
-
|
|
10818
|
+
async release() {
|
|
10819
|
+
}
|
|
10820
|
+
};
|
|
10821
|
+
function shQuote2(s) {
|
|
10822
|
+
return `'${s.replace(/'/g, `'\\''`)}'`;
|
|
10823
|
+
}
|
|
10824
|
+
function sandboxName(plan, now = Date.now()) {
|
|
10825
|
+
const base = [plan.route, plan.role, plan.ticket ?? ""].join("-").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
10826
|
+
return `crew-${base.slice(0, 36)}-${now.toString(36)}`.replace(/-+/g, "-");
|
|
10827
|
+
}
|
|
10828
|
+
var SandboxRunner = class {
|
|
10829
|
+
kind = "sandbox";
|
|
10830
|
+
cfg;
|
|
10831
|
+
/** Injected for tests; the real one reads the plan's checkout. */
|
|
10832
|
+
originOf;
|
|
10833
|
+
constructor(cfg = {}, originOf = gitOrigin) {
|
|
10834
|
+
this.cfg = { ...DEFAULT_SANDBOX_CONFIG, ...cfg };
|
|
10835
|
+
this.originOf = originOf;
|
|
10836
|
+
}
|
|
10837
|
+
os(args, o = {}) {
|
|
10838
|
+
return execFileP2(this.cfg.bin, args, { env: o.env ?? process.env, maxBuffer: 16 * 1024 * 1024 });
|
|
10839
|
+
}
|
|
10840
|
+
async start(plan, o) {
|
|
10841
|
+
const origin = this.originOf(plan.cwd);
|
|
10842
|
+
if (!origin) throw new Error(`cannot find the origin remote of ${plan.cwd}, so there is nothing to clone into the sandbox`);
|
|
10843
|
+
const repo = parseRemote(origin);
|
|
10844
|
+
if (!repo) throw new Error(`cannot read a forge host and path from the origin remote ${origin}`);
|
|
10845
|
+
const name = sandboxName(plan);
|
|
10846
|
+
const { cfg } = this;
|
|
10847
|
+
const secretKeys = Object.keys(plan.setEnv);
|
|
10848
|
+
if (cfg.forgeTokenVar && o.env[cfg.forgeTokenVar] !== void 0 && !secretKeys.includes(cfg.forgeTokenVar)) secretKeys.push(cfg.forgeTokenVar);
|
|
10849
|
+
const createEnv = { ...process.env, ...plan.setEnv };
|
|
10850
|
+
if (cfg.forgeTokenVar && o.env[cfg.forgeTokenVar] !== void 0) createEnv[cfg.forgeTokenVar] = o.env[cfg.forgeTokenVar];
|
|
10851
|
+
const create2 = ["sandbox", "create", "--name", name, "--detach"];
|
|
10852
|
+
if (cfg.policy) create2.push("--policy", cfg.policy);
|
|
10853
|
+
if (cfg.image) create2.push("--from", cfg.image);
|
|
10854
|
+
if (cfg.cacheVolume) {
|
|
10855
|
+
create2.push("--driver-config-json", JSON.stringify({
|
|
10856
|
+
docker: { mounts: [{ type: "volume", source: cfg.cacheVolume, target: cfg.mirrorDir, read_only: true }] }
|
|
10857
|
+
}));
|
|
10687
10858
|
}
|
|
10688
|
-
|
|
10689
|
-
|
|
10690
|
-
|
|
10691
|
-
|
|
10692
|
-
outcome: end.interrupted ? "interrupted" : end.code === 0 || end.code === null && obs.sawResult ? "success" : "error",
|
|
10693
|
-
startedAt,
|
|
10694
|
-
finishedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
10695
|
-
cycles: obs.cycles,
|
|
10696
|
-
model: id.model,
|
|
10697
|
-
tokensIn: result?.inputTokens,
|
|
10698
|
-
tokensOut: result?.outputTokens,
|
|
10699
|
-
cacheReadTokens: result?.cacheReadTokens,
|
|
10700
|
-
cacheWriteTokens: result?.cacheWriteTokens,
|
|
10701
|
-
costUsd: result?.costUsd,
|
|
10702
|
-
// Both come from a finished run: without the result line (interrupted) neither is trustworthy.
|
|
10703
|
-
...result?.numTurns !== void 0 ? { numTurns: result.numTurns } : {},
|
|
10704
|
-
...result?.numTurns !== void 0 && obs.prefixTokens !== void 0 ? { prefixTokens: obs.prefixTokens } : {},
|
|
10705
|
-
harness: identity.harness,
|
|
10706
|
-
provider: identity.provider,
|
|
10707
|
-
promptVersion: id.promptVersion,
|
|
10708
|
-
promptSha: id.promptSha
|
|
10709
|
-
};
|
|
10859
|
+
for (const k of secretKeys) create2.push("--env-from", k);
|
|
10860
|
+
const maxSeconds = Math.max(60, Math.ceil((plan.maxRunMinutes ?? 0) * 60) + 600);
|
|
10861
|
+
create2.push("--", "sleep", String(plan.maxRunMinutes ? maxSeconds : 86400));
|
|
10862
|
+
const t0 = Date.now();
|
|
10710
10863
|
try {
|
|
10711
|
-
|
|
10712
|
-
try {
|
|
10713
|
-
clearShipAttention(new State(stateDirOfStream(id.streamPath)), agentLogRefusedKey(id.route));
|
|
10714
|
-
} catch {
|
|
10715
|
-
}
|
|
10716
|
-
if (sent.counterWarning) emit.warn(sent.counterWarning, { step: "agent" });
|
|
10717
|
-
if (sent.counterRetry) {
|
|
10718
|
-
try {
|
|
10719
|
-
queueTicketCounters(stateDirOfStream(id.streamPath), basename8(id.streamPath, ".jsonl"), {
|
|
10720
|
-
version: 1,
|
|
10721
|
-
route: id.route,
|
|
10722
|
-
role: id.role,
|
|
10723
|
-
target: id.agentLog,
|
|
10724
|
-
write: sent.counterRetry,
|
|
10725
|
-
attempts: 0,
|
|
10726
|
-
nextAttemptAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
10727
|
-
});
|
|
10728
|
-
} catch {
|
|
10729
|
-
}
|
|
10730
|
-
}
|
|
10864
|
+
await this.os(create2, { env: createEnv });
|
|
10731
10865
|
} catch (e) {
|
|
10732
|
-
|
|
10733
|
-
|
|
10734
|
-
|
|
10735
|
-
|
|
10736
|
-
|
|
10737
|
-
|
|
10738
|
-
|
|
10739
|
-
|
|
10740
|
-
|
|
10741
|
-
|
|
10742
|
-
|
|
10743
|
-
|
|
10744
|
-
|
|
10745
|
-
|
|
10746
|
-
|
|
10747
|
-
|
|
10748
|
-
|
|
10866
|
+
await this.deleteQuietly(name);
|
|
10867
|
+
throw new Error(`openshell could not create the sandbox: ${describeExecError(e)}`);
|
|
10868
|
+
}
|
|
10869
|
+
o.emit.emit(`sandbox_created: ${name}`, { role: plan.role, data: { event: "sandbox_created", sandbox: name, ms: Date.now() - t0 } });
|
|
10870
|
+
const handle = { runner: "sandbox", sandbox: name, sandboxBin: cfg.bin };
|
|
10871
|
+
try {
|
|
10872
|
+
const branch = `crew-run-${name.slice(-8)}`;
|
|
10873
|
+
const clone = cloneScript({
|
|
10874
|
+
mirrorDir: cfg.mirrorDir,
|
|
10875
|
+
repo,
|
|
10876
|
+
workDir: cfg.workDir,
|
|
10877
|
+
origin,
|
|
10878
|
+
branch,
|
|
10879
|
+
lockfile: cfg.depsLockfile ?? DEFAULT_CACHE_SETTINGS.depsLockfile
|
|
10880
|
+
});
|
|
10881
|
+
await this.os(["sandbox", "exec", "-n", name, "--no-login-shell", "--", "sh", "-c", clone]);
|
|
10882
|
+
const harness = cfg.agentBin ?? basename7(plan.bin);
|
|
10883
|
+
const viaUpload = Buffer.byteLength(plan.prompt) > SANDBOX_STDIN_LIMIT;
|
|
10884
|
+
if (viaUpload) {
|
|
10885
|
+
const tmp = mkdtempSync(join25(tmpdir3(), "crew-prompt-"));
|
|
10749
10886
|
try {
|
|
10750
|
-
|
|
10751
|
-
|
|
10752
|
-
|
|
10753
|
-
|
|
10754
|
-
|
|
10887
|
+
const file = join25(tmp, "prompt.txt");
|
|
10888
|
+
writeFileSync14(file, plan.prompt, { mode: 384 });
|
|
10889
|
+
await this.os(["sandbox", "upload", name, file, SANDBOX_PROMPT_PATH]);
|
|
10890
|
+
} finally {
|
|
10891
|
+
rmSync13(tmp, { recursive: true, force: true });
|
|
10755
10892
|
}
|
|
10756
10893
|
}
|
|
10757
|
-
|
|
10894
|
+
const command2 = viaUpload ? ["sh", "-c", `exec ${[harness, ...plan.args].map(shQuote2).join(" ")} < ${SANDBOX_PROMPT_PATH}`] : [harness, ...plan.args];
|
|
10895
|
+
const child = spawn3(cfg.bin, ["sandbox", "exec", "-n", name, "--no-login-shell", "--workdir", cfg.workDir, "--", ...command2], {
|
|
10896
|
+
env: process.env,
|
|
10897
|
+
stdio: ["pipe", o.streamFd, "inherit"],
|
|
10898
|
+
detached: true
|
|
10899
|
+
});
|
|
10900
|
+
return { child, handle: { ...handle, pid: child.pid }, ...viaUpload ? { promptDelivered: true } : {} };
|
|
10901
|
+
} catch (e) {
|
|
10902
|
+
await this.release(handle, o.emit);
|
|
10903
|
+
throw new Error(`could not prepare the sandbox ${name}: ${describeExecError(e)}`);
|
|
10758
10904
|
}
|
|
10759
10905
|
}
|
|
10760
|
-
|
|
10761
|
-
|
|
10762
|
-
|
|
10763
|
-
const adapter = adapterFor(plan.harness ?? LEGACY_TOOLCHAIN_HARNESS);
|
|
10764
|
-
const runner = runnerFor(plan.runner, plan.sandboxConfig);
|
|
10765
|
-
const started = Date.now();
|
|
10766
|
-
const startedAt = new Date(started).toISOString();
|
|
10767
|
-
for (const w of plan.warnings ?? []) emit.warn(w, { step: "agent", role: plan.role });
|
|
10768
|
-
const eventsSink = openSink(plan.eventsPath);
|
|
10769
|
-
const id = identityOf(plan, adapter.harness);
|
|
10770
|
-
let streamFd;
|
|
10771
|
-
try {
|
|
10772
|
-
mkdirSync13(dirname8(plan.streamPath), { recursive: true });
|
|
10773
|
-
streamFd = openSync3(plan.streamPath, "a");
|
|
10774
|
-
} catch (e) {
|
|
10775
|
-
void endSink(eventsSink);
|
|
10776
|
-
return Promise.reject(new AgentSpawnError(`cannot open the stream file ${plan.streamPath}: ${e.message}`));
|
|
10906
|
+
async kill(handle) {
|
|
10907
|
+
if (handle.sandbox) await this.deleteQuietly(handle.sandbox, handle.sandboxBin);
|
|
10908
|
+
if (handle.pid !== void 0) await killRunGroup(handle.pid, { graceMs: 2e3 });
|
|
10777
10909
|
}
|
|
10778
|
-
|
|
10779
|
-
|
|
10780
|
-
return Promise.reject(new AgentSpawnError(`cannot run ${plan.bin}: ${e.message}`));
|
|
10781
|
-
};
|
|
10782
|
-
let begun;
|
|
10783
|
-
try {
|
|
10784
|
-
begun = runner.start(plan, { env: { ...scrubbedEnv(process.env, adapter.harness), ...plan.setEnv }, streamFd, emit });
|
|
10785
|
-
} catch (e) {
|
|
10786
|
-
closeSync3(streamFd);
|
|
10787
|
-
return failed(e);
|
|
10910
|
+
isAlive(handle) {
|
|
10911
|
+
return new HostRunner().isAlive(handle);
|
|
10788
10912
|
}
|
|
10789
|
-
|
|
10790
|
-
return
|
|
10791
|
-
(run3) => {
|
|
10792
|
-
closeSync3(streamFd);
|
|
10793
|
-
return supervise(run3);
|
|
10794
|
-
},
|
|
10795
|
-
(e) => {
|
|
10796
|
-
closeSync3(streamFd);
|
|
10797
|
-
return failed(e);
|
|
10798
|
-
}
|
|
10799
|
-
);
|
|
10913
|
+
reattach(meta) {
|
|
10914
|
+
return { runner: "sandbox", pid: meta.pid, sandbox: meta.sandbox, sandboxBin: meta.sandboxBin };
|
|
10800
10915
|
}
|
|
10801
|
-
|
|
10802
|
-
|
|
10803
|
-
|
|
10804
|
-
|
|
10805
|
-
|
|
10806
|
-
const deadline = deadlineFor(spawnedAt, plan.maxRunMinutes);
|
|
10807
|
-
if (child.pid !== void 0) {
|
|
10808
|
-
const sidecar = {
|
|
10809
|
-
pid: child.pid,
|
|
10810
|
-
ownerPid: process.pid,
|
|
10811
|
-
spawnedAt,
|
|
10812
|
-
role: plan.role,
|
|
10813
|
-
route: plan.route,
|
|
10814
|
-
...deadline ? { deadline, maxRunMinutes: plan.maxRunMinutes } : {},
|
|
10815
|
-
...handle.runner === "sandbox" ? { runner: "sandbox", sandbox: handle.sandbox, sandboxBin: handle.sandboxBin } : {},
|
|
10816
|
-
ticket: plan.ticket ?? null,
|
|
10817
|
-
ticketRecordId: plan.ticketRecordId ?? null,
|
|
10818
|
-
repo: plan.repo ?? null,
|
|
10819
|
-
routeName: plan.routeName,
|
|
10820
|
-
model: plan.model,
|
|
10821
|
-
toolchain: id.toolchain,
|
|
10822
|
-
harness: id.harness,
|
|
10823
|
-
provider: id.provider,
|
|
10824
|
-
eventsPath: plan.eventsPath,
|
|
10825
|
-
agentLog: id.agentLog,
|
|
10826
|
-
promptVersion: plan.promptVersion,
|
|
10827
|
-
promptSha: plan.promptSha
|
|
10828
|
-
};
|
|
10829
|
-
try {
|
|
10830
|
-
writeFileSync15(pidPathFor(plan.streamPath), JSON.stringify(sidecar));
|
|
10831
|
-
} catch {
|
|
10832
|
-
}
|
|
10833
|
-
}
|
|
10834
|
-
const obs = new RunObserver(id, adapter, eventsSink, emit.cycle);
|
|
10835
|
-
const meta = { pid: child.pid ?? null, ...deadline ? { deadline } : {} };
|
|
10836
|
-
obs.onClaim = () => writeRunMeta(id, startedAt, obs.liveTicket, meta);
|
|
10837
|
-
if (child.pid !== void 0) writeRunMeta(id, startedAt, obs.liveTicket, meta);
|
|
10838
|
-
const tail3 = new FileTail(plan.streamPath);
|
|
10839
|
-
const timer = setInterval(() => obs.feed(tail3.poll()), TAIL_POLL_MS);
|
|
10840
|
-
let killed;
|
|
10841
|
-
const limitTimer = child.pid !== void 0 && deadline ? setTimeout(() => {
|
|
10842
|
-
void killForDeadline(() => runner.kill(handle, WALL_CLOCK_REASON), child.pid, emit, plan.role, obs.liveTicket ?? plan.ticket ?? null, spawnedAt, plan.maxRunMinutes).then((k) => {
|
|
10843
|
-
killed = k;
|
|
10844
|
-
});
|
|
10845
|
-
}, Math.max(0, msUntilDeadline(deadline) ?? 0)) : void 0;
|
|
10846
|
-
let spawnFailed = false;
|
|
10847
|
-
child.on("error", (err3) => {
|
|
10848
|
-
spawnFailed = true;
|
|
10849
|
-
clearInterval(timer);
|
|
10850
|
-
clearTimeout(limitTimer);
|
|
10851
|
-
void runner.release(handle, emit);
|
|
10852
|
-
reject(new AgentSpawnError(`cannot run ${plan.bin}: ${err3.message}`));
|
|
10853
|
-
});
|
|
10854
|
-
child.on("close", async (code) => {
|
|
10855
|
-
clearInterval(timer);
|
|
10856
|
-
clearTimeout(limitTimer);
|
|
10857
|
-
obs.feed(tail3.poll());
|
|
10858
|
-
obs.end();
|
|
10859
|
-
await endSink(eventsSink);
|
|
10860
|
-
if (spawnFailed) return;
|
|
10861
|
-
await runner.release(handle, emit);
|
|
10862
|
-
if (limitTimer && !killed && !obs.sawResult && msUntilDeadline(deadline) <= 0) {
|
|
10863
|
-
killed = { limitMinutes: plan.maxRunMinutes ?? 0, elapsedMs: Date.now() - Date.parse(spawnedAt) };
|
|
10864
|
-
}
|
|
10865
|
-
const { ms: ms3, handoffPending } = await finishRun(
|
|
10866
|
-
id,
|
|
10867
|
-
obs,
|
|
10868
|
-
emit,
|
|
10869
|
-
startedAt,
|
|
10870
|
-
{ code: code ?? 1, ...killed ? { interrupted: WALL_CLOCK_REASON } : {} },
|
|
10871
|
-
plan.agentLog?.apiKey,
|
|
10872
|
-
hintMoved
|
|
10873
|
-
);
|
|
10874
|
-
resolve7({ code: code ?? 1, ms: ms3, ...handoffPending ? { handoffPending } : {}, ...killed ? { killed } : {} });
|
|
10875
|
-
});
|
|
10876
|
-
child.stdin?.on("error", (err3) => {
|
|
10877
|
-
emit.warn(`agent closed stdin before the prompt was delivered: ${err3.message}`, { step: "agent" });
|
|
10878
|
-
});
|
|
10879
|
-
if (promptDelivered) child.stdin?.end();
|
|
10880
|
-
else child.stdin?.end(plan.prompt);
|
|
10881
|
-
});
|
|
10916
|
+
async release(handle, emit) {
|
|
10917
|
+
if (!handle.sandbox) return;
|
|
10918
|
+
const t0 = Date.now();
|
|
10919
|
+
await this.deleteQuietly(handle.sandbox, handle.sandboxBin);
|
|
10920
|
+
emit.emit(`sandbox_deleted: ${handle.sandbox}`, { data: { event: "sandbox_deleted", sandbox: handle.sandbox, ms: Date.now() - t0 } });
|
|
10882
10921
|
}
|
|
10883
|
-
|
|
10884
|
-
|
|
10885
|
-
|
|
10886
|
-
|
|
10887
|
-
const id = { ...sc, streamPath };
|
|
10888
|
-
const eventsSink = openSink(sc.eventsPath);
|
|
10889
|
-
const obs = new RunObserver(id, adapter, eventsSink, emit.cycle);
|
|
10890
|
-
const tail3 = new FileTail(streamPath);
|
|
10891
|
-
obs.muted = true;
|
|
10892
|
-
obs.feed(tail3.poll());
|
|
10893
|
-
obs.muted = false;
|
|
10894
|
-
const runner = runnerOfSidecar(sc);
|
|
10895
|
-
const adopted = isAlive(sc);
|
|
10896
|
-
let killed;
|
|
10897
|
-
if (adopted) {
|
|
10898
|
-
o.onAdopted?.();
|
|
10899
|
-
const pollMs = o.pollMs ?? TAIL_POLL_MS;
|
|
10900
|
-
while (isAlive(sc)) {
|
|
10901
|
-
obs.feed(tail3.poll());
|
|
10902
|
-
if (!killed && (msUntilDeadline(sc.deadline) ?? 1) <= 0) {
|
|
10903
|
-
killed = await killForDeadline(() => runner.kill(runner.reattach(sc), WALL_CLOCK_REASON), sc.pid, emit, sc.role, obs.liveTicket ?? sc.ticket, sc.spawnedAt, sc.maxRunMinutes);
|
|
10904
|
-
continue;
|
|
10905
|
-
}
|
|
10906
|
-
await new Promise((r) => setTimeout(r, pollMs));
|
|
10922
|
+
async deleteQuietly(name, bin = this.cfg.bin) {
|
|
10923
|
+
try {
|
|
10924
|
+
await execFileP2(bin, ["sandbox", "delete", name], { timeout: 6e4 });
|
|
10925
|
+
} catch {
|
|
10907
10926
|
}
|
|
10908
10927
|
}
|
|
10909
|
-
|
|
10910
|
-
|
|
10911
|
-
|
|
10912
|
-
|
|
10913
|
-
const interrupted = killed ? WALL_CLOCK_REASON : obs.sawResult ? void 0 : adopted ? "no-result" : "daemon-exit";
|
|
10914
|
-
const { ms: ms3, ticket } = await finishRun(
|
|
10915
|
-
id,
|
|
10916
|
-
obs,
|
|
10917
|
-
emit,
|
|
10918
|
-
sc.spawnedAt,
|
|
10919
|
-
{ code: null, ...interrupted ? { interrupted } : {} },
|
|
10920
|
-
o.agentLogKey
|
|
10921
|
-
);
|
|
10922
|
-
return { adopted, ...interrupted ? { interrupted } : {}, ...killed ? { killed } : {}, ms: ms3, ticket };
|
|
10923
|
-
}
|
|
10924
|
-
|
|
10925
|
-
// src/seat-keys.ts
|
|
10926
|
-
import { hostFromUrl as hostFromUrl2 } from "@tablation/client";
|
|
10927
|
-
var SEAT_KEY_ROLES = ROLE_NAMES.filter((r) => r !== "pair");
|
|
10928
|
-
function keyIsAdmin(route2) {
|
|
10929
|
-
const role = route2.resolved?.role;
|
|
10930
|
-
return role === void 0 || role === "WORKSPACE_ADMIN" || role === "PLATFORM_ADMIN";
|
|
10928
|
+
};
|
|
10929
|
+
function describeExecError(e) {
|
|
10930
|
+
const err3 = e;
|
|
10931
|
+
return (err3.stderr?.trim() || err3.message || String(e)).split("\n").slice(-3).join(" / ");
|
|
10931
10932
|
}
|
|
10932
|
-
|
|
10933
|
-
|
|
10934
|
-
|
|
10935
|
-
|
|
10936
|
-
|
|
10933
|
+
function gitOrigin(cwd) {
|
|
10934
|
+
try {
|
|
10935
|
+
return execFileSync6("git", ["-C", cwd, "remote", "get-url", "origin"], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
|
|
10936
|
+
} catch {
|
|
10937
|
+
return null;
|
|
10938
|
+
}
|
|
10937
10939
|
}
|
|
10938
|
-
function
|
|
10939
|
-
|
|
10940
|
-
return seatKeySlug(workspaceSlug, agentId);
|
|
10940
|
+
function runnerFor(kind, sandbox) {
|
|
10941
|
+
return kind === "sandbox" ? new SandboxRunner(sandbox) : new HostRunner();
|
|
10941
10942
|
}
|
|
10942
|
-
function
|
|
10943
|
-
return
|
|
10943
|
+
function runnerOfSidecar(sc) {
|
|
10944
|
+
return sc.runner === "sandbox" ? new SandboxRunner(sc.sandboxBin ? { bin: sc.sandboxBin } : {}) : new HostRunner();
|
|
10944
10945
|
}
|
|
10945
|
-
|
|
10946
|
-
const
|
|
10947
|
-
|
|
10948
|
-
if (!agentId) return ship(keyIsAdmin(route2) ? "release seat has no linked Agents row (run `crew agents sync`)" : `release seat ${MEMBER_NO_ROW}`);
|
|
10946
|
+
function checkSandboxRunner(cfg = {}, run3 = (b, a) => execFileSync6(b, a, { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], timeout: 15e3 })) {
|
|
10947
|
+
const bin = cfg.bin ?? DEFAULT_SANDBOX_CONFIG.bin;
|
|
10948
|
+
let version;
|
|
10949
10949
|
try {
|
|
10950
|
-
|
|
10951
|
-
if (session?.apiKey) return { kind: "agent", apiKey: session.apiKey, agentId };
|
|
10952
|
-
return ship(keyIsAdmin(route2) ? "no agent key stored for the release seat (run `crew agents sync`)" : MEMBER_NO_KEY);
|
|
10950
|
+
version = run3(bin, ["--version"]).trim();
|
|
10953
10951
|
} catch (e) {
|
|
10954
|
-
|
|
10955
|
-
return { kind: "ship", apiKey: resolveApiKey(route2), reason, storeError: reason };
|
|
10952
|
+
return { ok: false, detail: `${bin} is not installed or not on the scheduler PATH (${e.message.split("\n")[0]})` };
|
|
10956
10953
|
}
|
|
10957
|
-
}
|
|
10958
|
-
async function resolveSeatKey(route2, role, getStore = getSessionStore) {
|
|
10959
|
-
const ship = (reason) => ({ kind: "ship", apiKey: resolveApiKey(route2), reason });
|
|
10960
|
-
if (!SEAT_KEY_ROLES.includes(role)) return ship(`${role} always writes as the ship`);
|
|
10961
|
-
const agentId = seatAgentId(route2, role);
|
|
10962
|
-
if (!agentId) return ship(keyIsAdmin(route2) ? "seat has no linked Agents row (run `crew agents sync`)" : `seat ${MEMBER_NO_ROW}`);
|
|
10963
10954
|
try {
|
|
10964
|
-
|
|
10965
|
-
if (session?.apiKey) return { kind: "agent", apiKey: session.apiKey, agentId };
|
|
10966
|
-
return ship(keyIsAdmin(route2) ? "no agent key stored for this seat (run `crew agents sync`)" : MEMBER_NO_KEY);
|
|
10955
|
+
run3(bin, ["status"]);
|
|
10967
10956
|
} catch (e) {
|
|
10968
|
-
const
|
|
10969
|
-
return {
|
|
10957
|
+
const err3 = e;
|
|
10958
|
+
return {
|
|
10959
|
+
ok: false,
|
|
10960
|
+
detail: `${version}: the gateway does not answer (${(err3.stderr || err3.message).trim().split("\n").filter(Boolean).slice(0, 2).join(" / ")}). Start it; on Docker Desktop its docker section needs supervisor_network_mode = "bridge", and allow_driver_config = true for the cache volume mount.`
|
|
10961
|
+
};
|
|
10970
10962
|
}
|
|
10963
|
+
return { ok: true, detail: version };
|
|
10971
10964
|
}
|
|
10972
|
-
|
|
10973
|
-
|
|
10974
|
-
|
|
10975
|
-
|
|
10976
|
-
|
|
10977
|
-
|
|
10978
|
-
|
|
10979
|
-
|
|
10980
|
-
return
|
|
10965
|
+
|
|
10966
|
+
// src/agent.ts
|
|
10967
|
+
var DIGEST_MAX_AGE_SECONDS = 600;
|
|
10968
|
+
var LEGACY_TOOLCHAIN_HARNESS = "claude";
|
|
10969
|
+
var LEGACY_TOOLCHAIN_PROVIDER = "anthropic";
|
|
10970
|
+
function scrubbedEnv(env = process.env, harness = LEGACY_TOOLCHAIN_HARNESS) {
|
|
10971
|
+
const out = { ...env };
|
|
10972
|
+
for (const v of adapterFor(harness).unsetEnv) delete out[v];
|
|
10973
|
+
return out;
|
|
10981
10974
|
}
|
|
10982
|
-
|
|
10983
|
-
|
|
10984
|
-
|
|
10985
|
-
|
|
10986
|
-
|
|
10987
|
-
const what = typeof code === "number" ? `security exited ${code}` : typeof code === "string" ? `security failed: ${code}` : void 0;
|
|
10988
|
-
const detail = [what, line].filter(Boolean).join(what && line ? ": " : "");
|
|
10989
|
-
return detail ? `OS keychain unavailable (${detail})` : "OS keychain unavailable";
|
|
10975
|
+
var CREW_LANE_ROLE_VAR = "CREW_LANE_ROLE";
|
|
10976
|
+
function assemblePrompt(p) {
|
|
10977
|
+
const sections = [p.roster, p.environment, `${p.common}${p.brief}`];
|
|
10978
|
+
if (p.digest) sections.push(p.digest);
|
|
10979
|
+
return sections.join("\n\n");
|
|
10990
10980
|
}
|
|
10991
|
-
|
|
10992
|
-
|
|
10993
|
-
|
|
10981
|
+
var AgentError = class extends Error {
|
|
10982
|
+
};
|
|
10983
|
+
var AgentSpawnError = class extends AgentError {
|
|
10984
|
+
};
|
|
10985
|
+
var WorkspaceAuthRefused = class extends AgentSpawnError {
|
|
10986
|
+
};
|
|
10987
|
+
var WorkspaceAllowanceExhausted = class extends AgentError {
|
|
10988
|
+
};
|
|
10989
|
+
var ALLOWANCE_REFUSAL_MARKER = "AI_ALLOWANCE_EXCEEDED";
|
|
10990
|
+
function streamShowsAllowanceRefusal(streamPath) {
|
|
10991
|
+
let raw;
|
|
10992
|
+
try {
|
|
10993
|
+
raw = readFileSync21(streamPath, "utf8");
|
|
10994
|
+
} catch {
|
|
10995
|
+
return false;
|
|
10996
|
+
}
|
|
10997
|
+
if (!raw.includes(ALLOWANCE_REFUSAL_MARKER)) return false;
|
|
10998
|
+
for (const line of raw.split("\n")) {
|
|
10999
|
+
if (!line.includes(ALLOWANCE_REFUSAL_MARKER)) continue;
|
|
11000
|
+
let ev;
|
|
11001
|
+
try {
|
|
11002
|
+
ev = JSON.parse(line);
|
|
11003
|
+
} catch {
|
|
11004
|
+
continue;
|
|
11005
|
+
}
|
|
11006
|
+
if (typeof ev !== "object" || ev === null) continue;
|
|
11007
|
+
const e = ev;
|
|
11008
|
+
if (e.type === "result" && e.is_error === true) {
|
|
11009
|
+
if (typeof e.result === "string" && e.result.includes(ALLOWANCE_REFUSAL_MARKER)) return true;
|
|
11010
|
+
} else if (e.type === "assistant" && (e.isApiErrorMessage === true || e.error !== void 0)) {
|
|
11011
|
+
if (JSON.stringify(e.message ?? "").includes(ALLOWANCE_REFUSAL_MARKER)) return true;
|
|
11012
|
+
}
|
|
11013
|
+
}
|
|
11014
|
+
return false;
|
|
10994
11015
|
}
|
|
10995
|
-
|
|
10996
|
-
|
|
10997
|
-
|
|
10998
|
-
|
|
11016
|
+
var digestPathFor = (stateDir, routeLabel, role, runId) => join26(stateDir, `digest-${routeLabel}-${role}${runId ? `-${runId}` : ""}.md`);
|
|
11017
|
+
function planAgentRun(o) {
|
|
11018
|
+
const briefPath = join26(o.route.promptsDir, "personas", `lane-${o.role}.md`);
|
|
11019
|
+
if (!existsSync24(briefPath)) {
|
|
11020
|
+
throw new AgentError(`no brief at ${briefPath} \u2014 refusing to run an unscoped session`);
|
|
11021
|
+
}
|
|
11022
|
+
const commonPath = join26(o.route.promptsDir, "personas", "common.md");
|
|
11023
|
+
const usesCommon = !SELF_CONTAINED_ROLES.has(o.role) && existsSync24(commonPath);
|
|
11024
|
+
const now = o.now ?? (() => Date.now());
|
|
11025
|
+
const routeLabel = routeSlug(o.route.route);
|
|
11026
|
+
const ownDigest = o.runId ? digestPathFor(o.stateDir, routeLabel, o.role, o.runId) : void 0;
|
|
11027
|
+
const digestPath2 = ownDigest && existsSync24(ownDigest) ? ownDigest : digestPathFor(o.stateDir, routeLabel, o.role);
|
|
11028
|
+
let digest;
|
|
11029
|
+
let ageSeconds;
|
|
11030
|
+
if (existsSync24(digestPath2)) {
|
|
11031
|
+
ageSeconds = Math.floor((now() - statSync7(digestPath2).mtimeMs) / 1e3);
|
|
11032
|
+
if (ageSeconds <= DIGEST_MAX_AGE_SECONDS) digest = readFileSync21(digestPath2, "utf8");
|
|
11033
|
+
}
|
|
11034
|
+
const common = usesCommon ? readFileSync21(commonPath, "utf8") : "";
|
|
11035
|
+
const brief = readFileSync21(briefPath, "utf8");
|
|
11036
|
+
const effectiveCommon = o.divergedPrompt ? "" : common;
|
|
11037
|
+
const effectiveBrief = appendAdditions(o.divergedPrompt ?? brief, o.personaAdditions);
|
|
11038
|
+
const localSkills = new Map(listLocalSkills(o.route.promptsDir).map((s) => [s.name, s.prompt]));
|
|
11039
|
+
let expanded;
|
|
11040
|
+
try {
|
|
11041
|
+
expanded = expandSkillMarkers(
|
|
11042
|
+
`${effectiveCommon}${effectiveBrief}`,
|
|
11043
|
+
(name) => o.skillOverrides?.[name] ?? localSkills.get(name)
|
|
11044
|
+
);
|
|
11045
|
+
} catch (e) {
|
|
11046
|
+
if (e instanceof SkillIncludeError) throw new AgentError(`${e.message} \u2014 refusing to run an unscoped session`);
|
|
11047
|
+
throw e;
|
|
11048
|
+
}
|
|
11049
|
+
const prompt = assemblePrompt({
|
|
11050
|
+
roster: o.roster,
|
|
11051
|
+
environment: o.environment,
|
|
11052
|
+
common: "",
|
|
11053
|
+
brief: expanded.text,
|
|
11054
|
+
digest
|
|
11055
|
+
});
|
|
11056
|
+
const promptSha = createHash6("sha256").update(expanded.text).digest("hex");
|
|
11057
|
+
const streamsDir = join26(o.stateDir, "streams");
|
|
11058
|
+
const base = `${routeLabel}-${o.role}-${o.cycle}${o.runId ? `-${o.runId}` : ""}`;
|
|
11059
|
+
const seat = resolveSeat(o.ship, o.route, o.role, o.agentPersona ?? (o.agentModel ? { model: o.agentModel } : {}), o.repo);
|
|
11060
|
+
const { model } = seat;
|
|
11061
|
+
let toolchain = seat.toolchain;
|
|
11062
|
+
if (toolchain.auth === "workspace") {
|
|
11063
|
+
if (!o.apiKey || o.seatKind === "ship") {
|
|
11064
|
+
throw new WorkspaceAuthRefused(`${o.role}: toolchain ${toolchain.name} has auth: workspace, which needs the seat's own agent key \u2014 this seat would fall back to the ship key (run \`crew agents sync\`)`);
|
|
11065
|
+
}
|
|
11066
|
+
if (!o.route.resolved) {
|
|
11067
|
+
throw new WorkspaceAuthRefused(`${o.role}: toolchain ${toolchain.name} has auth: workspace, but route ${o.route.route} is not connected (run \`crew connect\`)`);
|
|
11068
|
+
}
|
|
11069
|
+
toolchain = { ...toolchain, inference: { baseUrl: inferenceBaseUrl(o.route.baseUrl, o.route.resolved.workspaceId), token: o.apiKey } };
|
|
11070
|
+
}
|
|
11071
|
+
const adapter = adapterFor(toolchain.harness);
|
|
11072
|
+
const invocation = adapter.invocation(toolchain, {
|
|
11073
|
+
role: o.role,
|
|
11074
|
+
model,
|
|
11075
|
+
// Skipped when there's no key to send: a route `crew connect` hasn't
|
|
11076
|
+
// resolved yet has nothing to authenticate the MCP connection with either.
|
|
11077
|
+
mcp: o.apiKey ? { url: `${o.route.baseUrl}/api/mcp`, keyVar: API_KEY_VAR } : void 0
|
|
11078
|
+
});
|
|
11079
|
+
const columns = resolveContract(o.route.contract).columns;
|
|
11080
|
+
const agentLog = o.route.resolved && o.apiKey ? {
|
|
11081
|
+
baseUrl: o.route.baseUrl,
|
|
11082
|
+
workspaceId: o.route.resolved.workspaceId,
|
|
11083
|
+
apiKey: o.apiKey,
|
|
11084
|
+
userAgent: o.ship.userAgent,
|
|
11085
|
+
agentId: o.resolvedAgentId,
|
|
11086
|
+
...o.route.resolved.models?.issues ? { tickets: {
|
|
11087
|
+
issuesModelId: o.route.resolved.models.issues,
|
|
11088
|
+
keyColumn: columns.key,
|
|
11089
|
+
tagColumn: columns.tag,
|
|
11090
|
+
counters: {
|
|
11091
|
+
agentRuns: columns.agentRuns,
|
|
11092
|
+
tokensIn: columns.tokensIn,
|
|
11093
|
+
tokensOut: columns.tokensOut,
|
|
11094
|
+
cacheReadTokens: columns.cacheReadTokens,
|
|
11095
|
+
cacheWriteTokens: columns.cacheWriteTokens,
|
|
11096
|
+
costUsd: columns.costUsd
|
|
11097
|
+
}
|
|
11098
|
+
} } : {}
|
|
11099
|
+
} : void 0;
|
|
11100
|
+
return {
|
|
11101
|
+
role: o.role,
|
|
11102
|
+
route: routeLabel,
|
|
11103
|
+
// The route's directory, which is a STARTING POINT and not the
|
|
11104
|
+
// ticket's repo: the session picks its own working directory even when
|
|
11105
|
+
// the runner already selected a ticket (`o.repo` is known then, and
|
|
11106
|
+
// only feeds the per-repo toolchain choice). Where an area spans
|
|
11107
|
+
// several repos this is whichever one `repos` happened to list first
|
|
11108
|
+
// (config.ts), and the digest's `repo` column is what actually places
|
|
11109
|
+
// the work. Kept deliberately rather than guessed at (ISSUE-349).
|
|
11110
|
+
cwd: o.route.dir,
|
|
11111
|
+
bin: invocation.bin,
|
|
11112
|
+
args: invocation.args,
|
|
11113
|
+
toolchain: toolchain.name,
|
|
11114
|
+
harness: toolchain.harness,
|
|
11115
|
+
workspaceAuth: toolchain.auth === "workspace",
|
|
11116
|
+
provider: toolchainProvider(toolchain),
|
|
11117
|
+
maxRunMinutes: resolveMaxRunMinutes(o.route.maxRunMinutes, toolchain.maxRunMinutes),
|
|
11118
|
+
...o.ship.runner === "sandbox" ? { runner: "sandbox", ...o.ship.sandbox ? { sandboxConfig: o.ship.sandbox } : {} } : {},
|
|
11119
|
+
resolution: { toolchain: seat.toolchainWhy, model: seat.modelWhy },
|
|
11120
|
+
warnings: [...seat.warnings ?? [], ...expanded.warnings],
|
|
11121
|
+
prompt,
|
|
11122
|
+
promptBytes: Buffer.byteLength(prompt, "utf8"),
|
|
11123
|
+
unsetEnv: adapter.unsetEnv,
|
|
11124
|
+
setEnv: {
|
|
11125
|
+
...o.apiKey ? { [API_KEY_VAR]: o.apiKey } : {},
|
|
11126
|
+
...adapter.runEnv(toolchain),
|
|
11127
|
+
// The operator's global SessionStart hook (pair-context-hook.sh) injects
|
|
11128
|
+
// the interactive Pair persona's prompt for any session it can't tell
|
|
11129
|
+
// apart from a person's own terminal. Every headless lane run this
|
|
11130
|
+
// planner produces carries a real ticket brief already (`prompt`
|
|
11131
|
+
// above) — it must never be replaced by the Pair persona with no
|
|
11132
|
+
// ticket attached, which silently no-ops and strands the ticket
|
|
11133
|
+
// in_progress (ISSUE-490, ISSUE-591). Setting this is what lets that
|
|
11134
|
+
// hook tell the two apart; it has nothing to do with the run itself.
|
|
11135
|
+
[CREW_LANE_ROLE_VAR]: o.role,
|
|
11136
|
+
// CREW-2783: the ship's forge credential — git helper config plus the
|
|
11137
|
+
// token as an env var for `gh` and friends. Empty without `forges:`.
|
|
11138
|
+
...forgeRunEnv(o.ship.forges, { crewCommand: crewCommandLine(), configFile: process.env.CREW_CONFIG, stateDir: o.stateDir })
|
|
11139
|
+
},
|
|
11140
|
+
digestAttached: digest !== void 0,
|
|
11141
|
+
digestAgeSeconds: ageSeconds,
|
|
11142
|
+
model,
|
|
11143
|
+
streamPath: join26(streamsDir, `${base}.jsonl`),
|
|
11144
|
+
eventsPath: join26(streamsDir, `${base}.events.jsonl`),
|
|
11145
|
+
ticket: o.ticket,
|
|
11146
|
+
ticketRecordId: o.ticketRecordId ?? null,
|
|
11147
|
+
repo: o.repo ?? null,
|
|
11148
|
+
routeName: o.route.route,
|
|
11149
|
+
agentLog,
|
|
11150
|
+
promptVersion: o.route.resolved?.agentPersonas?.[o.role]?.historyId,
|
|
11151
|
+
promptSha
|
|
11152
|
+
};
|
|
11153
|
+
}
|
|
11154
|
+
function describePlan(p) {
|
|
11155
|
+
return [
|
|
11156
|
+
`role: ${p.role}`,
|
|
11157
|
+
...p.resolution ? [`resolved: toolchain ${p.toolchain} \u2190 ${p.resolution.toolchain}; model ${p.model} \u2190 ${p.resolution.model}${p.provider ? `; provider ${p.provider}` : ""}`] : [],
|
|
11158
|
+
...(p.warnings ?? []).map((w) => `warning: ${w}`),
|
|
11159
|
+
// Only for a non-default toolchain, so a plain Claude run's output is unchanged.
|
|
11160
|
+
...p.toolchain && p.toolchain !== "claude" ? [`toolchain: ${p.toolchain} (${p.harness ?? "claude"})`] : [],
|
|
11161
|
+
`cwd: ${p.cwd}`,
|
|
11162
|
+
`command: ${p.bin} ${p.args.join(" ")}`,
|
|
11163
|
+
`prompt: ${p.promptBytes} bytes on stdin (never argv)`,
|
|
11164
|
+
`digest: ${p.digestAttached ? `attached, ${p.digestAgeSeconds}s old` : "absent \u2014 the agent will fetch the tracker itself"}`,
|
|
11165
|
+
`unset: ${p.unsetEnv.join(" ")}`,
|
|
11166
|
+
// The VALUE is deliberately absent: --dry-run output is pasted into
|
|
11167
|
+
// tickets and chat.
|
|
11168
|
+
`set: ${Object.keys(p.setEnv).join(" ") || "(nothing)"}`,
|
|
11169
|
+
`stream: ${p.streamPath}`
|
|
11170
|
+
].join("\n");
|
|
11171
|
+
}
|
|
11172
|
+
var pidPathFor = (streamPath) => `${streamPath}.pid`;
|
|
11173
|
+
function readRunSidecar(streamPath) {
|
|
11174
|
+
try {
|
|
11175
|
+
const sc = JSON.parse(readFileSync21(pidPathFor(streamPath), "utf8"));
|
|
11176
|
+
return typeof sc.pid === "number" && typeof sc.spawnedAt === "string" && typeof sc.role === "string" ? sc : null;
|
|
11177
|
+
} catch {
|
|
11178
|
+
return null;
|
|
11179
|
+
}
|
|
11180
|
+
}
|
|
11181
|
+
function runIsAlive(sc) {
|
|
11182
|
+
try {
|
|
11183
|
+
process.kill(sc.pid, 0);
|
|
11184
|
+
} catch (e) {
|
|
11185
|
+
if (e.code !== "EPERM") return false;
|
|
11186
|
+
}
|
|
11187
|
+
const ps = spawnSync6("ps", ["-o", "lstart=", "-p", String(sc.pid)], { encoding: "utf8" });
|
|
11188
|
+
const started = Date.parse(ps.stdout.trim());
|
|
11189
|
+
if (ps.status !== 0 || Number.isNaN(started)) return ps.status === 0;
|
|
11190
|
+
return Math.abs(started - Date.parse(sc.spawnedAt)) < 15e3;
|
|
11191
|
+
}
|
|
11192
|
+
function identityOf(plan, adapterHarness) {
|
|
11193
|
+
return {
|
|
11194
|
+
role: plan.role,
|
|
11195
|
+
route: plan.route,
|
|
11196
|
+
ticket: plan.ticket ?? null,
|
|
11197
|
+
model: plan.model,
|
|
11198
|
+
toolchain: plan.toolchain ?? LEGACY_TOOLCHAIN_HARNESS,
|
|
11199
|
+
harness: adapterHarness,
|
|
11200
|
+
provider: plan.provider ?? LEGACY_TOOLCHAIN_PROVIDER,
|
|
11201
|
+
eventsPath: plan.eventsPath,
|
|
11202
|
+
streamPath: plan.streamPath,
|
|
11203
|
+
agentLog: plan.agentLog ? withoutKey(plan.agentLog) : void 0,
|
|
11204
|
+
promptVersion: plan.promptVersion,
|
|
11205
|
+
promptSha: plan.promptSha
|
|
11206
|
+
};
|
|
11207
|
+
}
|
|
11208
|
+
async function killForDeadline(kill, pid, emit, role, ticket, spawnedAt, limitMinutes) {
|
|
11209
|
+
const elapsedMs = Date.now() - Date.parse(spawnedAt);
|
|
11210
|
+
const limit = limitMinutes ?? Math.round(elapsedMs / 6e4);
|
|
11211
|
+
emit.emit(`run_killed: ${role} run exceeded its wall-clock limit of ${limit} min (ran ${Math.round(elapsedMs / 6e4)} min)`, {
|
|
11212
|
+
role,
|
|
11213
|
+
...ticket ? { ticket } : {},
|
|
11214
|
+
data: { event: "run_killed", reason: WALL_CLOCK_REASON, limitMinutes: limit, elapsedMs, pid }
|
|
11215
|
+
});
|
|
11216
|
+
await kill();
|
|
11217
|
+
return { limitMinutes: limit, elapsedMs };
|
|
11218
|
+
}
|
|
11219
|
+
var withoutKey = ({ apiKey: _apiKey, ...rest }) => rest;
|
|
11220
|
+
function openSink(path) {
|
|
11221
|
+
try {
|
|
11222
|
+
mkdirSync13(dirname8(path), { recursive: true });
|
|
11223
|
+
return createWriteStream(path, { flags: "a" });
|
|
11224
|
+
} catch {
|
|
11225
|
+
return void 0;
|
|
11226
|
+
}
|
|
11227
|
+
}
|
|
11228
|
+
function endSink(sink) {
|
|
11229
|
+
if (!sink) return Promise.resolve();
|
|
11230
|
+
sink.end();
|
|
11231
|
+
return finished(sink).catch(() => void 0);
|
|
11232
|
+
}
|
|
11233
|
+
var FileTail = class {
|
|
11234
|
+
offset = 0;
|
|
11235
|
+
decoder = new StringDecoder("utf8");
|
|
11236
|
+
path;
|
|
11237
|
+
constructor(path) {
|
|
11238
|
+
this.path = path;
|
|
11239
|
+
}
|
|
11240
|
+
poll() {
|
|
11241
|
+
let fd;
|
|
11242
|
+
try {
|
|
11243
|
+
fd = openSync3(this.path, "r");
|
|
11244
|
+
} catch {
|
|
11245
|
+
return "";
|
|
11246
|
+
}
|
|
11247
|
+
try {
|
|
11248
|
+
let text2 = "";
|
|
11249
|
+
const chunk = Buffer.alloc(64 * 1024);
|
|
11250
|
+
for (; ; ) {
|
|
11251
|
+
const n = readSync(fd, chunk, 0, chunk.length, this.offset);
|
|
11252
|
+
if (n <= 0) break;
|
|
11253
|
+
this.offset += n;
|
|
11254
|
+
text2 += this.decoder.write(chunk.subarray(0, n));
|
|
11255
|
+
}
|
|
11256
|
+
return text2;
|
|
11257
|
+
} finally {
|
|
11258
|
+
closeSync3(fd);
|
|
11259
|
+
}
|
|
11260
|
+
}
|
|
11261
|
+
};
|
|
11262
|
+
var TAIL_POLL_MS = 200;
|
|
11263
|
+
var RunObserver = class {
|
|
11264
|
+
cycles = [];
|
|
11265
|
+
attribution = new TicketAttributionTracker();
|
|
11266
|
+
/** Hand-off writes and their outcomes, to tell a run whose hand-off never landed (CREW-2363). */
|
|
11267
|
+
handoff = new HandoffTranscriptTracker();
|
|
11268
|
+
result;
|
|
11269
|
+
/** Prefix tokens of the first assistant call that carried usage; kept once. */
|
|
11270
|
+
prefixTokens;
|
|
11271
|
+
/** Set once the stream carries the harness's final result (turn count or cost). */
|
|
11272
|
+
sawResult = false;
|
|
11273
|
+
muted = false;
|
|
11274
|
+
/**
|
|
11275
|
+
* The label stamped on events. Starts as the poll hint and switches to the
|
|
11276
|
+
* ticket the run actually claims (CREW-1776); `id.ticket` stays the hint.
|
|
11277
|
+
*/
|
|
11278
|
+
liveTicket;
|
|
11279
|
+
/** Called after `liveTicket` changes, so the sidecar can be rewritten. */
|
|
11280
|
+
onClaim;
|
|
11281
|
+
buf = "";
|
|
11282
|
+
mapper;
|
|
11283
|
+
id;
|
|
11284
|
+
adapter;
|
|
11285
|
+
eventsSink;
|
|
11286
|
+
emitCycle;
|
|
11287
|
+
constructor(id, adapter, eventsSink, emitCycle) {
|
|
11288
|
+
this.id = id;
|
|
11289
|
+
this.adapter = adapter;
|
|
11290
|
+
this.eventsSink = eventsSink;
|
|
11291
|
+
this.emitCycle = emitCycle;
|
|
11292
|
+
this.liveTicket = id.ticket ?? void 0;
|
|
11293
|
+
this.mapper = adapter.createStreamMapper?.() ?? { map: (p) => adapter.mapStreamLine(p), flush: () => [] };
|
|
11294
|
+
}
|
|
11295
|
+
feed(text2) {
|
|
11296
|
+
this.buf += text2;
|
|
11297
|
+
const lines2 = this.buf.split("\n");
|
|
11298
|
+
this.buf = lines2.pop() ?? "";
|
|
11299
|
+
for (const line of lines2) this.handleLine(line);
|
|
11300
|
+
}
|
|
11301
|
+
end() {
|
|
11302
|
+
if (this.buf) this.handleLine(this.buf);
|
|
11303
|
+
this.buf = "";
|
|
11304
|
+
try {
|
|
11305
|
+
this.consume(void 0, this.mapper.flush());
|
|
11306
|
+
} catch {
|
|
11307
|
+
}
|
|
11308
|
+
}
|
|
11309
|
+
handleLine(line) {
|
|
11310
|
+
if (!line) return;
|
|
11311
|
+
let parsed;
|
|
11312
|
+
try {
|
|
11313
|
+
parsed = JSON.parse(line);
|
|
11314
|
+
} catch {
|
|
11315
|
+
return;
|
|
11316
|
+
}
|
|
11317
|
+
try {
|
|
11318
|
+
const got = this.adapter.extractResult(parsed);
|
|
11319
|
+
if (got) {
|
|
11320
|
+
this.result = { ...this.result, ...Object.fromEntries(Object.entries(got).filter(([, v]) => v !== void 0)) };
|
|
11321
|
+
if (got.numTurns !== void 0 || got.costUsd !== void 0) this.sawResult = true;
|
|
11322
|
+
}
|
|
11323
|
+
this.handoff.feed(parsed);
|
|
11324
|
+
this.consume(parsed, this.mapper.map(parsed));
|
|
11325
|
+
} catch {
|
|
11326
|
+
}
|
|
11327
|
+
}
|
|
11328
|
+
// Turns mapped events (and, when there is a source line, its token usage)
|
|
11329
|
+
// into sink writes and buffered cycles. `parsed` is undefined for events
|
|
11330
|
+
// the mapper flushes at end of stream, which have no line of their own.
|
|
11331
|
+
consume(parsed, events) {
|
|
11332
|
+
const plan = this.id;
|
|
11333
|
+
let turnText = "";
|
|
11334
|
+
for (const ev of events) if ((ev.kind === "thought" || ev.kind === "text") && ev.text) this.attribution.note(ev.text);
|
|
11335
|
+
for (const ev of events) {
|
|
11336
|
+
if (ev.kind === "tool") this.attribution.noteWrite(ev.tool, ev.input);
|
|
11337
|
+
if (ev.kind === "tool" && isClaimWrite(ev.tool, ev.input)) {
|
|
11338
|
+
const claimed = this.attribution.current;
|
|
11339
|
+
if (claimed && claimed !== this.liveTicket) {
|
|
11340
|
+
if (!this.muted) {
|
|
11341
|
+
this.eventsSink?.write(`${JSON.stringify({
|
|
11342
|
+
at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
11343
|
+
cycle: this.emitCycle,
|
|
11344
|
+
route: plan.route,
|
|
11345
|
+
step: "agent",
|
|
11346
|
+
level: "info",
|
|
11347
|
+
message: `ticket: ${this.liveTicket ?? "none"} \u2192 ${claimed}`,
|
|
11348
|
+
role: plan.role,
|
|
11349
|
+
ticket: claimed,
|
|
11350
|
+
data: { kind: "ticket", from: this.liveTicket ?? null, to: claimed }
|
|
11351
|
+
})}
|
|
11352
|
+
`);
|
|
11353
|
+
}
|
|
11354
|
+
this.liveTicket = claimed;
|
|
11355
|
+
if (!this.muted) this.onClaim?.();
|
|
11356
|
+
}
|
|
11357
|
+
}
|
|
11358
|
+
const e = {
|
|
11359
|
+
at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
11360
|
+
cycle: this.emitCycle,
|
|
11361
|
+
route: plan.route,
|
|
11362
|
+
step: "agent",
|
|
11363
|
+
level: "info",
|
|
11364
|
+
message: ev.kind === "tool" ? `${ev.tool ?? "tool"}${ev.target ? ` ${ev.target}` : ""}` : ev.kind,
|
|
11365
|
+
role: plan.role,
|
|
11366
|
+
...this.liveTicket ? { ticket: this.liveTicket } : {},
|
|
11367
|
+
data: { kind: ev.kind, ...ev.text ? { text: ev.text } : {}, ...ev.tool ? { tool: ev.tool } : {}, ...ev.target ? { target: ev.target } : {} }
|
|
11368
|
+
};
|
|
11369
|
+
if (!this.muted) this.eventsSink?.write(`${JSON.stringify(e)}
|
|
11370
|
+
`);
|
|
11371
|
+
if ((ev.kind === "thought" || ev.kind === "text") && ev.text) {
|
|
11372
|
+
turnText += `${ev.text}
|
|
11373
|
+
`;
|
|
11374
|
+
const cycleIndex = this.cycles.length;
|
|
11375
|
+
this.cycles.push({
|
|
11376
|
+
cycleIndex,
|
|
11377
|
+
occurredAt: e.at,
|
|
11378
|
+
thinking: ev.text,
|
|
11379
|
+
// A conversational `text` turn is tagged 'assistant' so it's
|
|
11380
|
+
// distinguishable from a thinking block once both share this
|
|
11381
|
+
// buffer (ISSUE-594) — matching the precedent
|
|
11382
|
+
// `agent-chat.service.ts`'s logChatRun (ISSUE-504) already set
|
|
11383
|
+
// for the same column. A `thought` cycle stays untagged, same
|
|
11384
|
+
// as before this change.
|
|
11385
|
+
...ev.kind === "text" ? { role: "assistant" } : {}
|
|
11386
|
+
});
|
|
11387
|
+
if (!this.muted) {
|
|
11388
|
+
this.eventsSink?.write(`${JSON.stringify({
|
|
11389
|
+
...e,
|
|
11390
|
+
message: "cycle",
|
|
11391
|
+
data: { kind: "cycle", cycleIndex, text: ev.text }
|
|
11392
|
+
})}
|
|
11393
|
+
`);
|
|
11394
|
+
}
|
|
11395
|
+
}
|
|
11396
|
+
}
|
|
11397
|
+
if (this.prefixTokens === void 0 && parsed !== void 0) this.prefixTokens = this.adapter.extractPrefixTokens?.(parsed);
|
|
11398
|
+
const turnTokens = parsed === void 0 ? void 0 : this.adapter.extractTurnTokens(parsed);
|
|
11399
|
+
if (turnTokens !== void 0) this.attribution.add(turnText, turnTokens);
|
|
10999
11400
|
}
|
|
11000
|
-
|
|
11001
|
-
|
|
11002
|
-
async function mintMissingSeatKeys(route2, o) {
|
|
11003
|
-
const out = [];
|
|
11004
|
-
if (!route2.resolved) return out;
|
|
11005
|
-
const resolved = route2.resolved;
|
|
11006
|
-
const doFetch = o.fetchImpl ?? fetch;
|
|
11007
|
-
const targets = [
|
|
11008
|
-
...SEAT_KEY_ROLES.filter((r) => resolved.seats[r]).map((role) => ({ role, agentId: seatAgentId(route2, role) })),
|
|
11009
|
-
...resolved.releaseSeat ? [{ role: "release", agentId: resolved.releaseAgentId }] : []
|
|
11010
|
-
];
|
|
11011
|
-
let store;
|
|
11401
|
+
};
|
|
11402
|
+
function writeRunMeta(id, startedAt, liveTicket, extra) {
|
|
11012
11403
|
try {
|
|
11013
|
-
|
|
11014
|
-
|
|
11015
|
-
|
|
11016
|
-
|
|
11017
|
-
|
|
11018
|
-
|
|
11404
|
+
writeFileSync15(`${id.streamPath}.meta.json`, JSON.stringify({
|
|
11405
|
+
route: id.route,
|
|
11406
|
+
role: id.role,
|
|
11407
|
+
hint: id.ticket,
|
|
11408
|
+
ticket: liveTicket ?? id.ticket,
|
|
11409
|
+
startedAt,
|
|
11410
|
+
model: id.model,
|
|
11411
|
+
toolchain: id.toolchain,
|
|
11412
|
+
harness: id.harness,
|
|
11413
|
+
provider: id.provider,
|
|
11414
|
+
exitCode: null,
|
|
11415
|
+
running: true,
|
|
11416
|
+
...extra
|
|
11019
11417
|
}));
|
|
11418
|
+
} catch {
|
|
11020
11419
|
}
|
|
11021
|
-
|
|
11022
|
-
|
|
11023
|
-
|
|
11024
|
-
|
|
11420
|
+
}
|
|
11421
|
+
async function finishRun(id, obs, emit, startedAt, end, agentLogKey, hintMoved) {
|
|
11422
|
+
let moved = false;
|
|
11423
|
+
if (hintMoved) {
|
|
11025
11424
|
try {
|
|
11026
|
-
await
|
|
11027
|
-
|
|
11028
|
-
|
|
11029
|
-
|
|
11030
|
-
|
|
11425
|
+
moved = await hintMoved();
|
|
11426
|
+
} catch {
|
|
11427
|
+
}
|
|
11428
|
+
}
|
|
11429
|
+
const { ticket: attributedTicket, by: attributedBy } = obs.attribution.decide(obs.liveTicket, { hintMoved: moved });
|
|
11430
|
+
const identity = { toolchain: id.toolchain, harness: id.harness, provider: id.provider };
|
|
11431
|
+
const { result } = obs;
|
|
11432
|
+
const pending = end.interrupted ? null : obs.handoff.pending();
|
|
11433
|
+
if (pending) {
|
|
11434
|
+
try {
|
|
11435
|
+
const now = /* @__PURE__ */ new Date();
|
|
11436
|
+
writePendingHandoff(stateDirOfStream(id.streamPath), {
|
|
11437
|
+
...pending,
|
|
11438
|
+
version: 1,
|
|
11439
|
+
route: id.route,
|
|
11440
|
+
role: id.role,
|
|
11441
|
+
...attributedTicket ? { ticket: attributedTicket } : {},
|
|
11442
|
+
streamPath: id.streamPath,
|
|
11443
|
+
createdAt: now.toISOString(),
|
|
11444
|
+
attempts: 0,
|
|
11445
|
+
nextAttemptAt: now.toISOString()
|
|
11031
11446
|
});
|
|
11032
|
-
await store.clear(host, probeSlug);
|
|
11033
11447
|
} catch (e) {
|
|
11034
|
-
|
|
11035
|
-
return targets.map(({ role, agentId }) => agentId ? { role, action: "failed", detail } : { role, action: "no_agent" });
|
|
11448
|
+
emit.warn(`could not save the pending hand-off: ${e.message}`, { step: "agent" });
|
|
11036
11449
|
}
|
|
11037
11450
|
}
|
|
11038
|
-
|
|
11039
|
-
|
|
11040
|
-
|
|
11041
|
-
|
|
11451
|
+
try {
|
|
11452
|
+
writeRunMeta(id, startedAt, obs.liveTicket, {
|
|
11453
|
+
attributedTicket: attributedTicket ?? null,
|
|
11454
|
+
attributedBy,
|
|
11455
|
+
running: void 0,
|
|
11456
|
+
exitCode: end.code,
|
|
11457
|
+
...end.interrupted ? { interrupted: end.interrupted } : {},
|
|
11458
|
+
...pending ? { handoff: "pending" } : {},
|
|
11459
|
+
sessionId: result?.sessionId ?? null,
|
|
11460
|
+
numTurns: result?.numTurns ?? null,
|
|
11461
|
+
prefixTokens: obs.prefixTokens ?? null,
|
|
11462
|
+
totalCostUsd: result?.costUsd ?? null
|
|
11463
|
+
});
|
|
11464
|
+
} catch {
|
|
11465
|
+
}
|
|
11466
|
+
const ms3 = Date.now() - Date.parse(startedAt);
|
|
11467
|
+
emit.emit(end.interrupted ? "agent run interrupted" : `agent run finished (${pending ? "hand-off pending" : `exit ${end.code ?? "unknown"}`})`, {
|
|
11468
|
+
data: {
|
|
11469
|
+
ms: ms3,
|
|
11470
|
+
code: end.code,
|
|
11471
|
+
...pending ? { handoff: "pending" } : {},
|
|
11472
|
+
...end.interrupted ? { interrupted: end.interrupted } : {},
|
|
11473
|
+
...identity,
|
|
11474
|
+
attributedTicket: attributedTicket ?? null,
|
|
11475
|
+
attributedBy,
|
|
11476
|
+
...result?.sessionId ? { sessionId: result.sessionId } : {},
|
|
11477
|
+
...result?.numTurns !== void 0 ? { numTurns: result.numTurns } : {},
|
|
11478
|
+
...result?.costUsd !== void 0 ? { totalCostUsd: result.costUsd } : {}
|
|
11042
11479
|
}
|
|
11043
|
-
|
|
11480
|
+
});
|
|
11481
|
+
if (id.agentLog && agentLogKey) {
|
|
11482
|
+
const report = {
|
|
11483
|
+
ticketReference: attributedTicket,
|
|
11484
|
+
outcome: end.interrupted ? "interrupted" : end.code === 0 || end.code === null && obs.sawResult ? "success" : "error",
|
|
11485
|
+
startedAt,
|
|
11486
|
+
finishedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
11487
|
+
cycles: obs.cycles,
|
|
11488
|
+
model: id.model,
|
|
11489
|
+
tokensIn: result?.inputTokens,
|
|
11490
|
+
tokensOut: result?.outputTokens,
|
|
11491
|
+
cacheReadTokens: result?.cacheReadTokens,
|
|
11492
|
+
cacheWriteTokens: result?.cacheWriteTokens,
|
|
11493
|
+
costUsd: result?.costUsd,
|
|
11494
|
+
// Both come from a finished run: without the result line (interrupted) neither is trustworthy.
|
|
11495
|
+
...result?.numTurns !== void 0 ? { numTurns: result.numTurns } : {},
|
|
11496
|
+
...result?.numTurns !== void 0 && obs.prefixTokens !== void 0 ? { prefixTokens: obs.prefixTokens } : {},
|
|
11497
|
+
harness: identity.harness,
|
|
11498
|
+
provider: identity.provider,
|
|
11499
|
+
promptVersion: id.promptVersion,
|
|
11500
|
+
promptSha: id.promptSha
|
|
11501
|
+
};
|
|
11044
11502
|
try {
|
|
11045
|
-
|
|
11046
|
-
out.push({ role, action: "already_stored" });
|
|
11047
|
-
continue;
|
|
11048
|
-
}
|
|
11049
|
-
if (o.dryRun) {
|
|
11050
|
-
out.push({ role, action: "minted", detail: "dry run" });
|
|
11051
|
-
continue;
|
|
11052
|
-
}
|
|
11053
|
-
const res = await doFetch(`${base}/api/workspaces/${resolved.workspaceId}/agents/${agentId}/api-keys`, {
|
|
11054
|
-
method: "POST",
|
|
11055
|
-
headers: {
|
|
11056
|
-
Authorization: `Bearer ${resolveApiKey(route2)}`,
|
|
11057
|
-
"Content-Type": "application/json",
|
|
11058
|
-
"User-Agent": o.userAgent ?? "Mozilla/5.0 TablationCrewAgent/1.0"
|
|
11059
|
-
},
|
|
11060
|
-
body: JSON.stringify({ name: `crew ${o.shipName ?? "ship"} ${role}` })
|
|
11061
|
-
});
|
|
11062
|
-
if (res.status === 403) {
|
|
11063
|
-
out.push({ role, action: "forbidden" });
|
|
11064
|
-
break;
|
|
11065
|
-
}
|
|
11066
|
-
if (!res.ok) {
|
|
11067
|
-
out.push({ role, action: "failed", detail: `${res.status} ${res.statusText}` });
|
|
11068
|
-
continue;
|
|
11069
|
-
}
|
|
11070
|
-
const body = await res.json();
|
|
11071
|
-
if (!body.key) {
|
|
11072
|
-
out.push({ role, action: "failed", detail: "response carried no key" });
|
|
11073
|
-
continue;
|
|
11074
|
-
}
|
|
11503
|
+
const sent = await reportAgentRun({ ...id.agentLog, apiKey: agentLogKey }, report);
|
|
11075
11504
|
try {
|
|
11076
|
-
|
|
11077
|
-
|
|
11078
|
-
|
|
11079
|
-
|
|
11080
|
-
|
|
11081
|
-
|
|
11082
|
-
|
|
11083
|
-
|
|
11084
|
-
|
|
11085
|
-
|
|
11086
|
-
|
|
11087
|
-
|
|
11088
|
-
|
|
11089
|
-
|
|
11090
|
-
|
|
11091
|
-
|
|
11092
|
-
revoked = del2.ok;
|
|
11093
|
-
} catch {
|
|
11094
|
-
revoked = false;
|
|
11095
|
-
}
|
|
11505
|
+
clearShipAttention(new State(stateDirOfStream(id.streamPath)), agentLogRefusedKey(id.route));
|
|
11506
|
+
} catch {
|
|
11507
|
+
}
|
|
11508
|
+
if (sent.counterWarning) emit.warn(sent.counterWarning, { step: "agent" });
|
|
11509
|
+
if (sent.counterRetry) {
|
|
11510
|
+
try {
|
|
11511
|
+
queueTicketCounters(stateDirOfStream(id.streamPath), basename8(id.streamPath, ".jsonl"), {
|
|
11512
|
+
version: 1,
|
|
11513
|
+
route: id.route,
|
|
11514
|
+
role: id.role,
|
|
11515
|
+
target: id.agentLog,
|
|
11516
|
+
write: sent.counterRetry,
|
|
11517
|
+
attempts: 0,
|
|
11518
|
+
nextAttemptAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
11519
|
+
});
|
|
11520
|
+
} catch {
|
|
11096
11521
|
}
|
|
11097
|
-
out.push({
|
|
11098
|
-
role,
|
|
11099
|
-
action: "failed",
|
|
11100
|
-
detail: revoked ? `minted but could not be stored (${describeStoreError(e)}); revoked` : `a key for ${role} is live on the server and not stored (${describeStoreError(e)}) \u2014 revoke it from that seat's Agents row`
|
|
11101
|
-
});
|
|
11102
|
-
continue;
|
|
11103
11522
|
}
|
|
11104
|
-
out.push({ role, action: "minted" });
|
|
11105
11523
|
} catch (e) {
|
|
11106
|
-
|
|
11524
|
+
if (e instanceof AgentLogUnreachableError) {
|
|
11525
|
+
try {
|
|
11526
|
+
queueAgentRun(stateDirOfStream(id.streamPath), basename8(id.streamPath, ".jsonl"), {
|
|
11527
|
+
version: 1,
|
|
11528
|
+
route: id.route,
|
|
11529
|
+
role: id.role,
|
|
11530
|
+
target: id.agentLog,
|
|
11531
|
+
report,
|
|
11532
|
+
attempts: 0,
|
|
11533
|
+
nextAttemptAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
11534
|
+
});
|
|
11535
|
+
emit.warn(`could not report agent log (${e.message}); queued to retry`, { step: "agent" });
|
|
11536
|
+
return { ms: ms3, ticket: attributedTicket, handoffPending: !!pending };
|
|
11537
|
+
} catch {
|
|
11538
|
+
}
|
|
11539
|
+
}
|
|
11540
|
+
if (e instanceof AgentLogHttpError && e.row && e.status >= 400 && e.status < 500) {
|
|
11541
|
+
try {
|
|
11542
|
+
await raiseShipAttention(
|
|
11543
|
+
{ state: new State(stateDirOfStream(id.streamPath)), emit },
|
|
11544
|
+
agentLogRefusedItem(id.route, id.role, e.status)
|
|
11545
|
+
);
|
|
11546
|
+
} catch {
|
|
11547
|
+
}
|
|
11548
|
+
}
|
|
11549
|
+
emit.warn(`could not report agent log: ${e.message}`, { step: "agent" });
|
|
11107
11550
|
}
|
|
11108
11551
|
}
|
|
11109
|
-
return
|
|
11552
|
+
return { ms: ms3, ticket: attributedTicket, handoffPending: !!pending };
|
|
11110
11553
|
}
|
|
11111
|
-
function
|
|
11112
|
-
|
|
11113
|
-
|
|
11114
|
-
|
|
11115
|
-
|
|
11116
|
-
|
|
11117
|
-
|
|
11118
|
-
|
|
11119
|
-
|
|
11120
|
-
|
|
11121
|
-
|
|
11122
|
-
|
|
11554
|
+
function spawnAgent(plan, emit, hintMoved) {
|
|
11555
|
+
const adapter = adapterFor(plan.harness ?? LEGACY_TOOLCHAIN_HARNESS);
|
|
11556
|
+
const runner = runnerFor(plan.runner, plan.sandboxConfig);
|
|
11557
|
+
const started = Date.now();
|
|
11558
|
+
const startedAt = new Date(started).toISOString();
|
|
11559
|
+
for (const w of plan.warnings ?? []) emit.warn(w, { step: "agent", role: plan.role });
|
|
11560
|
+
const eventsSink = openSink(plan.eventsPath);
|
|
11561
|
+
const id = identityOf(plan, adapter.harness);
|
|
11562
|
+
let streamFd;
|
|
11563
|
+
try {
|
|
11564
|
+
mkdirSync13(dirname8(plan.streamPath), { recursive: true });
|
|
11565
|
+
streamFd = openSync3(plan.streamPath, "a");
|
|
11566
|
+
} catch (e) {
|
|
11567
|
+
void endSink(eventsSink);
|
|
11568
|
+
return Promise.reject(new AgentSpawnError(`cannot open the stream file ${plan.streamPath}: ${e.message}`));
|
|
11123
11569
|
}
|
|
11124
|
-
|
|
11125
|
-
|
|
11126
|
-
|
|
11127
|
-
|
|
11128
|
-
|
|
11129
|
-
const doFetch = o.fetchImpl ?? fetch;
|
|
11130
|
-
const targets = [
|
|
11131
|
-
...SEAT_KEY_ROLES.filter((r) => resolved.seats[r]).map((role) => ({ role, agentId: seatAgentId(route2, role) })),
|
|
11132
|
-
...resolved.releaseSeat ? [{ role: "release", agentId: resolved.releaseAgentId }] : []
|
|
11133
|
-
];
|
|
11134
|
-
let store;
|
|
11570
|
+
const failed = (e) => {
|
|
11571
|
+
void endSink(eventsSink);
|
|
11572
|
+
return Promise.reject(new AgentSpawnError(`cannot run ${plan.bin}: ${e.message}`));
|
|
11573
|
+
};
|
|
11574
|
+
let begun;
|
|
11135
11575
|
try {
|
|
11136
|
-
|
|
11576
|
+
begun = runner.start(plan, { env: { ...scrubbedEnv(process.env, adapter.harness), ...plan.setEnv }, streamFd, emit });
|
|
11137
11577
|
} catch (e) {
|
|
11138
|
-
|
|
11578
|
+
closeSync3(streamFd);
|
|
11579
|
+
return failed(e);
|
|
11139
11580
|
}
|
|
11140
|
-
|
|
11141
|
-
|
|
11142
|
-
|
|
11143
|
-
|
|
11144
|
-
|
|
11145
|
-
|
|
11146
|
-
|
|
11147
|
-
|
|
11148
|
-
|
|
11149
|
-
const session = await store.get(host, slug2);
|
|
11150
|
-
if (!session?.apiKey) {
|
|
11151
|
-
out.push({ role, action: "not_stored" });
|
|
11152
|
-
continue;
|
|
11153
|
-
}
|
|
11154
|
-
if (!session.keyId) {
|
|
11155
|
-
out.push({ role, action: "failed", detail: "no key id was recorded when this key was minted \u2014 revoke it from the seat's Agents row" });
|
|
11156
|
-
continue;
|
|
11581
|
+
if (begun instanceof Promise) {
|
|
11582
|
+
return begun.then(
|
|
11583
|
+
(run3) => {
|
|
11584
|
+
closeSync3(streamFd);
|
|
11585
|
+
return supervise(run3);
|
|
11586
|
+
},
|
|
11587
|
+
(e) => {
|
|
11588
|
+
closeSync3(streamFd);
|
|
11589
|
+
return failed(e);
|
|
11157
11590
|
}
|
|
11158
|
-
|
|
11159
|
-
|
|
11160
|
-
|
|
11591
|
+
);
|
|
11592
|
+
}
|
|
11593
|
+
closeSync3(streamFd);
|
|
11594
|
+
return supervise(begun);
|
|
11595
|
+
function supervise({ child, handle, promptDelivered }) {
|
|
11596
|
+
return new Promise((resolve7, reject) => {
|
|
11597
|
+
const spawnedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
11598
|
+
const deadline = deadlineFor(spawnedAt, plan.maxRunMinutes);
|
|
11599
|
+
if (child.pid !== void 0) {
|
|
11600
|
+
const sidecar = {
|
|
11601
|
+
pid: child.pid,
|
|
11602
|
+
ownerPid: process.pid,
|
|
11603
|
+
spawnedAt,
|
|
11604
|
+
role: plan.role,
|
|
11605
|
+
route: plan.route,
|
|
11606
|
+
...deadline ? { deadline, maxRunMinutes: plan.maxRunMinutes } : {},
|
|
11607
|
+
...handle.runner === "sandbox" ? { runner: "sandbox", sandbox: handle.sandbox, sandboxBin: handle.sandboxBin } : {},
|
|
11608
|
+
ticket: plan.ticket ?? null,
|
|
11609
|
+
ticketRecordId: plan.ticketRecordId ?? null,
|
|
11610
|
+
repo: plan.repo ?? null,
|
|
11611
|
+
routeName: plan.routeName,
|
|
11612
|
+
model: plan.model,
|
|
11613
|
+
toolchain: id.toolchain,
|
|
11614
|
+
harness: id.harness,
|
|
11615
|
+
provider: id.provider,
|
|
11616
|
+
eventsPath: plan.eventsPath,
|
|
11617
|
+
agentLog: id.agentLog,
|
|
11618
|
+
promptVersion: plan.promptVersion,
|
|
11619
|
+
promptSha: plan.promptSha
|
|
11620
|
+
};
|
|
11621
|
+
try {
|
|
11622
|
+
writeFileSync15(pidPathFor(plan.streamPath), JSON.stringify(sidecar));
|
|
11623
|
+
} catch {
|
|
11624
|
+
}
|
|
11161
11625
|
}
|
|
11162
|
-
const
|
|
11163
|
-
|
|
11164
|
-
|
|
11626
|
+
const obs = new RunObserver(id, adapter, eventsSink, emit.cycle);
|
|
11627
|
+
const meta = { pid: child.pid ?? null, ...deadline ? { deadline } : {} };
|
|
11628
|
+
obs.onClaim = () => writeRunMeta(id, startedAt, obs.liveTicket, meta);
|
|
11629
|
+
if (child.pid !== void 0) writeRunMeta(id, startedAt, obs.liveTicket, meta);
|
|
11630
|
+
const tail3 = new FileTail(plan.streamPath);
|
|
11631
|
+
const timer = setInterval(() => obs.feed(tail3.poll()), TAIL_POLL_MS);
|
|
11632
|
+
let killed;
|
|
11633
|
+
const limitTimer = child.pid !== void 0 && deadline ? setTimeout(() => {
|
|
11634
|
+
void killForDeadline(() => runner.kill(handle, WALL_CLOCK_REASON), child.pid, emit, plan.role, obs.liveTicket ?? plan.ticket ?? null, spawnedAt, plan.maxRunMinutes).then((k) => {
|
|
11635
|
+
killed = k;
|
|
11636
|
+
});
|
|
11637
|
+
}, Math.max(0, msUntilDeadline(deadline) ?? 0)) : void 0;
|
|
11638
|
+
let spawnFailed = false;
|
|
11639
|
+
child.on("error", (err3) => {
|
|
11640
|
+
spawnFailed = true;
|
|
11641
|
+
clearInterval(timer);
|
|
11642
|
+
clearTimeout(limitTimer);
|
|
11643
|
+
void runner.release(handle, emit);
|
|
11644
|
+
reject(new AgentSpawnError(`cannot run ${plan.bin}: ${err3.message}`));
|
|
11165
11645
|
});
|
|
11166
|
-
|
|
11167
|
-
|
|
11168
|
-
|
|
11169
|
-
|
|
11170
|
-
|
|
11171
|
-
|
|
11172
|
-
|
|
11173
|
-
|
|
11174
|
-
|
|
11175
|
-
|
|
11176
|
-
|
|
11177
|
-
}
|
|
11178
|
-
|
|
11179
|
-
|
|
11180
|
-
|
|
11181
|
-
|
|
11182
|
-
|
|
11183
|
-
|
|
11184
|
-
|
|
11185
|
-
|
|
11186
|
-
|
|
11187
|
-
|
|
11646
|
+
child.on("close", async (code) => {
|
|
11647
|
+
clearInterval(timer);
|
|
11648
|
+
clearTimeout(limitTimer);
|
|
11649
|
+
obs.feed(tail3.poll());
|
|
11650
|
+
obs.end();
|
|
11651
|
+
await endSink(eventsSink);
|
|
11652
|
+
if (spawnFailed) return;
|
|
11653
|
+
await runner.release(handle, emit);
|
|
11654
|
+
if (limitTimer && !killed && !obs.sawResult && msUntilDeadline(deadline) <= 0) {
|
|
11655
|
+
killed = { limitMinutes: plan.maxRunMinutes ?? 0, elapsedMs: Date.now() - Date.parse(spawnedAt) };
|
|
11656
|
+
}
|
|
11657
|
+
const { ms: ms3, handoffPending } = await finishRun(
|
|
11658
|
+
id,
|
|
11659
|
+
obs,
|
|
11660
|
+
emit,
|
|
11661
|
+
startedAt,
|
|
11662
|
+
{ code: code ?? 1, ...killed ? { interrupted: WALL_CLOCK_REASON } : {} },
|
|
11663
|
+
plan.agentLog?.apiKey,
|
|
11664
|
+
hintMoved
|
|
11665
|
+
);
|
|
11666
|
+
resolve7({ code: code ?? 1, ms: ms3, ...handoffPending ? { handoffPending } : {}, ...killed ? { killed } : {} });
|
|
11667
|
+
});
|
|
11668
|
+
child.stdin?.on("error", (err3) => {
|
|
11669
|
+
emit.warn(`agent closed stdin before the prompt was delivered: ${err3.message}`, { step: "agent" });
|
|
11670
|
+
});
|
|
11671
|
+
if (promptDelivered) child.stdin?.end();
|
|
11672
|
+
else child.stdin?.end(plan.prompt);
|
|
11673
|
+
});
|
|
11188
11674
|
}
|
|
11189
11675
|
}
|
|
11190
|
-
async function
|
|
11191
|
-
const
|
|
11192
|
-
|
|
11193
|
-
const
|
|
11194
|
-
|
|
11195
|
-
|
|
11196
|
-
|
|
11197
|
-
|
|
11198
|
-
|
|
11199
|
-
|
|
11200
|
-
|
|
11201
|
-
|
|
11202
|
-
|
|
11676
|
+
async function reattachRun(streamPath, sc, emit, o = {}) {
|
|
11677
|
+
const adapter = adapterFor(sc.harness);
|
|
11678
|
+
const isAlive = o.isAlive ?? runIsAlive;
|
|
11679
|
+
const id = { ...sc, streamPath };
|
|
11680
|
+
const eventsSink = openSink(sc.eventsPath);
|
|
11681
|
+
const obs = new RunObserver(id, adapter, eventsSink, emit.cycle);
|
|
11682
|
+
const tail3 = new FileTail(streamPath);
|
|
11683
|
+
obs.muted = true;
|
|
11684
|
+
obs.feed(tail3.poll());
|
|
11685
|
+
obs.muted = false;
|
|
11686
|
+
const runner = runnerOfSidecar(sc);
|
|
11687
|
+
const adopted = isAlive(sc);
|
|
11688
|
+
let killed;
|
|
11689
|
+
if (adopted) {
|
|
11690
|
+
o.onAdopted?.();
|
|
11691
|
+
const pollMs = o.pollMs ?? TAIL_POLL_MS;
|
|
11692
|
+
while (isAlive(sc)) {
|
|
11693
|
+
obs.feed(tail3.poll());
|
|
11694
|
+
if (!killed && (msUntilDeadline(sc.deadline) ?? 1) <= 0) {
|
|
11695
|
+
killed = await killForDeadline(() => runner.kill(runner.reattach(sc), WALL_CLOCK_REASON), sc.pid, emit, sc.role, obs.liveTicket ?? sc.ticket, sc.spawnedAt, sc.maxRunMinutes);
|
|
11696
|
+
continue;
|
|
11697
|
+
}
|
|
11698
|
+
await new Promise((r) => setTimeout(r, pollMs));
|
|
11699
|
+
}
|
|
11203
11700
|
}
|
|
11701
|
+
obs.feed(tail3.poll());
|
|
11702
|
+
obs.end();
|
|
11703
|
+
await endSink(eventsSink);
|
|
11704
|
+
await runner.release(runner.reattach(sc), emit);
|
|
11705
|
+
const interrupted = killed ? WALL_CLOCK_REASON : obs.sawResult ? void 0 : adopted ? "no-result" : "daemon-exit";
|
|
11706
|
+
const { ms: ms3, ticket } = await finishRun(
|
|
11707
|
+
id,
|
|
11708
|
+
obs,
|
|
11709
|
+
emit,
|
|
11710
|
+
sc.spawnedAt,
|
|
11711
|
+
{ code: null, ...interrupted ? { interrupted } : {} },
|
|
11712
|
+
o.agentLogKey
|
|
11713
|
+
);
|
|
11714
|
+
return { adopted, ...interrupted ? { interrupted } : {}, ...killed ? { killed } : {}, ms: ms3, ticket };
|
|
11204
11715
|
}
|
|
11205
11716
|
|
|
11206
11717
|
// src/selftest.ts
|
|
@@ -11364,7 +11875,7 @@ async function runSelftest(i, deps = realDeps) {
|
|
|
11364
11875
|
...adapter.runEnv(spec),
|
|
11365
11876
|
[KEY_VAR]: i.apiKey
|
|
11366
11877
|
};
|
|
11367
|
-
const cwd = mkdtempSync2(
|
|
11878
|
+
const cwd = mkdtempSync2(join27(tmpdir4(), "crew-selftest-"));
|
|
11368
11879
|
try {
|
|
11369
11880
|
const run3 = await deps.run({
|
|
11370
11881
|
bin: invocation.bin,
|
|
@@ -11397,11 +11908,11 @@ function formatResult(r) {
|
|
|
11397
11908
|
const secs = `${(r.ms / 1e3).toFixed(1)}s`;
|
|
11398
11909
|
return r.ok ? `${r.toolchain}: PASS ${r.detail} (${secs}${tok})` : `${r.toolchain}: FAIL ${r.failure} \u2014 ${r.detail} (${secs})`;
|
|
11399
11910
|
}
|
|
11400
|
-
var fileFor2 = (stateDir) =>
|
|
11911
|
+
var fileFor2 = (stateDir) => join27(stateDir, "selftest.json");
|
|
11401
11912
|
function readSelftest(stateDir) {
|
|
11402
11913
|
try {
|
|
11403
11914
|
const p = fileFor2(stateDir);
|
|
11404
|
-
return
|
|
11915
|
+
return existsSync25(p) ? JSON.parse(readFileSync22(p, "utf8")) : {};
|
|
11405
11916
|
} catch {
|
|
11406
11917
|
return {};
|
|
11407
11918
|
}
|
|
@@ -11619,14 +12130,14 @@ function rosterMarkdown(roster, meId) {
|
|
|
11619
12130
|
}
|
|
11620
12131
|
|
|
11621
12132
|
// src/connect-resume.ts
|
|
11622
|
-
import { mkdirSync as mkdirSync15, readFileSync as
|
|
11623
|
-
import { dirname as dirname9, join as
|
|
12133
|
+
import { mkdirSync as mkdirSync15, readFileSync as readFileSync23, rmSync as rmSync15, writeFileSync as writeFileSync17 } from "fs";
|
|
12134
|
+
import { dirname as dirname9, join as join28 } from "path";
|
|
11624
12135
|
function lastSessionFile(stateDir) {
|
|
11625
|
-
return
|
|
12136
|
+
return join28(stateDir, "connect-session.json");
|
|
11626
12137
|
}
|
|
11627
12138
|
function readPointers(stateDir) {
|
|
11628
12139
|
try {
|
|
11629
|
-
const parsed = JSON.parse(
|
|
12140
|
+
const parsed = JSON.parse(readFileSync23(lastSessionFile(stateDir), "utf8"));
|
|
11630
12141
|
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return {};
|
|
11631
12142
|
const out = {};
|
|
11632
12143
|
for (const [host, v] of Object.entries(parsed)) {
|
|
@@ -11726,15 +12237,15 @@ async function signOutSessions(o) {
|
|
|
11726
12237
|
}
|
|
11727
12238
|
|
|
11728
12239
|
// src/reset.ts
|
|
11729
|
-
import { existsSync as
|
|
11730
|
-
import { dirname as dirname10, join as
|
|
12240
|
+
import { existsSync as existsSync26, mkdirSync as mkdirSync16, readFileSync as readFileSync24, readdirSync as readdirSync12, rmSync as rmSync16, statSync as statSync8, writeFileSync as writeFileSync18 } from "fs";
|
|
12241
|
+
import { dirname as dirname10, join as join29 } from "path";
|
|
11731
12242
|
import { hostFromUrl as hostFromUrl3 } from "@tablation/client";
|
|
11732
12243
|
function clonedCheckoutsFile(stateDir) {
|
|
11733
|
-
return
|
|
12244
|
+
return join29(stateDir, "cloned-checkouts.json");
|
|
11734
12245
|
}
|
|
11735
12246
|
function readClonedCheckouts(stateDir) {
|
|
11736
12247
|
try {
|
|
11737
|
-
const parsed = JSON.parse(
|
|
12248
|
+
const parsed = JSON.parse(readFileSync24(clonedCheckoutsFile(stateDir), "utf8"));
|
|
11738
12249
|
return Array.isArray(parsed) ? parsed.filter((d) => typeof d === "string") : [];
|
|
11739
12250
|
} catch {
|
|
11740
12251
|
return [];
|
|
@@ -11787,411 +12298,141 @@ async function runReset(o, fx) {
|
|
|
11787
12298
|
}
|
|
11788
12299
|
if (!o.dryRun) await s.clear();
|
|
11789
12300
|
steps.push({ step: "credential", target, action: mark(o.dryRun), detail: "host session stored by a dry-run connect" });
|
|
11790
|
-
} catch (e) {
|
|
11791
|
-
steps.push({ step: "credential", target, action: "failed", detail: e.message });
|
|
11792
|
-
}
|
|
11793
|
-
}
|
|
11794
|
-
} catch (e) {
|
|
11795
|
-
steps.push({ step: "credential", target: "host sessions", action: "failed", detail: e.message });
|
|
11796
|
-
}
|
|
11797
|
-
if (o.removeCheckouts) {
|
|
11798
|
-
for (const dir of readClonedCheckouts(o.stateDir)) steps.push(removeCheckout(dir, o.dryRun, fx));
|
|
11799
|
-
}
|
|
11800
|
-
if (o.beforeState) steps.push(...await o.beforeState());
|
|
11801
|
-
steps.push(...o.wipeState ? wipeStateDir(o.stateDir, o.dryRun) : removeState(o.stateDir, o.dryRun));
|
|
11802
|
-
if (existsSync23(o.configFile)) {
|
|
11803
|
-
if (!o.dryRun) rmSync16(o.configFile, { force: true });
|
|
11804
|
-
steps.push({ step: "config", target: o.configFile, action: mark(o.dryRun) });
|
|
11805
|
-
} else {
|
|
11806
|
-
steps.push({ step: "config", target: o.configFile, action: "absent" });
|
|
11807
|
-
}
|
|
11808
|
-
return steps;
|
|
11809
|
-
}
|
|
11810
|
-
function removeCheckout(dir, dryRun, fx) {
|
|
11811
|
-
if (!existsSync23(dir)) return { step: "checkout", target: dir, action: "absent", detail: "already gone" };
|
|
11812
|
-
const { dirty, linkedWorktrees } = fx.inspectCheckout(dir);
|
|
11813
|
-
if (dirty.length) {
|
|
11814
|
-
return { step: "checkout", target: dir, action: "kept", detail: `has uncommitted changes (${dirty.length} path${dirty.length === 1 ? "" : "s"}: ${dirty.slice(0, 5).join(", ")}${dirty.length > 5 ? ", \u2026" : ""})` };
|
|
11815
|
-
}
|
|
11816
|
-
if (linkedWorktrees.length) {
|
|
11817
|
-
return { step: "checkout", target: dir, action: "kept", detail: `has linked worktrees (${linkedWorktrees.join(", ")}); remove them first` };
|
|
11818
|
-
}
|
|
11819
|
-
if (!dryRun) rmSync16(dir, { recursive: true, force: true });
|
|
11820
|
-
return { step: "checkout", target: dir, action: mark(dryRun) };
|
|
11821
|
-
}
|
|
11822
|
-
function removeState(stateDir, dryRun) {
|
|
11823
|
-
const out = [];
|
|
11824
|
-
const targets = [join27(stateDir, "resolved"), join27(stateDir, "keys"), clonedCheckoutsFile(stateDir), join27(stateDir, "connect-session.json"), join27(stateDir, "reload-requested"), join27(stateDir, "daemon.started")];
|
|
11825
|
-
try {
|
|
11826
|
-
for (const name of readdirSync9(stateDir)) {
|
|
11827
|
-
if (name.startsWith(".") && statSync8(join27(stateDir, name)).isFile()) targets.push(join27(stateDir, name));
|
|
11828
|
-
}
|
|
11829
|
-
} catch {
|
|
11830
|
-
}
|
|
11831
|
-
for (const t of targets) {
|
|
11832
|
-
if (!existsSync23(t)) continue;
|
|
11833
|
-
if (!dryRun) rmSync16(t, { recursive: true, force: true });
|
|
11834
|
-
out.push({ step: "state", target: t, action: mark(dryRun) });
|
|
11835
|
-
}
|
|
11836
|
-
if (!out.length) out.push({ step: "state", target: stateDir, action: "absent", detail: "nothing to remove" });
|
|
11837
|
-
return out;
|
|
11838
|
-
}
|
|
11839
|
-
function wipeStateDir(stateDir, dryRun) {
|
|
11840
|
-
let names;
|
|
11841
|
-
try {
|
|
11842
|
-
names = readdirSync9(stateDir);
|
|
11843
|
-
} catch {
|
|
11844
|
-
return [{ step: "state", target: stateDir, action: "absent", detail: "no state directory" }];
|
|
11845
|
-
}
|
|
11846
|
-
if (!names.length) return [{ step: "state", target: stateDir, action: "absent", detail: "already empty" }];
|
|
11847
|
-
if (!dryRun) for (const n of names) rmSync16(join27(stateDir, n), { recursive: true, force: true });
|
|
11848
|
-
return [{ step: "state", target: stateDir, action: mark(dryRun), detail: `${names.length} entr${names.length === 1 ? "y" : "ies"}` }];
|
|
11849
|
-
}
|
|
11850
|
-
function describeResetStep(s) {
|
|
11851
|
-
const verb = { removed: "removed", "would-remove": "would remove", absent: "nothing to remove", kept: "KEPT", failed: "FAILED" }[s.action];
|
|
11852
|
-
return `${s.step}: ${verb}: ${s.target}${s.detail ? ` (${s.detail})` : ""}`;
|
|
11853
|
-
}
|
|
11854
|
-
function resetIncomplete(steps) {
|
|
11855
|
-
return steps.some((s) => s.action === "failed" || s.action === "kept");
|
|
11856
|
-
}
|
|
11857
|
-
|
|
11858
|
-
// src/deprovision.ts
|
|
11859
|
-
var mark2 = (dryRun) => dryRun ? "would-remove" : "removed";
|
|
11860
|
-
async function runDeprovision(o, fx, deps) {
|
|
11861
|
-
const attempt = (step, target, fn, dry) => {
|
|
11862
|
-
try {
|
|
11863
|
-
if (!dry) fn();
|
|
11864
|
-
return { step, target, action: mark2(dry) };
|
|
11865
|
-
} catch (e) {
|
|
11866
|
-
return { step, target, action: "failed", detail: e.message };
|
|
11867
|
-
}
|
|
11868
|
-
};
|
|
11869
|
-
const afterScheduler = async () => {
|
|
11870
|
-
const steps2 = [];
|
|
11871
|
-
for (const run3 of deps.liveRuns()) {
|
|
11872
|
-
const target = `${run3.role} run on ${run3.route} (pid ${run3.pid})`;
|
|
11873
|
-
if (o.dryRun) {
|
|
11874
|
-
steps2.push({ step: "run", target, action: "would-remove" });
|
|
11875
|
-
continue;
|
|
11876
|
-
}
|
|
11877
|
-
try {
|
|
11878
|
-
await deps.killRun(run3);
|
|
11879
|
-
deps.recordKill(run3);
|
|
11880
|
-
} catch (e) {
|
|
11881
|
-
steps2.push({ step: "run", target, action: "failed", detail: e.message });
|
|
11882
|
-
continue;
|
|
11883
|
-
}
|
|
11884
|
-
try {
|
|
11885
|
-
await deps.handBack(run3);
|
|
11886
|
-
steps2.push({ step: "run", target, action: "removed", detail: "killed" });
|
|
11887
|
-
} catch (e) {
|
|
11888
|
-
steps2.push({ step: "run", target, action: "removed", detail: `killed; could not tell its ticket (${e.message})` });
|
|
11889
|
-
}
|
|
11890
|
-
}
|
|
11891
|
-
for (const name of deps.sandboxes()) steps2.push(attempt("sandbox", name, () => {
|
|
11892
|
-
deps.removeSandbox(name);
|
|
11893
|
-
}, o.dryRun));
|
|
11894
|
-
if (!o.dryRun) {
|
|
11895
|
-
for (const r of await deps.clearShipRows()) {
|
|
11896
|
-
steps2.push({ step: "ship-row", target: r.route, action: r.ok ? "removed" : "failed", detail: r.detail });
|
|
11897
|
-
}
|
|
11898
|
-
}
|
|
11899
|
-
return steps2;
|
|
11900
|
-
};
|
|
11901
|
-
const beforeState = async () => {
|
|
11902
|
-
const steps2 = [];
|
|
11903
|
-
for (const host of deps.forgeHosts()) {
|
|
11904
|
-
try {
|
|
11905
|
-
const result = o.dryRun ? "erased" : deps.eraseForgeToken(host);
|
|
11906
|
-
if (result === "env") steps2.push({ step: "forge", target: host, action: "kept", detail: "the token is read from the environment; crew does not own it" });
|
|
11907
|
-
else if (result === "absent") steps2.push({ step: "forge", target: host, action: "absent" });
|
|
11908
|
-
else steps2.push({ step: "forge", target: host, action: mark2(o.dryRun) });
|
|
11909
|
-
} catch (e) {
|
|
11910
|
-
steps2.push({ step: "forge", target: host, action: "failed", detail: e.message });
|
|
11911
|
-
}
|
|
11912
|
-
}
|
|
11913
|
-
const { volumes, images } = deps.labeled();
|
|
11914
|
-
for (const v of volumes) steps2.push(attempt("cache", `volume ${v}`, () => {
|
|
11915
|
-
deps.removeVolume(v);
|
|
11916
|
-
}, o.dryRun));
|
|
11917
|
-
for (const i of images) steps2.push(attempt("cache", `image ${i}`, () => {
|
|
11918
|
-
deps.removeImage(i);
|
|
11919
|
-
}, o.dryRun));
|
|
11920
|
-
return steps2;
|
|
11921
|
-
};
|
|
11922
|
-
const steps = await runReset({ ...o, removeCheckouts: true, wipeState: true, afterScheduler, beforeState }, fx);
|
|
11923
|
-
const remaining = o.dryRun ? [] : deps.remaining();
|
|
11924
|
-
return { steps, remaining, ok: !resetIncomplete(steps) && remaining.length === 0 };
|
|
11925
|
-
}
|
|
11926
|
-
|
|
11927
|
-
// src/deprovision-host.ts
|
|
11928
|
-
import { spawnSync as spawnSync6 } from "child_process";
|
|
11929
|
-
import { existsSync as existsSync26, readdirSync as readdirSync12 } from "fs";
|
|
11930
|
-
|
|
11931
|
-
// src/interrupted.ts
|
|
11932
|
-
import { existsSync as existsSync24, readdirSync as readdirSync10, readFileSync as readFileSync24 } from "fs";
|
|
11933
|
-
import { join as join28 } from "path";
|
|
11934
|
-
var INTERRUPTED_MARKER = calloutHeader("NOTE", "Interrupted");
|
|
11935
|
-
function interruptedComment(b) {
|
|
11936
|
-
const tree = b.dirty === null ? "The worktree could not be checked." : b.dirty ? "The worktree has uncommitted changes \u2014 they are this seat's own, from that run." : "The worktree is clean.";
|
|
11937
|
-
return `${INTERRUPTED_MARKER}
|
|
11938
|
-
|
|
11939
|
-
The agent run on this ticket was cut short (${b.reason}) before it handed on${b.branch ? `; its branch is \`${b.branch}\`` : ""}. ${tree} The next run on this seat resumes from the worktree rather than starting over.`;
|
|
11940
|
-
}
|
|
11941
|
-
var handled = /* @__PURE__ */ new Set();
|
|
11942
|
-
var pidAlive = (pid) => {
|
|
11943
|
-
try {
|
|
11944
|
-
process.kill(pid, 0);
|
|
11945
|
-
return true;
|
|
11946
|
-
} catch (e) {
|
|
11947
|
-
return e.code === "EPERM";
|
|
11948
|
-
}
|
|
11949
|
-
};
|
|
11950
|
-
function runFinished(streamPath) {
|
|
11951
|
-
const path = `${streamPath}.meta.json`;
|
|
11952
|
-
if (!existsSync24(path)) return false;
|
|
11953
|
-
try {
|
|
11954
|
-
return JSON.parse(readFileSync24(path, "utf8")).running !== true;
|
|
11955
|
-
} catch {
|
|
11956
|
-
return true;
|
|
11957
|
-
}
|
|
11958
|
-
}
|
|
11959
|
-
function orphanedStreams(o) {
|
|
11960
|
-
const dir = join28(o.stateDir, "streams");
|
|
11961
|
-
if (!existsSync24(dir)) return [];
|
|
11962
|
-
const ownerAlive = o.isOwnerAlive ?? pidAlive;
|
|
11963
|
-
const out = [];
|
|
11964
|
-
for (const name of readdirSync10(dir)) {
|
|
11965
|
-
if (!name.endsWith(".jsonl") || name.endsWith(".events.jsonl")) continue;
|
|
11966
|
-
const streamPath = join28(dir, name);
|
|
11967
|
-
if (runFinished(streamPath)) continue;
|
|
11968
|
-
const sidecar = readRunSidecar(streamPath);
|
|
11969
|
-
if (!sidecar || sidecar.route !== o.routeLabel) continue;
|
|
11970
|
-
if (ownerAlive(sidecar.ownerPid)) continue;
|
|
11971
|
-
out.push({ streamPath, sidecar });
|
|
11972
|
-
}
|
|
11973
|
-
return out;
|
|
11974
|
-
}
|
|
11975
|
-
async function reconcileRuns(o) {
|
|
11976
|
-
let adopted = 0;
|
|
11977
|
-
let interrupted = 0;
|
|
11978
|
-
for (const { streamPath, sidecar } of orphanedStreams(o)) {
|
|
11979
|
-
if (handled.has(streamPath)) continue;
|
|
11980
|
-
handled.add(streamPath);
|
|
11981
|
-
const isAlive = o.isAlive ?? runIsAlive;
|
|
11982
|
-
const lock = isAlive(sidecar) ? o.state.acquireRun(sidecar.role, o.maxConcurrentAgents, o.roleConcurrency?.[sidecar.role] ?? 1) : null;
|
|
11983
|
-
const ticketLock = isAlive(sidecar) && sidecar.ticketRecordId ? o.state.acquireTicket(sidecar.ticketRecordId) : null;
|
|
11984
|
-
const work = (async () => {
|
|
11985
|
-
try {
|
|
11986
|
-
const result = await reattachRun(streamPath, sidecar, o.emit, {
|
|
11987
|
-
agentLogKey: await o.agentLogKey?.(sidecar.role).catch(() => void 0),
|
|
11988
|
-
isAlive: o.isAlive,
|
|
11989
|
-
pollMs: o.pollMs,
|
|
11990
|
-
onAdopted: () => o.emit.emit(`adopted ${sidecar.role} run (pid ${sidecar.pid}) left by a previous crew process`, { step: "agent", role: sidecar.role })
|
|
11991
|
-
});
|
|
11992
|
-
if (result.adopted) adopted++;
|
|
11993
|
-
else interrupted++;
|
|
11994
|
-
try {
|
|
11995
|
-
await o.afterRun?.(sidecar, result);
|
|
11996
|
-
} catch (e) {
|
|
11997
|
-
o.emit.warn(`could not follow up on ${sidecar.role} run ${streamPath}: ${e.message}`, { step: "agent" });
|
|
11998
|
-
}
|
|
11999
|
-
} catch (e) {
|
|
12000
|
-
o.emit.warn(`could not reattach ${sidecar.role} run ${streamPath}: ${e.message}`, { step: "agent" });
|
|
12001
|
-
} finally {
|
|
12002
|
-
if (lock?.ok) lock.release();
|
|
12003
|
-
if (ticketLock?.ok) ticketLock.release();
|
|
12004
|
-
}
|
|
12005
|
-
})();
|
|
12006
|
-
if (isAlive(sidecar)) {
|
|
12007
|
-
if (o.inFlight) {
|
|
12008
|
-
o.inFlight.add(work);
|
|
12009
|
-
void work.finally(() => o.inFlight.delete(work));
|
|
12010
|
-
}
|
|
12011
|
-
} else {
|
|
12012
|
-
await work;
|
|
12013
|
-
}
|
|
12014
|
-
}
|
|
12015
|
-
return { adopted, interrupted };
|
|
12016
|
-
}
|
|
12017
|
-
|
|
12018
|
-
// src/live-runs.ts
|
|
12019
|
-
import { existsSync as existsSync25, readdirSync as readdirSync11 } from "fs";
|
|
12020
|
-
import { join as join29 } from "path";
|
|
12021
|
-
function liveRuns(stateDir, isAlive = runIsAlive) {
|
|
12022
|
-
const dir = join29(stateDir, "streams");
|
|
12023
|
-
if (!existsSync25(dir)) return [];
|
|
12024
|
-
const out = [];
|
|
12025
|
-
for (const name of readdirSync11(dir)) {
|
|
12026
|
-
if (!name.endsWith(".jsonl") || name.endsWith(".events.jsonl")) continue;
|
|
12027
|
-
const streamPath = join29(dir, name);
|
|
12028
|
-
if (runFinished(streamPath)) continue;
|
|
12029
|
-
const sidecar = readRunSidecar(streamPath);
|
|
12030
|
-
if (!sidecar || !isAlive(sidecar)) continue;
|
|
12031
|
-
out.push({ ...sidecar, streamPath });
|
|
12032
|
-
}
|
|
12033
|
-
return out.sort((a, b) => a.spawnedAt.localeCompare(b.spawnedAt));
|
|
12034
|
-
}
|
|
12035
|
-
function siblingRuns(runs, ownTicketRecordId) {
|
|
12036
|
-
return runs.filter((r) => !ownTicketRecordId || r.ticketRecordId !== ownTicketRecordId);
|
|
12037
|
-
}
|
|
12038
|
-
async function settleRunEnd(o) {
|
|
12039
|
-
const { tracker, emit } = o;
|
|
12040
|
-
const all = o.runs ?? liveRuns(o.stateDir);
|
|
12041
|
-
const slug2 = routeSlug(o.route.route);
|
|
12042
|
-
const seatSiblings = siblingRuns(all.filter((r) => r.role === o.role && r.route === slug2), o.ownTicketRecordId);
|
|
12043
|
-
const shipSiblings = siblingRuns(all, o.ownTicketRecordId);
|
|
12044
|
-
const newest = (rs) => rs[rs.length - 1];
|
|
12045
|
-
if (o.memberId) {
|
|
12046
|
-
try {
|
|
12047
|
-
if (seatSiblings.length) await tracker.setCrewStatus(o.memberId, "working", newest(seatSiblings).ticketRecordId);
|
|
12048
|
-
else await tracker.setCrewStatus(o.memberId, "idle");
|
|
12049
|
-
} catch (e) {
|
|
12050
|
-
emit.warn(`could not clear crew status: ${e.message}`, { step: "agent" });
|
|
12301
|
+
} catch (e) {
|
|
12302
|
+
steps.push({ step: "credential", target, action: "failed", detail: e.message });
|
|
12303
|
+
}
|
|
12051
12304
|
}
|
|
12305
|
+
} catch (e) {
|
|
12306
|
+
steps.push({ step: "credential", target: "host sessions", action: "failed", detail: e.message });
|
|
12307
|
+
}
|
|
12308
|
+
if (o.removeCheckouts) {
|
|
12309
|
+
for (const dir of readClonedCheckouts(o.stateDir)) steps.push(removeCheckout(dir, o.dryRun, fx));
|
|
12310
|
+
}
|
|
12311
|
+
if (o.beforeState) steps.push(...await o.beforeState());
|
|
12312
|
+
steps.push(...o.wipeState ? wipeStateDir(o.stateDir, o.dryRun) : removeState(o.stateDir, o.dryRun));
|
|
12313
|
+
if (existsSync26(o.configFile)) {
|
|
12314
|
+
if (!o.dryRun) rmSync16(o.configFile, { force: true });
|
|
12315
|
+
steps.push({ step: "config", target: o.configFile, action: mark(o.dryRun) });
|
|
12316
|
+
} else {
|
|
12317
|
+
steps.push({ step: "config", target: o.configFile, action: "absent" });
|
|
12318
|
+
}
|
|
12319
|
+
return steps;
|
|
12320
|
+
}
|
|
12321
|
+
function removeCheckout(dir, dryRun, fx) {
|
|
12322
|
+
if (!existsSync26(dir)) return { step: "checkout", target: dir, action: "absent", detail: "already gone" };
|
|
12323
|
+
const { dirty, linkedWorktrees } = fx.inspectCheckout(dir);
|
|
12324
|
+
if (dirty.length) {
|
|
12325
|
+
return { step: "checkout", target: dir, action: "kept", detail: `has uncommitted changes (${dirty.length} path${dirty.length === 1 ? "" : "s"}: ${dirty.slice(0, 5).join(", ")}${dirty.length > 5 ? ", \u2026" : ""})` };
|
|
12326
|
+
}
|
|
12327
|
+
if (linkedWorktrees.length) {
|
|
12328
|
+
return { step: "checkout", target: dir, action: "kept", detail: `has linked worktrees (${linkedWorktrees.join(", ")}); remove them first` };
|
|
12052
12329
|
}
|
|
12330
|
+
if (!dryRun) rmSync16(dir, { recursive: true, force: true });
|
|
12331
|
+
return { step: "checkout", target: dir, action: mark(dryRun) };
|
|
12332
|
+
}
|
|
12333
|
+
function removeState(stateDir, dryRun) {
|
|
12334
|
+
const out = [];
|
|
12335
|
+
const targets = [join29(stateDir, "resolved"), join29(stateDir, "keys"), clonedCheckoutsFile(stateDir), join29(stateDir, "connect-session.json"), join29(stateDir, "reload-requested"), join29(stateDir, "daemon.started")];
|
|
12053
12336
|
try {
|
|
12054
|
-
|
|
12055
|
-
|
|
12056
|
-
await tracker.beatEngaged(o.shipName, n.routeName ?? n.route, n.ticketRecordId, n.spawnedAt);
|
|
12057
|
-
} else {
|
|
12058
|
-
await tracker.beatIdle(o.shipName);
|
|
12337
|
+
for (const name of readdirSync12(stateDir)) {
|
|
12338
|
+
if (name.startsWith(".") && statSync8(join29(stateDir, name)).isFile()) targets.push(join29(stateDir, name));
|
|
12059
12339
|
}
|
|
12060
|
-
} catch
|
|
12061
|
-
|
|
12340
|
+
} catch {
|
|
12341
|
+
}
|
|
12342
|
+
for (const t of targets) {
|
|
12343
|
+
if (!existsSync26(t)) continue;
|
|
12344
|
+
if (!dryRun) rmSync16(t, { recursive: true, force: true });
|
|
12345
|
+
out.push({ step: "state", target: t, action: mark(dryRun) });
|
|
12062
12346
|
}
|
|
12347
|
+
if (!out.length) out.push({ step: "state", target: stateDir, action: "absent", detail: "nothing to remove" });
|
|
12348
|
+
return out;
|
|
12063
12349
|
}
|
|
12064
|
-
function
|
|
12350
|
+
function wipeStateDir(stateDir, dryRun) {
|
|
12351
|
+
let names;
|
|
12065
12352
|
try {
|
|
12066
|
-
|
|
12353
|
+
names = readdirSync12(stateDir);
|
|
12067
12354
|
} catch {
|
|
12068
|
-
return
|
|
12355
|
+
return [{ step: "state", target: stateDir, action: "absent", detail: "no state directory" }];
|
|
12069
12356
|
}
|
|
12070
|
-
|
|
12071
|
-
const
|
|
12072
|
-
|
|
12073
|
-
const num = run3.ticket?.replace(/^\D+/, "") ?? "";
|
|
12074
|
-
return {
|
|
12075
|
-
role: run3.role,
|
|
12076
|
-
seat: ownRoute ? seatIds[run3.role] ?? null : null,
|
|
12077
|
-
ticket: run3.ticket,
|
|
12078
|
-
ticketRecordId: run3.ticketRecordId,
|
|
12079
|
-
repo: run3.repo ?? null,
|
|
12080
|
-
worktree: repoDir && num ? worktreeOf(repoDir, num) : null,
|
|
12081
|
-
stream: run3.streamPath,
|
|
12082
|
-
pid: run3.pid,
|
|
12083
|
-
startedAt: run3.spawnedAt,
|
|
12084
|
-
deadline: run3.deadline ?? null
|
|
12085
|
-
};
|
|
12357
|
+
if (!names.length) return [{ step: "state", target: stateDir, action: "absent", detail: "already empty" }];
|
|
12358
|
+
if (!dryRun) for (const n of names) rmSync16(join29(stateDir, n), { recursive: true, force: true });
|
|
12359
|
+
return [{ step: "state", target: stateDir, action: mark(dryRun), detail: `${names.length} entr${names.length === 1 ? "y" : "ies"}` }];
|
|
12086
12360
|
}
|
|
12087
|
-
|
|
12088
|
-
|
|
12089
|
-
|
|
12090
|
-
var DOCKER_LABEL_CACHE = "crew.kind=cache";
|
|
12091
|
-
var DOCKER_LABEL_REPO_LAYER = "crew.layer=repo";
|
|
12092
|
-
function docker(args) {
|
|
12093
|
-
const res = spawnSync6("docker", args, { encoding: "utf8" });
|
|
12094
|
-
if (res.error || res.status !== 0) throw new Error(`docker ${args[0]} ${args[1] ?? ""} failed: ${(res.stderr || String(res.error ?? "")).trim().split("\n").pop()}`);
|
|
12095
|
-
return res.stdout;
|
|
12361
|
+
function describeResetStep(s) {
|
|
12362
|
+
const verb = { removed: "removed", "would-remove": "would remove", absent: "nothing to remove", kept: "KEPT", failed: "FAILED" }[s.action];
|
|
12363
|
+
return `${s.step}: ${verb}: ${s.target}${s.detail ? ` (${s.detail})` : ""}`;
|
|
12096
12364
|
}
|
|
12097
|
-
|
|
12098
|
-
|
|
12099
|
-
if (!dockerAvailable()) return { volumes: [], images: [] };
|
|
12100
|
-
return {
|
|
12101
|
-
volumes: lines(docker(["volume", "ls", "-q", "--filter", `label=${DOCKER_LABEL_OWNER}`, "--filter", `label=${DOCKER_LABEL_CACHE}`])),
|
|
12102
|
-
images: lines(docker(["image", "ls", "-q", "--filter", `label=${DOCKER_LABEL_OWNER}`, "--filter", `label=${DOCKER_LABEL_REPO_LAYER}`]))
|
|
12103
|
-
};
|
|
12365
|
+
function resetIncomplete(steps) {
|
|
12366
|
+
return steps.some((s) => s.action === "failed" || s.action === "kept");
|
|
12104
12367
|
}
|
|
12105
|
-
|
|
12106
|
-
|
|
12107
|
-
|
|
12108
|
-
|
|
12109
|
-
const
|
|
12110
|
-
|
|
12111
|
-
|
|
12112
|
-
|
|
12113
|
-
|
|
12114
|
-
|
|
12115
|
-
|
|
12368
|
+
|
|
12369
|
+
// src/deprovision.ts
|
|
12370
|
+
var mark2 = (dryRun) => dryRun ? "would-remove" : "removed";
|
|
12371
|
+
async function runDeprovision(o, fx, deps) {
|
|
12372
|
+
const attempt = (step, target, fn, dry) => {
|
|
12373
|
+
try {
|
|
12374
|
+
if (!dry) fn();
|
|
12375
|
+
return { step, target, action: mark2(dry) };
|
|
12376
|
+
} catch (e) {
|
|
12377
|
+
return { step, target, action: "failed", detail: e.message };
|
|
12378
|
+
}
|
|
12116
12379
|
};
|
|
12117
|
-
|
|
12118
|
-
|
|
12119
|
-
|
|
12120
|
-
|
|
12121
|
-
|
|
12122
|
-
step: "
|
|
12123
|
-
|
|
12124
|
-
data: { event: "run_killed", reason: "deprovision", pid: run3.pid }
|
|
12125
|
-
});
|
|
12126
|
-
},
|
|
12127
|
-
async handBack(run3) {
|
|
12128
|
-
const route2 = routeFor(run3.route);
|
|
12129
|
-
if (!run3.ticketRecordId || !route2) return;
|
|
12130
|
-
const memberId = route2.resolved?.seats[run3.role];
|
|
12131
|
-
if (!memberId) return;
|
|
12132
|
-
const identity = await resolveSeatKey(route2, run3.role, getStore);
|
|
12133
|
-
const tracker = new Tracker(route2, cfg.ship, { apiKey: identity.apiKey });
|
|
12134
|
-
const t = await tracker.ticket(run3.ticketRecordId);
|
|
12135
|
-
const target = repoTargetFor(route2, t.repo_id);
|
|
12136
|
-
const num = String(t[tracker.contract.columns.key] ?? "").replace(/^\D+/, "");
|
|
12137
|
-
const wt = target && num ? worktreeForNumber(target.dir, num) : null;
|
|
12138
|
-
let dirty = null;
|
|
12139
|
-
try {
|
|
12140
|
-
if (wt) dirty = status(wt.path).length > 0;
|
|
12141
|
-
} catch {
|
|
12380
|
+
const afterScheduler = async () => {
|
|
12381
|
+
const steps2 = [];
|
|
12382
|
+
for (const run3 of deps.liveRuns()) {
|
|
12383
|
+
const target = `${run3.role} run on ${run3.route} (pid ${run3.pid})`;
|
|
12384
|
+
if (o.dryRun) {
|
|
12385
|
+
steps2.push({ step: "run", target, action: "would-remove" });
|
|
12386
|
+
continue;
|
|
12142
12387
|
}
|
|
12143
|
-
|
|
12144
|
-
|
|
12145
|
-
|
|
12146
|
-
|
|
12147
|
-
|
|
12148
|
-
|
|
12149
|
-
sandboxes,
|
|
12150
|
-
removeSandbox: (name) => {
|
|
12151
|
-
stopContainer(name);
|
|
12152
|
-
},
|
|
12153
|
-
async clearShipRows() {
|
|
12154
|
-
const out = [];
|
|
12155
|
-
for (const r of cfg.routes.filter((x) => x.resolved?.shipsModelId)) {
|
|
12156
|
-
try {
|
|
12157
|
-
const found = await new Tracker(r, cfg.ship).clearForRelease(cfg.ship.name);
|
|
12158
|
-
out.push({ route: r.route, ok: true, detail: found ? "cleared engaged, tunnel_status, relay_url" : "no Ships row for this ship" });
|
|
12159
|
-
} catch (e) {
|
|
12160
|
-
out.push({ route: r.route, ok: false, detail: e.message });
|
|
12161
|
-
}
|
|
12388
|
+
try {
|
|
12389
|
+
await deps.killRun(run3);
|
|
12390
|
+
deps.recordKill(run3);
|
|
12391
|
+
} catch (e) {
|
|
12392
|
+
steps2.push({ step: "run", target, action: "failed", detail: e.message });
|
|
12393
|
+
continue;
|
|
12162
12394
|
}
|
|
12163
|
-
return out;
|
|
12164
|
-
},
|
|
12165
|
-
forgeHosts,
|
|
12166
|
-
eraseForgeToken: (host) => eraseForgeToken(cfg.ship.forges, host, stateDir),
|
|
12167
|
-
labeled: labeledDocker,
|
|
12168
|
-
removeVolume: (name) => {
|
|
12169
|
-
docker(["volume", "rm", "-f", name]);
|
|
12170
|
-
},
|
|
12171
|
-
removeImage: (name) => {
|
|
12172
|
-
docker(["image", "rm", "-f", name]);
|
|
12173
|
-
},
|
|
12174
|
-
remaining() {
|
|
12175
|
-
const left = [];
|
|
12176
|
-
if (existsSync26(cfg.configFile)) left.push(`config ${cfg.configFile}`);
|
|
12177
|
-
for (const h of forgeHosts()) if (hasForgeToken(cfg.ship.forges, h, stateDir)) left.push(`forge token for ${h}`);
|
|
12178
12395
|
try {
|
|
12179
|
-
|
|
12180
|
-
|
|
12396
|
+
await deps.handBack(run3);
|
|
12397
|
+
steps2.push({ step: "run", target, action: "removed", detail: "killed" });
|
|
12398
|
+
} catch (e) {
|
|
12399
|
+
steps2.push({ step: "run", target, action: "removed", detail: `killed; could not tell its ticket (${e.message})` });
|
|
12181
12400
|
}
|
|
12182
|
-
|
|
12401
|
+
}
|
|
12402
|
+
for (const name of deps.sandboxes()) steps2.push(attempt("sandbox", name, () => {
|
|
12403
|
+
deps.removeSandbox(name);
|
|
12404
|
+
}, o.dryRun));
|
|
12405
|
+
if (!o.dryRun) {
|
|
12406
|
+
for (const r of await deps.clearShipRows()) {
|
|
12407
|
+
steps2.push({ step: "ship-row", target: r.route, action: r.ok ? "removed" : "failed", detail: r.detail });
|
|
12408
|
+
}
|
|
12409
|
+
}
|
|
12410
|
+
return steps2;
|
|
12411
|
+
};
|
|
12412
|
+
const beforeState = async () => {
|
|
12413
|
+
const steps2 = [];
|
|
12414
|
+
for (const host of deps.forgeHosts()) {
|
|
12183
12415
|
try {
|
|
12184
|
-
const
|
|
12185
|
-
|
|
12186
|
-
|
|
12416
|
+
const result = o.dryRun ? "erased" : deps.eraseForgeToken(host);
|
|
12417
|
+
if (result === "env") steps2.push({ step: "forge", target: host, action: "kept", detail: "the token is read from the environment; crew does not own it" });
|
|
12418
|
+
else if (result === "absent") steps2.push({ step: "forge", target: host, action: "absent" });
|
|
12419
|
+
else steps2.push({ step: "forge", target: host, action: mark2(o.dryRun) });
|
|
12187
12420
|
} catch (e) {
|
|
12188
|
-
|
|
12421
|
+
steps2.push({ step: "forge", target: host, action: "failed", detail: e.message });
|
|
12189
12422
|
}
|
|
12190
|
-
for (const u of unitPaths()) left.push(`unit ${u}`);
|
|
12191
|
-
if (loadedCrewLabels().length) left.push(`loaded units ${loadedCrewLabels().join(", ")}`);
|
|
12192
|
-
return left;
|
|
12193
12423
|
}
|
|
12424
|
+
const { volumes, images } = deps.labeled();
|
|
12425
|
+
for (const v of volumes) steps2.push(attempt("cache", `volume ${v}`, () => {
|
|
12426
|
+
deps.removeVolume(v);
|
|
12427
|
+
}, o.dryRun));
|
|
12428
|
+
for (const i of images) steps2.push(attempt("cache", `image ${i}`, () => {
|
|
12429
|
+
deps.removeImage(i);
|
|
12430
|
+
}, o.dryRun));
|
|
12431
|
+
return steps2;
|
|
12194
12432
|
};
|
|
12433
|
+
const steps = await runReset({ ...o, removeCheckouts: true, wipeState: true, afterScheduler, beforeState }, fx);
|
|
12434
|
+
const remaining = o.dryRun ? [] : deps.remaining();
|
|
12435
|
+
return { steps, remaining, ok: !resetIncomplete(steps) && remaining.length === 0 };
|
|
12195
12436
|
}
|
|
12196
12437
|
|
|
12197
12438
|
// src/pool-member.ts
|
|
@@ -15858,11 +16099,11 @@ ${h.prFailure.tail}
|
|
|
15858
16099
|
emit.warn(`could not move to ${reviewing}: ${e.message}`, { ticket: t.issue_id });
|
|
15859
16100
|
}
|
|
15860
16101
|
}
|
|
15861
|
-
for (const
|
|
15862
|
-
const t =
|
|
15863
|
-
const body = `The branch is no longer what was verified: QA verified ${
|
|
16102
|
+
for (const q2 of outcome.requeues) {
|
|
16103
|
+
const t = q2.ticket;
|
|
16104
|
+
const body = `The branch is no longer what was verified: QA verified ${q2.verified.slice(0, 8)}, the branch is now at ${q2.head.slice(0, 8)}. Returned to ${contract.statuses.handoff} for QA to check what changed.`;
|
|
15864
16105
|
if (dryRun) {
|
|
15865
|
-
emit.emit(`would return to ${contract.statuses.handoff} \u2014 branch moved past ${
|
|
16106
|
+
emit.emit(`would return to ${contract.statuses.handoff} \u2014 branch moved past ${q2.verified.slice(0, 8)}`, { ticket: t.issue_id });
|
|
15866
16107
|
continue;
|
|
15867
16108
|
}
|
|
15868
16109
|
try {
|
|
@@ -15870,11 +16111,11 @@ ${h.prFailure.tail}
|
|
|
15870
16111
|
await tracker.postEvent(t.id, body, seat, "requeued");
|
|
15871
16112
|
await tracker.postEvent(
|
|
15872
16113
|
t.id,
|
|
15873
|
-
pushedEventBody(
|
|
16114
|
+
pushedEventBody(q2.head, remote, q2.branch),
|
|
15874
16115
|
seat,
|
|
15875
16116
|
"pushed"
|
|
15876
16117
|
);
|
|
15877
|
-
emit.emit(`returned to ${contract.statuses.handoff} \u2014 branch moved past ${
|
|
16118
|
+
emit.emit(`returned to ${contract.statuses.handoff} \u2014 branch moved past ${q2.verified.slice(0, 8)}`, { ticket: t.issue_id });
|
|
15878
16119
|
} catch (e) {
|
|
15879
16120
|
emit.warn(`could not return to ${contract.statuses.handoff}: ${e.message}`, { ticket: t.issue_id });
|
|
15880
16121
|
}
|
|
@@ -17071,8 +17312,8 @@ async function buildWhoami(routes, deps = {}) {
|
|
|
17071
17312
|
const credential = credentialSource(route2);
|
|
17072
17313
|
try {
|
|
17073
17314
|
const key = resolveApiKey(route2);
|
|
17074
|
-
const
|
|
17075
|
-
const res = await doFetch(`${route2.baseUrl.replace(/\/+$/, "")}/api/auth/me${
|
|
17315
|
+
const q2 = route2.resolved ? `?workspaceId=${encodeURIComponent(route2.resolved.workspaceId)}` : "";
|
|
17316
|
+
const res = await doFetch(`${route2.baseUrl.replace(/\/+$/, "")}/api/auth/me${q2}`, {
|
|
17076
17317
|
headers: { Authorization: `Bearer ${key}`, "User-Agent": deps.userAgent ?? "Mozilla/5.0 TablationCrewAgent/1.0" }
|
|
17077
17318
|
});
|
|
17078
17319
|
if (res.status === 401 || res.status === 403) {
|
|
@@ -17417,9 +17658,9 @@ function startWatch(o) {
|
|
|
17417
17658
|
|
|
17418
17659
|
// src/inbox.ts
|
|
17419
17660
|
function matchMembers(rows, query) {
|
|
17420
|
-
const
|
|
17421
|
-
const asEmail =
|
|
17422
|
-
return rows.filter((r) => asEmail ? (r.email ?? "").toLowerCase() ===
|
|
17661
|
+
const q2 = query.trim().toLowerCase();
|
|
17662
|
+
const asEmail = q2.includes("@");
|
|
17663
|
+
return rows.filter((r) => asEmail ? (r.email ?? "").toLowerCase() === q2 : (r.name ?? "").toLowerCase().includes(q2)).map((r) => ({ id: r.id, name: r.name ?? "", email: r.email ?? "", by: asEmail ? "email" : "name" }));
|
|
17423
17664
|
}
|
|
17424
17665
|
async function gatherInbox(routes, ship, opts = {}) {
|
|
17425
17666
|
const items = [];
|
|
@@ -20507,6 +20748,13 @@ async function runOnePassUnguarded(o) {
|
|
|
20507
20748
|
const spawn11 = o.spawnRoleAgent ?? runRoleAgent;
|
|
20508
20749
|
const breaker = o.spawnBreaker ?? new SpawnBreaker();
|
|
20509
20750
|
const ctx = { route: o.route, ship: o.ship, state: o.state, emit, getSessionStore: o.getSessionStore };
|
|
20751
|
+
if (o.sandboxCache) {
|
|
20752
|
+
try {
|
|
20753
|
+
await o.sandboxCache();
|
|
20754
|
+
} catch (e) {
|
|
20755
|
+
emit.warn(`sandbox cache refresh failed: ${e instanceof Error ? e.message : String(e)}`);
|
|
20756
|
+
}
|
|
20757
|
+
}
|
|
20510
20758
|
if (o.configFile) await reconcileRouteShipName({ route: o.route, ship: o.ship, configFile: o.configFile, emit });
|
|
20511
20759
|
try {
|
|
20512
20760
|
await (o.reconcile ?? reconcileForRoute)(ctx, o);
|
|
@@ -21010,7 +21258,7 @@ import {
|
|
|
21010
21258
|
import { createConnection } from "net";
|
|
21011
21259
|
import { createServer } from "http";
|
|
21012
21260
|
import { basename as basename13, dirname as dirname23, join as join46 } from "path";
|
|
21013
|
-
import { execFile as
|
|
21261
|
+
import { execFile as execFile3, spawn as spawn10 } from "child_process";
|
|
21014
21262
|
|
|
21015
21263
|
// src/periscope-control.ts
|
|
21016
21264
|
import { spawn as spawn9 } from "child_process";
|
|
@@ -21097,23 +21345,23 @@ async function execute(h, args, streaming, after) {
|
|
|
21097
21345
|
};
|
|
21098
21346
|
}
|
|
21099
21347
|
var isPeriscopePath = (p) => p === "/periscopes" || p.startsWith("/periscopes/");
|
|
21100
|
-
async function handlePeriscopes(h,
|
|
21348
|
+
async function handlePeriscopes(h, q2) {
|
|
21101
21349
|
if (!h || !h.routes.some((r) => r.hostPeriscopes)) return err(404, "not found");
|
|
21102
|
-
if (!
|
|
21103
|
-
const down = /^\/periscopes\/([^/]+)\/down$/.exec(
|
|
21104
|
-
const isUp =
|
|
21105
|
-
const isList =
|
|
21106
|
-
if (
|
|
21107
|
-
if (down &&
|
|
21108
|
-
if (
|
|
21350
|
+
if (!q2.token) return err(401, "a bearer token is required");
|
|
21351
|
+
const down = /^\/periscopes\/([^/]+)\/down$/.exec(q2.path);
|
|
21352
|
+
const isUp = q2.path === "/periscopes" && q2.method === "POST";
|
|
21353
|
+
const isList = q2.path === "/periscopes" && q2.method === "GET";
|
|
21354
|
+
if (q2.path === "/periscopes" && !isUp && !isList) return err(405, "method not allowed");
|
|
21355
|
+
if (down && q2.method !== "POST") return err(405, "method not allowed");
|
|
21356
|
+
if (q2.path !== "/periscopes" && !down) return err(404, "not found");
|
|
21109
21357
|
const member = async (workspaceId) => {
|
|
21110
|
-
const v = await h.verifyIn(
|
|
21358
|
+
const v = await h.verifyIn(q2.token, workspaceId);
|
|
21111
21359
|
if (v === "ok") return void 0;
|
|
21112
21360
|
if (v === "unavailable") return err(503, "could not verify the key right now");
|
|
21113
21361
|
return err(403, "this key does not belong to the workspace of this route");
|
|
21114
21362
|
};
|
|
21115
21363
|
if (isList) {
|
|
21116
|
-
const named =
|
|
21364
|
+
const named = q2.query.get("route") ?? void 0;
|
|
21117
21365
|
const candidates2 = named === void 0 ? h.routes.filter((r) => r.hostPeriscopes) : [pickRoute(h, named)];
|
|
21118
21366
|
const rows = [];
|
|
21119
21367
|
const hosting = [];
|
|
@@ -21145,7 +21393,7 @@ async function handlePeriscopes(h, q) {
|
|
|
21145
21393
|
}
|
|
21146
21394
|
return reply(200, { periscopes: rows, hosting });
|
|
21147
21395
|
}
|
|
21148
|
-
const b =
|
|
21396
|
+
const b = q2.body;
|
|
21149
21397
|
if (!b) return err(400, "body must be a small JSON object");
|
|
21150
21398
|
const routeName = plainText(b.route, "route", false);
|
|
21151
21399
|
if (isErr(routeName)) return err(400, routeName.error);
|
|
@@ -21166,7 +21414,7 @@ async function handlePeriscopes(h, q) {
|
|
|
21166
21414
|
`--${b.outcome}`,
|
|
21167
21415
|
...b.allowEmpty === true ? ["--allow-empty"] : [],
|
|
21168
21416
|
"--json"
|
|
21169
|
-
],
|
|
21417
|
+
], q2.wantsStream, h.mcp ? async (done) => {
|
|
21170
21418
|
await h.mcp.onDown(picked, done);
|
|
21171
21419
|
return done;
|
|
21172
21420
|
} : void 0);
|
|
@@ -21200,7 +21448,7 @@ async function handlePeriscopes(h, q) {
|
|
|
21200
21448
|
...h.mcp ? ["--serve-mcp"] : [],
|
|
21201
21449
|
"--no-open",
|
|
21202
21450
|
"--json"
|
|
21203
|
-
],
|
|
21451
|
+
], q2.wantsStream, h.mcp ? async (done) => ({ ...done, ...await h.mcp.onUp(picked, done) }) : void 0);
|
|
21204
21452
|
}
|
|
21205
21453
|
function runCrew(args, onLine, timeoutMs = 20 * 6e4) {
|
|
21206
21454
|
return new Promise((resolve7) => {
|
|
@@ -21475,11 +21723,11 @@ async function readJsonBody(req) {
|
|
|
21475
21723
|
return null;
|
|
21476
21724
|
}
|
|
21477
21725
|
}
|
|
21478
|
-
async function tailReply(path,
|
|
21479
|
-
const after =
|
|
21480
|
-
const tail3 =
|
|
21726
|
+
async function tailReply(path, q2, rotated, abort) {
|
|
21727
|
+
const after = q2.get("after") ?? void 0;
|
|
21728
|
+
const tail3 = q2.has("tail") ? Number(q2.get("tail")) : void 0;
|
|
21481
21729
|
if (tail3 !== void 0 && !(Number.isInteger(tail3) && tail3 >= 0 && tail3 <= 1e3)) return err2(400, "tail must be an integer from 0 to 1000");
|
|
21482
|
-
const wait =
|
|
21730
|
+
const wait = q2.has("wait") ? Number(q2.get("wait")) : 0;
|
|
21483
21731
|
if (!(Number.isFinite(wait) && wait >= 0)) return err2(400, "wait must be a number of seconds");
|
|
21484
21732
|
if (after !== void 0 && !decodeCursor(after)) return err2(400, "after is not a cursor this API issued");
|
|
21485
21733
|
const deadline = Date.now() + Math.min(wait, MAX_WAIT_SECONDS) * 1e3;
|
|
@@ -21662,7 +21910,7 @@ function schedulerHandler(control, run3 = runDetached) {
|
|
|
21662
21910
|
}
|
|
21663
21911
|
function crewJson(args, timeoutMs = 6e4) {
|
|
21664
21912
|
return new Promise((resolve7) => {
|
|
21665
|
-
|
|
21913
|
+
execFile3(process.execPath, [process.argv[1] ?? "", ...args], { timeout: timeoutMs, maxBuffer: 16 * 1024 * 1024 }, (_e, stdout) => {
|
|
21666
21914
|
try {
|
|
21667
21915
|
const v = JSON.parse(String(stdout));
|
|
21668
21916
|
resolve7(v && typeof v === "object" && !Array.isArray(v) ? v : null);
|
|
@@ -22138,12 +22386,12 @@ function terminalPrompter() {
|
|
|
22138
22386
|
process.stderr.write(`${line}
|
|
22139
22387
|
`);
|
|
22140
22388
|
},
|
|
22141
|
-
ask: async (
|
|
22142
|
-
const a = (await rl.question(fallback ? `${
|
|
22389
|
+
ask: async (q2, fallback) => {
|
|
22390
|
+
const a = (await rl.question(fallback ? `${q2} [${fallback}]: ` : `${q2}: `)).trim();
|
|
22143
22391
|
return a || fallback || "";
|
|
22144
22392
|
},
|
|
22145
|
-
confirm: async (
|
|
22146
|
-
const a = (await rl.question(`${
|
|
22393
|
+
confirm: async (q2, fallback) => {
|
|
22394
|
+
const a = (await rl.question(`${q2} [${fallback ? "Y/n" : "y/N"}] `)).trim();
|
|
22147
22395
|
return a ? /^y/i.test(a) : fallback;
|
|
22148
22396
|
}
|
|
22149
22397
|
};
|
|
@@ -22268,6 +22516,7 @@ function usage(problem, requested = false) {
|
|
|
22268
22516
|
crew shell-init [--shell zsh|bash|fish] print the periscope shell function (install: eval "$(crew shell-init)")
|
|
22269
22517
|
crew worktree [route] NNN cut (or refresh) this run's worktree for a ticket from the remote; prints its path
|
|
22270
22518
|
crew unassign [route] NNN hand back a session's ticket \u2014 clears assignee, next cycle picks it up
|
|
22519
|
+
crew sandbox cache [--prune|--reset] [--json] list the sandbox cache volume's mirrors; --prune drops unserved repositories, --reset recreates the volume
|
|
22271
22520
|
crew sync [route] fast-forward the checkout and its worktrees from the remote
|
|
22272
22521
|
crew pause|resume [route] [R|release] pause everything, one role, or releases only (merge/deploy/release; passengers keeps running).
|
|
22273
22522
|
resume with no argument clears only the whole-crew pause; a role or release pause needs its own resume
|
|
@@ -22584,6 +22833,31 @@ if (command === "passenger-sync-daemon") {
|
|
|
22584
22833
|
const out = refreshBaseBranch(r.dir, remote, base, true);
|
|
22585
22834
|
const unsafe = baseBranchUnsafe(out);
|
|
22586
22835
|
return { action: out.action, unsafe, detail: unsafe ? describeUnsafeBase(out, remote, base) : out.detail };
|
|
22836
|
+
}, sandboxCacheRepos = function(c, ship) {
|
|
22837
|
+
const out = [];
|
|
22838
|
+
for (const t of reposOf(c)) {
|
|
22839
|
+
const origin = originUrlOf(t.dir, "origin");
|
|
22840
|
+
const repo = origin ? parseRemote(origin) : null;
|
|
22841
|
+
if (!repo) continue;
|
|
22842
|
+
let token = null;
|
|
22843
|
+
try {
|
|
22844
|
+
token = readForgeToken(ship.forges, repo.host, ship.stateDir);
|
|
22845
|
+
} catch {
|
|
22846
|
+
}
|
|
22847
|
+
out.push({ ...repo, dir: t.dir, token, username: forgeFor(ship.forges, repo.host)?.entry.username });
|
|
22848
|
+
}
|
|
22849
|
+
return out;
|
|
22850
|
+
}, buildSandboxCachePass = function(c, ship, state2, emit2) {
|
|
22851
|
+
const settings = cacheSettingsOf(ship.sandbox);
|
|
22852
|
+
if (!settings) return void 0;
|
|
22853
|
+
return makeCachePass({
|
|
22854
|
+
cache: new SandboxCache(settings),
|
|
22855
|
+
volume: settings.volume,
|
|
22856
|
+
repos: () => sandboxCacheRepos(c, ship),
|
|
22857
|
+
lock: (dir) => acquireCheckoutLock(dir),
|
|
22858
|
+
fail: (key, message) => raiseShipAttention({ state: state2, emit: emit2, route: c, ship }, { kind: "sandbox_cache", key, message, since: (/* @__PURE__ */ new Date()).toISOString() }).then(() => void 0),
|
|
22859
|
+
ok: (key) => clearShipAttention(state2, key)
|
|
22860
|
+
});
|
|
22587
22861
|
}, sandboxTickDeps = function(emit2, c) {
|
|
22588
22862
|
return {
|
|
22589
22863
|
stateDir: cfg.ship.stateDir,
|
|
@@ -22597,7 +22871,7 @@ if (command === "passenger-sync-daemon") {
|
|
|
22597
22871
|
log: (m) => emit2.emit(m)
|
|
22598
22872
|
};
|
|
22599
22873
|
};
|
|
22600
|
-
requireArmed2 = requireArmed, parseTicketNumber2 = parseTicketNumber, routeRelayUrl2 = routeRelayUrl, passengerApiKeys2 = passengerApiKeys, endpointApiFor2 = endpointApiFor, periscopeEndpointDeps2 = periscopeEndpointDeps, periscopeMcpDeps2 = periscopeMcpDeps, endpointReconcileDeps2 = endpointReconcileDeps, releaseRefused2 = releaseRefused, doctorMachineId2 = doctorMachineId, baseHealth2 = baseHealth, sandboxTickDeps2 = sandboxTickDeps;
|
|
22874
|
+
requireArmed2 = requireArmed, parseTicketNumber2 = parseTicketNumber, routeRelayUrl2 = routeRelayUrl, passengerApiKeys2 = passengerApiKeys, endpointApiFor2 = endpointApiFor, periscopeEndpointDeps2 = periscopeEndpointDeps, periscopeMcpDeps2 = periscopeMcpDeps, endpointReconcileDeps2 = endpointReconcileDeps, releaseRefused2 = releaseRefused, doctorMachineId2 = doctorMachineId, baseHealth2 = baseHealth, sandboxCacheRepos2 = sandboxCacheRepos, buildSandboxCachePass2 = buildSandboxCachePass, sandboxTickDeps2 = sandboxTickDeps;
|
|
22601
22875
|
let cfg;
|
|
22602
22876
|
let firstRun;
|
|
22603
22877
|
let firstRunFromFlags = false;
|
|
@@ -22702,7 +22976,7 @@ if (command === "passenger-sync-daemon") {
|
|
|
22702
22976
|
const DAEMON_SUBCOMMANDS = /* @__PURE__ */ new Set(["start", "stop", "status", "restart"]);
|
|
22703
22977
|
const worktreeArgs = command === "worktree" ? splitWorktreeArgs(positional, cfg.routes.map((r) => r.route)) : void 0;
|
|
22704
22978
|
const named = worktreeArgs ? worktreeArgs.route : command === "daemon" && DAEMON_SUBCOMMANDS.has(positional[1] ?? "") ? positional[2] : positional[1];
|
|
22705
|
-
const fleetWide = command === "inbox" || command === "connect" || command === "disconnect" || command === "deprovision" || command === "agents" || command === "skills" || command === "logbook" || command === "repos" || command === "ship" || command === "projects" || command === "periscope" || command === "
|
|
22979
|
+
const fleetWide = command === "inbox" || command === "connect" || command === "disconnect" || command === "deprovision" || command === "agents" || command === "skills" || command === "logbook" || command === "repos" || command === "ship" || command === "projects" || command === "periscope" || command === "whoami" || command === "sandbox" || releaseFleetWide || command === "pause" || command === "resume" || command === "reload" || FLEET_CAPABLE.has(command) && !named && cfg.routes.length > 1 || worktreeArgs !== void 0 && !named && cfg.routes.length > 1;
|
|
22706
22980
|
let route2;
|
|
22707
22981
|
try {
|
|
22708
22982
|
route2 = fleetWide ? cfg.routes[0] : findRoute(cfg, named);
|
|
@@ -24225,6 +24499,7 @@ ${cu.tail.trimEnd()}
|
|
|
24225
24499
|
});
|
|
24226
24500
|
if (controlServer) emit.emit(`control API listening on ${controlServer.path}`);
|
|
24227
24501
|
const inFlight = /* @__PURE__ */ new Set();
|
|
24502
|
+
const sandboxCachePass = cfg.ship.runner === "sandbox" ? buildSandboxCachePass(route2, cfg.ship, state, emit) : void 0;
|
|
24228
24503
|
let sandboxBuilding = false;
|
|
24229
24504
|
const stopReason = await runDaemonLoop({
|
|
24230
24505
|
inFlight,
|
|
@@ -24233,6 +24508,7 @@ ${cu.tail.trimEnd()}
|
|
|
24233
24508
|
),
|
|
24234
24509
|
runPass: () => runOnePass({
|
|
24235
24510
|
inFlight,
|
|
24511
|
+
sandboxCache: sandboxCachePass,
|
|
24236
24512
|
route: route2,
|
|
24237
24513
|
ship: cfg.ship,
|
|
24238
24514
|
state,
|
|
@@ -26943,6 +27219,45 @@ ${where.join("\n")}`);
|
|
|
26943
27219
|
if (results.some((r) => !r.ok)) process.exitCode = 1;
|
|
26944
27220
|
break;
|
|
26945
27221
|
}
|
|
27222
|
+
case "sandbox": {
|
|
27223
|
+
const settings = cacheSettingsOf(cfg.ship.sandbox);
|
|
27224
|
+
if (positional[1] !== "cache") {
|
|
27225
|
+
process.stderr.write("crew sandbox cache [--prune|--reset] [--json]\n");
|
|
27226
|
+
process.exit(2);
|
|
27227
|
+
}
|
|
27228
|
+
if (!settings) {
|
|
27229
|
+
process.stderr.write("crew sandbox cache: ship.sandbox.cacheVolume is not set, so there is no cache volume\n");
|
|
27230
|
+
process.exit(2);
|
|
27231
|
+
}
|
|
27232
|
+
const cache = new SandboxCache(settings);
|
|
27233
|
+
const served = cfg.routes.flatMap((r) => sandboxCacheRepos(r, cfg.ship));
|
|
27234
|
+
try {
|
|
27235
|
+
if (flag("reset")) {
|
|
27236
|
+
await cache.reset();
|
|
27237
|
+
process.stdout.write(`recreated volume ${settings.volume}; the next daemon pass refreshes every mirror
|
|
27238
|
+
`);
|
|
27239
|
+
break;
|
|
27240
|
+
}
|
|
27241
|
+
const dropped = flag("prune") ? await cache.prune(served) : [];
|
|
27242
|
+
const mirrors = await cache.list();
|
|
27243
|
+
if (flag("json")) {
|
|
27244
|
+
process.stdout.write(`${JSON.stringify({ volume: settings.volume, mirrors, dropped }, null, 2)}
|
|
27245
|
+
`);
|
|
27246
|
+
} else {
|
|
27247
|
+
for (const d of dropped) process.stdout.write(`dropped ${d.host}/${d.path}
|
|
27248
|
+
`);
|
|
27249
|
+
if (mirrors.length === 0) process.stdout.write(`volume ${settings.volume}: no mirrors yet
|
|
27250
|
+
`);
|
|
27251
|
+
for (const m of mirrors) process.stdout.write(`${m.repo.host}/${m.repo.path} last refresh ${m.lastRefresh ?? "never"}
|
|
27252
|
+
`);
|
|
27253
|
+
}
|
|
27254
|
+
} catch (e) {
|
|
27255
|
+
process.stderr.write(`crew sandbox cache: ${e instanceof Error ? e.message : String(e)}
|
|
27256
|
+
`);
|
|
27257
|
+
process.exitCode = 1;
|
|
27258
|
+
}
|
|
27259
|
+
break;
|
|
27260
|
+
}
|
|
26946
27261
|
case "whoami": {
|
|
26947
27262
|
const only = value("route") ?? positional[1];
|
|
26948
27263
|
const wanted = only ? cfg.routes.filter((r) => r.route === only) : cfg.routes;
|
|
@@ -27824,4 +28139,6 @@ var endpointReconcileDeps2;
|
|
|
27824
28139
|
var releaseRefused2;
|
|
27825
28140
|
var doctorMachineId2;
|
|
27826
28141
|
var baseHealth2;
|
|
28142
|
+
var sandboxCacheRepos2;
|
|
28143
|
+
var buildSandboxCachePass2;
|
|
27827
28144
|
var sandboxTickDeps2;
|