@tablation/crew 0.96.0 → 0.98.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 +253 -46
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -684,11 +684,17 @@ var claudePair = {
|
|
|
684
684
|
},
|
|
685
685
|
removePair: (o) => removeClaudeMcp({ bin: o.bin ?? "claude", dryRun: o.dryRun })
|
|
686
686
|
};
|
|
687
|
+
var inferenceBaseUrl = (baseUrl, workspaceId) => `${baseUrl.replace(/\/+$/, "")}/api/workspaces/${workspaceId}/inference`;
|
|
688
|
+
function workspaceSettings(baseUrl) {
|
|
689
|
+
const blank = BILLING_VARS_TO_UNSET.filter((v) => v !== "ANTHROPIC_AUTH_TOKEN");
|
|
690
|
+
return JSON.stringify({ env: { ...Object.fromEntries(blank.map((v) => [v, ""])), ANTHROPIC_BASE_URL: baseUrl } });
|
|
691
|
+
}
|
|
687
692
|
function subscriptionSettings() {
|
|
688
693
|
return JSON.stringify({ env: Object.fromEntries(BILLING_VARS_TO_UNSET.map((v) => [v, ""])) });
|
|
689
694
|
}
|
|
690
695
|
function describeClaudeAuth(spec, env = process.env, home = homedir3()) {
|
|
691
696
|
if (spec.auth === "subscription") return "subscription (settings override)";
|
|
697
|
+
if (spec.auth === "workspace") return `workspace${spec.inference ? ` (${new URL(spec.inference.baseUrl).host})` : " (endpoint resolved per route at run time)"}`;
|
|
692
698
|
let fromSettings;
|
|
693
699
|
try {
|
|
694
700
|
const parsed = JSON.parse(readFileSync6(join6(home, ".claude", "settings.json"), "utf8"));
|
|
@@ -702,6 +708,9 @@ function describeClaudeAuth(spec, env = process.env, home = homedir3()) {
|
|
|
702
708
|
const shell = BILLING_VARS_TO_UNSET.find((v) => v.startsWith("ANTHROPIC_API") && env[v]);
|
|
703
709
|
return shell ? `inherit: ${shell} in env (unset at exec)` : "inherit: subscription login";
|
|
704
710
|
}
|
|
711
|
+
function withoutProviderVars(env) {
|
|
712
|
+
return Object.fromEntries(Object.entries(env).filter(([k]) => !BILLING_VARS_TO_UNSET.includes(k) && k !== "ANTHROPIC_BASE_URL" && !/^(CLAUDE_CODE_USE_|ANTHROPIC_(VERTEX|BEDROCK|FOUNDRY))/.test(k)));
|
|
713
|
+
}
|
|
705
714
|
var claudeAdapter = {
|
|
706
715
|
harness: "claude",
|
|
707
716
|
displayName: "Claude Code",
|
|
@@ -757,6 +766,7 @@ var claudeAdapter = {
|
|
|
757
766
|
}
|
|
758
767
|
})] : [],
|
|
759
768
|
...spec.auth === "subscription" ? ["--settings", subscriptionSettings()] : [],
|
|
769
|
+
...spec.auth === "workspace" && spec.inference ? ["--settings", workspaceSettings(spec.inference.baseUrl)] : [],
|
|
760
770
|
...spec.args
|
|
761
771
|
];
|
|
762
772
|
return wrapInvocation(spec, { model: o.model, args });
|
|
@@ -767,7 +777,11 @@ var claudeAdapter = {
|
|
|
767
777
|
// blocks at all — without it, Agent Log Cycles reporting has nothing
|
|
768
778
|
// to report (ISSUE-558).
|
|
769
779
|
...spec.maxThinkingTokens > 0 ? { MAX_THINKING_TOKENS: String(spec.maxThinkingTokens) } : {},
|
|
770
|
-
|
|
780
|
+
// CREW-2796: under `workspace` the only route to a model is the endpoint. The provider
|
|
781
|
+
// variables are already unset at exec (`unsetEnv`), and a spec-level `env:` entry must not
|
|
782
|
+
// smuggle one back in.
|
|
783
|
+
...spec.auth === "workspace" ? withoutProviderVars(spec.env) : spec.env,
|
|
784
|
+
...spec.auth === "workspace" && spec.inference ? { ANTHROPIC_BASE_URL: spec.inference.baseUrl, ANTHROPIC_AUTH_TOKEN: spec.inference.token } : {}
|
|
771
785
|
};
|
|
772
786
|
},
|
|
773
787
|
provider: () => "anthropic",
|
|
@@ -3168,10 +3182,10 @@ function parseToolchains(raw, base, missing, file) {
|
|
|
3168
3182
|
const maxRunMinutes = parseMaxRunMinutes(entryRaw.maxRunMinutes, `${where}.maxRunMinutes`, missing);
|
|
3169
3183
|
let auth;
|
|
3170
3184
|
if (entryRaw.auth !== void 0) {
|
|
3171
|
-
if (entryRaw.auth !== "subscription" && entryRaw.auth !== "inherit") {
|
|
3172
|
-
missing.add(`${where}.auth must be subscription|inherit (got "${entryRaw.auth}")`);
|
|
3185
|
+
if (entryRaw.auth !== "subscription" && entryRaw.auth !== "inherit" && entryRaw.auth !== "workspace") {
|
|
3186
|
+
missing.add(`${where}.auth must be subscription|inherit|workspace (got "${entryRaw.auth}")`);
|
|
3173
3187
|
} else if (harness !== "claude") {
|
|
3174
|
-
missing.add(`${where}.auth is only supported on the claude harness`);
|
|
3188
|
+
missing.add(`${where}.auth is only supported on the claude harness (${entryRaw.auth === "workspace" ? "workspace is not available for codex or gemini yet" : "got " + harness})`);
|
|
3175
3189
|
} else {
|
|
3176
3190
|
auth = entryRaw.auth;
|
|
3177
3191
|
}
|
|
@@ -4939,6 +4953,16 @@ function agentLogRefusedItem(route2, role, status2, nowIso = (/* @__PURE__ */ ne
|
|
|
4939
4953
|
message: `${route2}: the tracker refused the ${role} seat's run report (${status2}) \u2014 runs and ticket spend are not being recorded; check the seat key's role`
|
|
4940
4954
|
};
|
|
4941
4955
|
}
|
|
4956
|
+
var workspaceAuthKey = (role) => `workspace_auth:${role}`;
|
|
4957
|
+
var workspaceAllowanceKey = (role) => `workspace_allowance:${role}`;
|
|
4958
|
+
function workspaceAllowanceItem(route2, role, nowIso = (/* @__PURE__ */ new Date()).toISOString()) {
|
|
4959
|
+
return {
|
|
4960
|
+
kind: "workspace_allowance",
|
|
4961
|
+
key: workspaceAllowanceKey(role),
|
|
4962
|
+
since: nowIso,
|
|
4963
|
+
message: `${route2}: the ${role} seat is paused \u2014 the workspace is past its AI allowance; it resumes on its own once the limit is raised`
|
|
4964
|
+
};
|
|
4965
|
+
}
|
|
4942
4966
|
var diskLowKey = (repo) => `disk_low:${repo}`;
|
|
4943
4967
|
var LOW_DISK_BYTES = 2 * 1024 ** 3;
|
|
4944
4968
|
var gib = (bytes) => (bytes / 1024 ** 3).toFixed(1);
|
|
@@ -7278,6 +7302,18 @@ var Tracker = class {
|
|
|
7278
7302
|
if (!row) return;
|
|
7279
7303
|
await this.client.records.update(this.route.resolved.shipsModelId, row.id, patch2);
|
|
7280
7304
|
}
|
|
7305
|
+
/**
|
|
7306
|
+
* Writes (or, with `null`, clears) this ship's `relay_url` (CREW-2803). A
|
|
7307
|
+
* no-op when there is no row for this ship or the Ships table has not taken
|
|
7308
|
+
* the column (probed by the key's absence from the row), so the tunnel
|
|
7309
|
+
* still runs on an install that predates it.
|
|
7310
|
+
*/
|
|
7311
|
+
async updateRelayUrl(shipName2, url) {
|
|
7312
|
+
const row = await this.myShipRow(shipName2);
|
|
7313
|
+
if (!row || !("relay_url" in row)) return;
|
|
7314
|
+
if ((row.relay_url ?? null) === url) return;
|
|
7315
|
+
await this.client.records.update(this.route.resolved.shipsModelId, row.id, { relay_url: url });
|
|
7316
|
+
}
|
|
7281
7317
|
/** The other half of `beatEngaged` — clears engagement back to idle. */
|
|
7282
7318
|
async beatIdle(shipName2) {
|
|
7283
7319
|
const row = await this.myShipRow(shipName2);
|
|
@@ -9367,6 +9403,37 @@ var AgentError = class extends Error {
|
|
|
9367
9403
|
};
|
|
9368
9404
|
var AgentSpawnError = class extends AgentError {
|
|
9369
9405
|
};
|
|
9406
|
+
var WorkspaceAuthRefused = class extends AgentSpawnError {
|
|
9407
|
+
};
|
|
9408
|
+
var WorkspaceAllowanceExhausted = class extends AgentError {
|
|
9409
|
+
};
|
|
9410
|
+
var ALLOWANCE_REFUSAL_MARKER = "AI_ALLOWANCE_EXCEEDED";
|
|
9411
|
+
function streamShowsAllowanceRefusal(streamPath) {
|
|
9412
|
+
let raw;
|
|
9413
|
+
try {
|
|
9414
|
+
raw = readFileSync19(streamPath, "utf8");
|
|
9415
|
+
} catch {
|
|
9416
|
+
return false;
|
|
9417
|
+
}
|
|
9418
|
+
if (!raw.includes(ALLOWANCE_REFUSAL_MARKER)) return false;
|
|
9419
|
+
for (const line of raw.split("\n")) {
|
|
9420
|
+
if (!line.includes(ALLOWANCE_REFUSAL_MARKER)) continue;
|
|
9421
|
+
let ev;
|
|
9422
|
+
try {
|
|
9423
|
+
ev = JSON.parse(line);
|
|
9424
|
+
} catch {
|
|
9425
|
+
continue;
|
|
9426
|
+
}
|
|
9427
|
+
if (typeof ev !== "object" || ev === null) continue;
|
|
9428
|
+
const e = ev;
|
|
9429
|
+
if (e.type === "result" && e.is_error === true) {
|
|
9430
|
+
if (typeof e.result === "string" && e.result.includes(ALLOWANCE_REFUSAL_MARKER)) return true;
|
|
9431
|
+
} else if (e.type === "assistant" && (e.isApiErrorMessage === true || e.error !== void 0)) {
|
|
9432
|
+
if (JSON.stringify(e.message ?? "").includes(ALLOWANCE_REFUSAL_MARKER)) return true;
|
|
9433
|
+
}
|
|
9434
|
+
}
|
|
9435
|
+
return false;
|
|
9436
|
+
}
|
|
9370
9437
|
var digestPathFor = (stateDir, routeLabel, role, runId) => join22(stateDir, `digest-${routeLabel}-${role}${runId ? `-${runId}` : ""}.md`);
|
|
9371
9438
|
function planAgentRun(o) {
|
|
9372
9439
|
const briefPath = join22(o.route.promptsDir, "personas", `lane-${o.role}.md`);
|
|
@@ -9411,7 +9478,17 @@ function planAgentRun(o) {
|
|
|
9411
9478
|
const streamsDir = join22(o.stateDir, "streams");
|
|
9412
9479
|
const base = `${routeLabel}-${o.role}-${o.cycle}${o.runId ? `-${o.runId}` : ""}`;
|
|
9413
9480
|
const seat = resolveSeat(o.ship, o.route, o.role, o.agentPersona ?? (o.agentModel ? { model: o.agentModel } : {}), o.repo);
|
|
9414
|
-
const {
|
|
9481
|
+
const { model } = seat;
|
|
9482
|
+
let toolchain = seat.toolchain;
|
|
9483
|
+
if (toolchain.auth === "workspace") {
|
|
9484
|
+
if (!o.apiKey || o.seatKind === "ship") {
|
|
9485
|
+
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\`)`);
|
|
9486
|
+
}
|
|
9487
|
+
if (!o.route.resolved) {
|
|
9488
|
+
throw new WorkspaceAuthRefused(`${o.role}: toolchain ${toolchain.name} has auth: workspace, but route ${o.route.route} is not connected (run \`crew connect\`)`);
|
|
9489
|
+
}
|
|
9490
|
+
toolchain = { ...toolchain, inference: { baseUrl: inferenceBaseUrl(o.route.baseUrl, o.route.resolved.workspaceId), token: o.apiKey } };
|
|
9491
|
+
}
|
|
9415
9492
|
const adapter = adapterFor(toolchain.harness);
|
|
9416
9493
|
const invocation = adapter.invocation(toolchain, {
|
|
9417
9494
|
role: o.role,
|
|
@@ -9456,6 +9533,7 @@ function planAgentRun(o) {
|
|
|
9456
9533
|
args: invocation.args,
|
|
9457
9534
|
toolchain: toolchain.name,
|
|
9458
9535
|
harness: toolchain.harness,
|
|
9536
|
+
workspaceAuth: toolchain.auth === "workspace",
|
|
9459
9537
|
provider: toolchainProvider(toolchain),
|
|
9460
9538
|
maxRunMinutes: resolveMaxRunMinutes(o.route.maxRunMinutes, toolchain.maxRunMinutes),
|
|
9461
9539
|
resolution: { toolchain: seat.toolchainWhy, model: seat.modelWhy },
|
|
@@ -18145,6 +18223,26 @@ function sshKeygenAvailable() {
|
|
|
18145
18223
|
}
|
|
18146
18224
|
}
|
|
18147
18225
|
|
|
18226
|
+
// src/relay-probe.ts
|
|
18227
|
+
async function probeRelayControl(relayUrl, apiKey, userAgent, fetchFn = fetch, timeoutMs = 1e4) {
|
|
18228
|
+
const started = Date.now();
|
|
18229
|
+
try {
|
|
18230
|
+
const res = await fetchFn(`${relayUrl.replace(/\/+$/, "")}/control/status`, {
|
|
18231
|
+
headers: { Authorization: `Bearer ${apiKey}`, "User-Agent": userAgent },
|
|
18232
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
18233
|
+
});
|
|
18234
|
+
const latencyMs = Date.now() - started;
|
|
18235
|
+
await res.arrayBuffer().catch(() => void 0);
|
|
18236
|
+
if (res.ok) return { ok: true, status: res.status, latencyMs };
|
|
18237
|
+
return { ok: false, status: res.status, error: `HTTP ${res.status}` };
|
|
18238
|
+
} catch (e) {
|
|
18239
|
+
return { ok: false, error: e.message };
|
|
18240
|
+
}
|
|
18241
|
+
}
|
|
18242
|
+
function describeRelayProbe(url, p) {
|
|
18243
|
+
return p.ok ? `relay control: OK ${url}/control/status (${p.status}, ${p.latencyMs} ms)` : `relay control: FAILED ${url}/control/status \u2014 ${p.error}`;
|
|
18244
|
+
}
|
|
18245
|
+
|
|
18148
18246
|
// src/passenger-containers.ts
|
|
18149
18247
|
import { execFileSync as execFileSync9 } from "child_process";
|
|
18150
18248
|
import { createHash as createHash7 } from "crypto";
|
|
@@ -18362,15 +18460,21 @@ function generateTunnelSlug() {
|
|
|
18362
18460
|
for (let i = 0; i < SLUG_LENGTH; i++) out += SLUG_ALPHABET[randomInt(SLUG_ALPHABET.length)];
|
|
18363
18461
|
return out;
|
|
18364
18462
|
}
|
|
18365
|
-
function bindAddrFor(o) {
|
|
18366
|
-
const base = `workspace-${o.workspaceId}-slug-${o.slug}-key-${o.apiKey}`;
|
|
18367
|
-
return o.mcpSecret ? `${base}-secret-${o.mcpSecret}` : base;
|
|
18463
|
+
function bindAddrFor(o, path) {
|
|
18464
|
+
const base = `workspace-${o.workspaceId}-slug-${o.slug}${path ? `-path-${path}` : ""}-key-${o.apiKey}`;
|
|
18465
|
+
return o.mcpSecret && !path ? `${base}-secret-${o.mcpSecret}` : base;
|
|
18368
18466
|
}
|
|
18369
|
-
function
|
|
18467
|
+
function relayBaseUrlFor(slug, relayHost, relayHttpPort, relayPublicDomain) {
|
|
18370
18468
|
const domain = relayPublicDomain ?? relayHost.replace(/^.*@/, "");
|
|
18371
|
-
return `https://${slug}.${domain}${relayHttpPort === 443 ? "" : `:${relayHttpPort}`}
|
|
18469
|
+
return `https://${slug}.${domain}${relayHttpPort === 443 ? "" : `:${relayHttpPort}`}`;
|
|
18470
|
+
}
|
|
18471
|
+
function publicUrlFor(slug, relayHost, relayHttpPort, relayPublicDomain) {
|
|
18472
|
+
return `${relayBaseUrlFor(slug, relayHost, relayHttpPort, relayPublicDomain)}/mcp`;
|
|
18372
18473
|
}
|
|
18373
18474
|
function sshArgsFor(o) {
|
|
18475
|
+
const forwards = [];
|
|
18476
|
+
if (o.localPort !== void 0) forwards.push("-R", `${bindAddrFor(o)}:0:127.0.0.1:${o.localPort}`);
|
|
18477
|
+
if (o.controlSocket) forwards.push("-R", `${bindAddrFor(o, "control")}:0:${o.controlSocket}`);
|
|
18374
18478
|
return [
|
|
18375
18479
|
"-N",
|
|
18376
18480
|
"-o",
|
|
@@ -18385,8 +18489,7 @@ function sshArgsFor(o) {
|
|
|
18385
18489
|
o.privateKeyPath,
|
|
18386
18490
|
"-p",
|
|
18387
18491
|
String(o.relayPort),
|
|
18388
|
-
|
|
18389
|
-
`${bindAddrFor(o)}:0:127.0.0.1:${o.localPort}`,
|
|
18492
|
+
...forwards,
|
|
18390
18493
|
o.relayHost
|
|
18391
18494
|
];
|
|
18392
18495
|
}
|
|
@@ -18457,16 +18560,19 @@ function isPidAlive(pid) {
|
|
|
18457
18560
|
return false;
|
|
18458
18561
|
}
|
|
18459
18562
|
}
|
|
18563
|
+
var CONTROL_ONLY_HASH = "control-only";
|
|
18460
18564
|
function syncPassengerTunnels(plans, relayHost, relayPort, privateKeyPath, stateDir, mcpSecrets = /* @__PURE__ */ new Map(), deps = {}) {
|
|
18461
18565
|
const alive = deps.isPidAlive ?? isPidAlive;
|
|
18462
18566
|
const plannedIds = new Set(plans.map((p) => p.workspaceId));
|
|
18463
18567
|
for (const plan of plans) {
|
|
18464
18568
|
const existing = readPersistedTunnel(stateDir, plan.workspaceId);
|
|
18569
|
+
const mountsHash = plan.mountsHash ?? CONTROL_ONLY_HASH;
|
|
18570
|
+
const controlUnchanged = existing?.controlSocket === deps.controlSocket;
|
|
18465
18571
|
const currentSecret = mcpSecrets.get(plan.workspaceId);
|
|
18466
18572
|
const secretUnchanged = (existing?.mcpSecret ?? void 0) === (currentSecret ?? void 0);
|
|
18467
18573
|
const slug = existing?.slug ?? generateTunnelSlug();
|
|
18468
18574
|
const slugUnchanged = existing?.liveSlug === slug;
|
|
18469
|
-
if (existing && alive(existing.pid) && existing.mountsHash ===
|
|
18575
|
+
if (existing && alive(existing.pid) && existing.mountsHash === mountsHash && secretUnchanged && slugUnchanged && controlUnchanged) {
|
|
18470
18576
|
if (existing.status === "connecting") {
|
|
18471
18577
|
const settled = { ...existing, status: "connected" };
|
|
18472
18578
|
writePersistedTunnel(stateDir, settled);
|
|
@@ -18493,6 +18599,7 @@ function syncPassengerTunnels(plans, relayHost, relayPort, privateKeyPath, state
|
|
|
18493
18599
|
slug,
|
|
18494
18600
|
apiKey,
|
|
18495
18601
|
localPort: plan.port,
|
|
18602
|
+
controlSocket: deps.controlSocket,
|
|
18496
18603
|
privateKeyPath,
|
|
18497
18604
|
mcpSecret: currentSecret,
|
|
18498
18605
|
spawnFn: deps.spawnFn,
|
|
@@ -18502,9 +18609,10 @@ function syncPassengerTunnels(plans, relayHost, relayPort, privateKeyPath, state
|
|
|
18502
18609
|
});
|
|
18503
18610
|
const started = {
|
|
18504
18611
|
pid: tunnel.pid ?? -1,
|
|
18505
|
-
containerName: plan.containerName,
|
|
18612
|
+
containerName: plan.containerName ?? "",
|
|
18506
18613
|
workspaceId: plan.workspaceId,
|
|
18507
|
-
mountsHash
|
|
18614
|
+
mountsHash,
|
|
18615
|
+
controlSocket: deps.controlSocket,
|
|
18508
18616
|
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
18509
18617
|
status: settledStatus,
|
|
18510
18618
|
mcpSecret: currentSecret,
|
|
@@ -18870,6 +18978,7 @@ import { join as join42 } from "path";
|
|
|
18870
18978
|
import { randomBytes as randomBytes2 } from "crypto";
|
|
18871
18979
|
var DEFAULT_FLOOR_MS = 5e3;
|
|
18872
18980
|
var DEFAULT_CEILING_MS = INTERVAL_SECONDS * 1e3;
|
|
18981
|
+
var ALLOWANCE_PAUSE_MS = 5 * 60 * 1e3;
|
|
18873
18982
|
var defaultSleep = (ms3) => new Promise((resolve6) => setTimeout(resolve6, ms3));
|
|
18874
18983
|
async function runDaemonLoop(o) {
|
|
18875
18984
|
const floor = o.floorMs ?? DEFAULT_FLOOR_MS;
|
|
@@ -19003,6 +19112,10 @@ var SpawnBreaker = class {
|
|
|
19003
19112
|
this.states.set(role, { failures, retryAt: this.now() + retryInMs, lastMessage: message });
|
|
19004
19113
|
return { parked, firstOfMessage: prev?.lastMessage !== message, failures, retryInMs };
|
|
19005
19114
|
}
|
|
19115
|
+
/** CREW-2796: holds `role` back for `ms` without counting a failed start (an allowance pause). */
|
|
19116
|
+
pause(role, ms3) {
|
|
19117
|
+
this.states.set(role, { failures: 0, retryAt: this.now() + ms3, lastMessage: "paused" });
|
|
19118
|
+
}
|
|
19006
19119
|
/** A run for `role` spawned: forget its earlier failed starts. */
|
|
19007
19120
|
recordSuccess(role) {
|
|
19008
19121
|
this.states.delete(role);
|
|
@@ -19130,8 +19243,27 @@ async function runOnePassUnguarded(o) {
|
|
|
19130
19243
|
const run2 = spawn9(role, decision, ctx, { ready }).then(() => {
|
|
19131
19244
|
breaker.recordSuccess(role);
|
|
19132
19245
|
clearShipAttention(o.state, `role_parked:${role}`);
|
|
19246
|
+
clearShipAttention(o.state, workspaceAuthKey(role));
|
|
19247
|
+
clearShipAttention(o.state, workspaceAllowanceKey(role));
|
|
19133
19248
|
}).catch(async (e) => {
|
|
19134
19249
|
const message = e.message;
|
|
19250
|
+
if (e instanceof WorkspaceAllowanceExhausted) {
|
|
19251
|
+
breaker.pause(role, ALLOWANCE_PAUSE_MS);
|
|
19252
|
+
emit.warn(`${role} paused: ${message}`, { step: "agent", role });
|
|
19253
|
+
await raiseShipAttention({ state: o.state, emit, route: o.route, ship: o.ship }, workspaceAllowanceItem(o.route.route, role));
|
|
19254
|
+
return;
|
|
19255
|
+
}
|
|
19256
|
+
if (e instanceof WorkspaceAuthRefused) {
|
|
19257
|
+
breaker.recordFailure(role, message);
|
|
19258
|
+
emit.warn(`${role} agent refused: ${message}`, { step: "agent", role });
|
|
19259
|
+
await raiseShipAttention({ state: o.state, emit, route: o.route, ship: o.ship }, {
|
|
19260
|
+
kind: "workspace_auth",
|
|
19261
|
+
key: workspaceAuthKey(role),
|
|
19262
|
+
message: `${o.route.route}: ${message}`,
|
|
19263
|
+
since: (/* @__PURE__ */ new Date()).toISOString()
|
|
19264
|
+
});
|
|
19265
|
+
return;
|
|
19266
|
+
}
|
|
19135
19267
|
if (!(e instanceof AgentSpawnError)) {
|
|
19136
19268
|
emit.warn(`${role} agent run failed: ${message}`, { step: "agent", role });
|
|
19137
19269
|
return;
|
|
@@ -19398,6 +19530,7 @@ async function runRoleAgentLocked(role, decision, ctx, hooks, held) {
|
|
|
19398
19530
|
roster: rosterFor(decision, route2, role),
|
|
19399
19531
|
environment: await buildEnvironment(route2, ship, ticketHint),
|
|
19400
19532
|
apiKey: identity.apiKey,
|
|
19533
|
+
seatKind: identity.kind,
|
|
19401
19534
|
cycle: emit.cycle,
|
|
19402
19535
|
runId,
|
|
19403
19536
|
ticket: ticketHint,
|
|
@@ -19442,6 +19575,9 @@ async function runRoleAgentLocked(role, decision, ctx, hooks, held) {
|
|
|
19442
19575
|
await postRunKilled(route2, tracker, workingId, memberId, result.killed.limitMinutes, emit);
|
|
19443
19576
|
} else {
|
|
19444
19577
|
await guardRunHandoff(route2, ship, tracker, role, workingId, emit, runStart);
|
|
19578
|
+
if (plan.workspaceAuth && streamShowsAllowanceRefusal(plan.streamPath)) {
|
|
19579
|
+
throw new WorkspaceAllowanceExhausted("the inference endpoint refused the run: the workspace is past its AI allowance");
|
|
19580
|
+
}
|
|
19445
19581
|
}
|
|
19446
19582
|
} finally {
|
|
19447
19583
|
await settleRunEnd({
|
|
@@ -20484,6 +20620,11 @@ if (command === "passenger-sync-daemon") {
|
|
|
20484
20620
|
}, parseTicketNumber = function(input) {
|
|
20485
20621
|
const m = /^(?:[A-Za-z]+-)?(\d+)$/.exec(input.trim());
|
|
20486
20622
|
return m ? m[1] : null;
|
|
20623
|
+
}, routeRelayUrl = function(route3) {
|
|
20624
|
+
const ws = route3.resolved?.workspaceId;
|
|
20625
|
+
const slug = ws ? readPersistedTunnel(cfg.ship.stateDir, ws)?.slug : void 0;
|
|
20626
|
+
if (!cfg.ship.relayHost || !slug) return null;
|
|
20627
|
+
return relayBaseUrlFor(slug, cfg.ship.relayHost, cfg.ship.relayHttpPort, cfg.ship.relayPublicDomain);
|
|
20487
20628
|
}, passengerApiKeys = function(routes) {
|
|
20488
20629
|
const keys = /* @__PURE__ */ new Map();
|
|
20489
20630
|
for (const r of routes) {
|
|
@@ -20531,7 +20672,7 @@ if (command === "passenger-sync-daemon") {
|
|
|
20531
20672
|
const unsafe = baseBranchUnsafe(out);
|
|
20532
20673
|
return { action: out.action, unsafe, detail: unsafe ? describeUnsafeBase(out, remote, base) : out.detail };
|
|
20533
20674
|
};
|
|
20534
|
-
requireArmed2 = requireArmed, parseTicketNumber2 = parseTicketNumber, passengerApiKeys2 = passengerApiKeys, endpointApiFor2 = endpointApiFor, endpointReconcileDeps2 = endpointReconcileDeps, releaseRefused2 = releaseRefused, doctorMachineId2 = doctorMachineId, baseHealth2 = baseHealth;
|
|
20675
|
+
requireArmed2 = requireArmed, parseTicketNumber2 = parseTicketNumber, routeRelayUrl2 = routeRelayUrl, passengerApiKeys2 = passengerApiKeys, endpointApiFor2 = endpointApiFor, endpointReconcileDeps2 = endpointReconcileDeps, releaseRefused2 = releaseRefused, doctorMachineId2 = doctorMachineId, baseHealth2 = baseHealth;
|
|
20535
20676
|
let cfg;
|
|
20536
20677
|
let firstRun;
|
|
20537
20678
|
let firstRunFromFlags = false;
|
|
@@ -20664,7 +20805,10 @@ if (command === "passenger-sync-daemon") {
|
|
|
20664
20805
|
async function syncHostPassengers() {
|
|
20665
20806
|
if (dryRun) return;
|
|
20666
20807
|
const passengerRoutes = cfg.routes.filter((r) => r.enabled && r.hostPassengers);
|
|
20667
|
-
if (passengerRoutes.length === 0)
|
|
20808
|
+
if (passengerRoutes.length === 0) {
|
|
20809
|
+
await syncShipTunnels([], /* @__PURE__ */ new Map(), []);
|
|
20810
|
+
return;
|
|
20811
|
+
}
|
|
20668
20812
|
if (!dockerAvailable()) {
|
|
20669
20813
|
const pathProblem = dockerPathProblem(cfg.ship, COMMON_DOCKER_DIRS);
|
|
20670
20814
|
emit.warn(
|
|
@@ -20706,37 +20850,64 @@ if (command === "passenger-sync-daemon") {
|
|
|
20706
20850
|
{ step: "passengers", data: { ...result } }
|
|
20707
20851
|
);
|
|
20708
20852
|
}
|
|
20709
|
-
|
|
20710
|
-
try {
|
|
20711
|
-
const { privateKeyPath } = ensureShipSshKeypair(cfg.ship.stateDir);
|
|
20712
|
-
const plans = planContainers(cfg);
|
|
20713
|
-
const mcpSecrets = new Map([...syncEndpoints].map(([workspaceId, e]) => [workspaceId, e.secret]));
|
|
20714
|
-
const relayHost = cfg.ship.relayHost;
|
|
20715
|
-
const reconcile = endpointReconcileDeps(passengerRoutes, relayHost);
|
|
20716
|
-
const pending = [];
|
|
20717
|
-
syncPassengerTunnels(plans, relayHost, cfg.ship.relayPort, privateKeyPath, cfg.ship.stateDir, mcpSecrets, {
|
|
20718
|
-
apiKeys: passengerApiKeys(passengerRoutes),
|
|
20719
|
-
onStatus: (workspaceId, status2, info) => {
|
|
20720
|
-
const owningRoute = passengerRoutes.find((r) => r.resolved?.workspaceId === workspaceId);
|
|
20721
|
-
if (!owningRoute) return;
|
|
20722
|
-
pending.push(
|
|
20723
|
-
new Tracker(owningRoute, cfg.ship).updateTunnelState(cfg.ship.name, { tunnel_status: status2 }).catch((e) => {
|
|
20724
|
-
emit.warn(`could not write tunnel_status: ${e.message}`, { step: "passengers" });
|
|
20725
|
-
})
|
|
20726
|
-
);
|
|
20727
|
-
if (status2 === "disconnected") pending.push(unregisterEndpoint(workspaceId, info, reconcile));
|
|
20728
|
-
}
|
|
20729
|
-
});
|
|
20730
|
-
for (const plan of plans) pending.push(reconcileEndpoint(plan.workspaceId, reconcile));
|
|
20731
|
-
await Promise.all(pending);
|
|
20732
|
-
} catch (e) {
|
|
20733
|
-
emit.warn(`passenger tunnel sync failed: ${e.message}`, { step: "passengers" });
|
|
20734
|
-
}
|
|
20735
|
-
}
|
|
20853
|
+
await syncShipTunnels(planContainers(cfg), new Map([...syncEndpoints].map(([workspaceId, e]) => [workspaceId, e.secret])), passengerRoutes);
|
|
20736
20854
|
} catch (e) {
|
|
20737
20855
|
emit.warn(`passenger container sync failed: ${e.message}`, { step: "passengers" });
|
|
20738
20856
|
}
|
|
20739
20857
|
}
|
|
20858
|
+
async function syncShipTunnels(containerPlans, mcpSecrets, passengerRoutes) {
|
|
20859
|
+
if (!cfg.ship.relayHost) return;
|
|
20860
|
+
const relayHost = cfg.ship.relayHost;
|
|
20861
|
+
try {
|
|
20862
|
+
const { privateKeyPath } = ensureShipSshKeypair(cfg.ship.stateDir);
|
|
20863
|
+
const controlRoutes = cfg.routes.filter((r) => r.enabled && r.resolved?.workspaceId);
|
|
20864
|
+
const planned = new Set(containerPlans.map((p) => p.workspaceId));
|
|
20865
|
+
const plans = [...containerPlans];
|
|
20866
|
+
for (const r of controlRoutes) {
|
|
20867
|
+
const ws = r.resolved.workspaceId;
|
|
20868
|
+
if (planned.has(ws)) continue;
|
|
20869
|
+
planned.add(ws);
|
|
20870
|
+
plans.push({ workspaceId: ws });
|
|
20871
|
+
}
|
|
20872
|
+
const apiRoutes = controlRoutes;
|
|
20873
|
+
const reconcile = endpointReconcileDeps(passengerRoutes, relayHost);
|
|
20874
|
+
const pending = [];
|
|
20875
|
+
syncPassengerTunnels(plans, relayHost, cfg.ship.relayPort, privateKeyPath, cfg.ship.stateDir, mcpSecrets, {
|
|
20876
|
+
apiKeys: passengerApiKeys(apiRoutes),
|
|
20877
|
+
controlSocket: controlSocketFor(cfg.ship.stateDir),
|
|
20878
|
+
onStatus: (workspaceId, status2, info) => {
|
|
20879
|
+
const owningRoute = apiRoutes.find((r) => r.resolved?.workspaceId === workspaceId);
|
|
20880
|
+
if (!owningRoute) return;
|
|
20881
|
+
pending.push(
|
|
20882
|
+
new Tracker(owningRoute, cfg.ship).updateTunnelState(cfg.ship.name, { tunnel_status: status2 }).catch((e) => {
|
|
20883
|
+
emit.warn(`could not write tunnel_status: ${e.message}`, { step: "passengers" });
|
|
20884
|
+
})
|
|
20885
|
+
);
|
|
20886
|
+
if (status2 === "disconnected") pending.push(unregisterEndpoint(workspaceId, info, reconcile));
|
|
20887
|
+
}
|
|
20888
|
+
});
|
|
20889
|
+
for (const plan of containerPlans) pending.push(reconcileEndpoint(plan.workspaceId, reconcile));
|
|
20890
|
+
for (const plan of plans) pending.push(reconcileRelayUrl(plan.workspaceId, apiRoutes));
|
|
20891
|
+
await Promise.all(pending);
|
|
20892
|
+
} catch (e) {
|
|
20893
|
+
emit.warn(`ship tunnel sync failed: ${e.message}`, { step: "passengers" });
|
|
20894
|
+
}
|
|
20895
|
+
}
|
|
20896
|
+
async function reconcileRelayUrl(workspaceId, routes) {
|
|
20897
|
+
if (!cfg.ship.relayHost) return;
|
|
20898
|
+
const persisted = readPersistedTunnel(cfg.ship.stateDir, workspaceId);
|
|
20899
|
+
if (!persisted?.slug || persisted.status !== "connected") return;
|
|
20900
|
+
const url = relayBaseUrlFor(persisted.slug, cfg.ship.relayHost, cfg.ship.relayHttpPort, cfg.ship.relayPublicDomain);
|
|
20901
|
+
if (persisted.relayUrl === url) return;
|
|
20902
|
+
const route3 = routes.find((r) => r.resolved?.workspaceId === workspaceId);
|
|
20903
|
+
if (!route3) return;
|
|
20904
|
+
try {
|
|
20905
|
+
await new Tracker(route3, cfg.ship).updateRelayUrl(cfg.ship.name, url);
|
|
20906
|
+
updatePersistedTunnel(cfg.ship.stateDir, workspaceId, { relayUrl: url });
|
|
20907
|
+
} catch (e) {
|
|
20908
|
+
emit.warn(`could not write relay_url: ${e.message}`, { step: "passengers" });
|
|
20909
|
+
}
|
|
20910
|
+
}
|
|
20740
20911
|
async function releaseFleet(opts = {}) {
|
|
20741
20912
|
if (releaseRefused()) return;
|
|
20742
20913
|
precheckTickets.clear();
|
|
@@ -23753,6 +23924,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
23753
23924
|
const rotatePending = [];
|
|
23754
23925
|
syncPassengerTunnels(plans, cfg.ship.relayHost, cfg.ship.relayPort, privateKeyPath, cfg.ship.stateDir, /* @__PURE__ */ new Map(), {
|
|
23755
23926
|
apiKeys: passengerApiKeys([route2]),
|
|
23927
|
+
controlSocket: controlSocketFor(cfg.ship.stateDir),
|
|
23756
23928
|
onStatus: (wsId, status2, info) => {
|
|
23757
23929
|
rotatePending.push(
|
|
23758
23930
|
new Tracker(route2, cfg.ship).updateTunnelState(cfg.ship.name, { tunnel_status: status2 }).catch((e) => {
|
|
@@ -24329,6 +24501,14 @@ ${where.join("\n")}`);
|
|
|
24329
24501
|
const url = mcpUrlFor(value("base-url") ?? cfg.routes[0]?.baseUrl ?? DEFAULT_BASE_URL);
|
|
24330
24502
|
const disconnectHarnesses = /* @__PURE__ */ new Set([...shipPair(cfg.ship), ...shipToolchains(cfg.ship).map((t) => t.harness).filter(isPairHarness)]);
|
|
24331
24503
|
const removePairEntries = (mcpUrl, dry) => [...disconnectHarnesses].map((h) => pairInstallerFor(h).removePair({ url: mcpUrl, bin: cfg.ship.agent.bin, dryRun: dry }));
|
|
24504
|
+
if (!dryRun) {
|
|
24505
|
+
for (const r of cfg.routes.filter((x) => x.resolved?.shipsModelId)) {
|
|
24506
|
+
await new Tracker(r, cfg.ship).updateRelayUrl(cfg.ship.name, null).catch((e) => {
|
|
24507
|
+
process.stderr.write(`could not clear relay_url for ${r.route}: ${e.message}
|
|
24508
|
+
`);
|
|
24509
|
+
});
|
|
24510
|
+
}
|
|
24511
|
+
}
|
|
24332
24512
|
if (!flag("reset")) {
|
|
24333
24513
|
for (const line of removePairEntries(url, dryRun)) process.stderr.write(`${line}
|
|
24334
24514
|
`);
|
|
@@ -24763,7 +24943,9 @@ ${where.join("\n")}`);
|
|
|
24763
24943
|
attention: openShipAttention(state),
|
|
24764
24944
|
// Additive (CREW-2495): this ship's Ships-row name and whether it has drifted from `name`.
|
|
24765
24945
|
rowName: nameSync.rowName ?? null,
|
|
24766
|
-
nameDrift: nameSync.drift
|
|
24946
|
+
nameDrift: nameSync.drift,
|
|
24947
|
+
// Additive (CREW-2803): the relay URL this route's tunnel is reachable at, or null.
|
|
24948
|
+
relayUrl: routeRelayUrl(route2)
|
|
24767
24949
|
},
|
|
24768
24950
|
// Additive (CREW-1402): what the operator owes. `version` stays 1.
|
|
24769
24951
|
todo: todo.map((i) => ({ ticket: displayKey(i.ticket), title: i.ticket.title ?? null, reasons: i.reasons })),
|
|
@@ -24853,7 +25035,8 @@ ${owed.map((l) => ` ${l}
|
|
|
24853
25035
|
route: ${route2.route} -> ${route2.dir}
|
|
24854
25036
|
enabled: ${route2.enabled}
|
|
24855
25037
|
periscopes: ${periscopeSummary(cfg)}
|
|
24856
|
-
` +
|
|
25038
|
+
` + (routeRelayUrl(route2) ? `relay: ${routeRelayUrl(route2)}
|
|
25039
|
+
` : "") + periscopes.map((p) => `periscope: ${p.ticket} ${p.repo} ${p.url} ${p.state}${p.serving ? "" : " (not serving)"} expires ${p.expiresAt}
|
|
24857
25040
|
`).join("") + `crew: ${state.isPaused() ? "paused" : "active"}
|
|
24858
25041
|
release: ${state.releaseSkipReason() ? "paused" : "active"}
|
|
24859
25042
|
watermark: ${state.watermark()}
|
|
@@ -25445,11 +25628,34 @@ ${foreign.join("\n")}
|
|
|
25445
25628
|
);
|
|
25446
25629
|
}
|
|
25447
25630
|
}
|
|
25631
|
+
if (cfg.ship.relayHost && !flag("offline")) {
|
|
25632
|
+
const relayUrl = routeRelayUrl(route2);
|
|
25633
|
+
if (!relayUrl) {
|
|
25634
|
+
process.stdout.write("relay control: no tunnel URL yet (the first `crew passengers` cycle opens it)\n");
|
|
25635
|
+
} else {
|
|
25636
|
+
let key;
|
|
25637
|
+
try {
|
|
25638
|
+
key = resolveApiKey(route2);
|
|
25639
|
+
} catch {
|
|
25640
|
+
key = void 0;
|
|
25641
|
+
}
|
|
25642
|
+
process.stdout.write(`${key ? describeRelayProbe(relayUrl, await probeRelayControl(relayUrl, key, cfg.ship.userAgent)) : "relay control: could not check (no API key for this route)"}
|
|
25643
|
+
`);
|
|
25644
|
+
}
|
|
25645
|
+
}
|
|
25448
25646
|
for (const l of doctorSelftestLines(readSelftest(cfg.ship.stateDir), shipToolchains(cfg.ship).map((t) => t.name))) {
|
|
25449
25647
|
process.stdout.write(`${l}
|
|
25450
25648
|
`);
|
|
25451
25649
|
}
|
|
25452
25650
|
for (const t of shipToolchains(cfg.ship).filter((t2) => t2.harness === "claude")) {
|
|
25651
|
+
if (t.auth === "workspace") {
|
|
25652
|
+
for (const r of cfg.routes.filter((r2) => r2.enabled)) {
|
|
25653
|
+
const inference = r.resolved ? { baseUrl: inferenceBaseUrl(r.baseUrl, r.resolved.workspaceId), token: "" } : void 0;
|
|
25654
|
+
process.stdout.write(`auth: ${t.name}: ${r.route}: ${describeClaudeAuth({ ...t, inference })}
|
|
25655
|
+
`);
|
|
25656
|
+
}
|
|
25657
|
+
continue;
|
|
25658
|
+
}
|
|
25453
25659
|
process.stdout.write(`auth: ${t.name}: ${describeClaudeAuth(t)}
|
|
25454
25660
|
`);
|
|
25455
25661
|
}
|
|
@@ -25466,6 +25672,7 @@ ${foreign.join("\n")}
|
|
|
25466
25672
|
}
|
|
25467
25673
|
var requireArmed2;
|
|
25468
25674
|
var parseTicketNumber2;
|
|
25675
|
+
var routeRelayUrl2;
|
|
25469
25676
|
var passengerApiKeys2;
|
|
25470
25677
|
var endpointApiFor2;
|
|
25471
25678
|
var endpointReconcileDeps2;
|
package/package.json
CHANGED