@tablation/crew 0.97.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.
Files changed (2) hide show
  1. package/dist/cli.js +107 -5
  2. 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
- ...spec.env
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);
@@ -9379,6 +9403,37 @@ var AgentError = class extends Error {
9379
9403
  };
9380
9404
  var AgentSpawnError = class extends AgentError {
9381
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
+ }
9382
9437
  var digestPathFor = (stateDir, routeLabel, role, runId) => join22(stateDir, `digest-${routeLabel}-${role}${runId ? `-${runId}` : ""}.md`);
9383
9438
  function planAgentRun(o) {
9384
9439
  const briefPath = join22(o.route.promptsDir, "personas", `lane-${o.role}.md`);
@@ -9423,7 +9478,17 @@ function planAgentRun(o) {
9423
9478
  const streamsDir = join22(o.stateDir, "streams");
9424
9479
  const base = `${routeLabel}-${o.role}-${o.cycle}${o.runId ? `-${o.runId}` : ""}`;
9425
9480
  const seat = resolveSeat(o.ship, o.route, o.role, o.agentPersona ?? (o.agentModel ? { model: o.agentModel } : {}), o.repo);
9426
- const { toolchain, model } = seat;
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
+ }
9427
9492
  const adapter = adapterFor(toolchain.harness);
9428
9493
  const invocation = adapter.invocation(toolchain, {
9429
9494
  role: o.role,
@@ -9468,6 +9533,7 @@ function planAgentRun(o) {
9468
9533
  args: invocation.args,
9469
9534
  toolchain: toolchain.name,
9470
9535
  harness: toolchain.harness,
9536
+ workspaceAuth: toolchain.auth === "workspace",
9471
9537
  provider: toolchainProvider(toolchain),
9472
9538
  maxRunMinutes: resolveMaxRunMinutes(o.route.maxRunMinutes, toolchain.maxRunMinutes),
9473
9539
  resolution: { toolchain: seat.toolchainWhy, model: seat.modelWhy },
@@ -18912,6 +18978,7 @@ import { join as join42 } from "path";
18912
18978
  import { randomBytes as randomBytes2 } from "crypto";
18913
18979
  var DEFAULT_FLOOR_MS = 5e3;
18914
18980
  var DEFAULT_CEILING_MS = INTERVAL_SECONDS * 1e3;
18981
+ var ALLOWANCE_PAUSE_MS = 5 * 60 * 1e3;
18915
18982
  var defaultSleep = (ms3) => new Promise((resolve6) => setTimeout(resolve6, ms3));
18916
18983
  async function runDaemonLoop(o) {
18917
18984
  const floor = o.floorMs ?? DEFAULT_FLOOR_MS;
@@ -19045,6 +19112,10 @@ var SpawnBreaker = class {
19045
19112
  this.states.set(role, { failures, retryAt: this.now() + retryInMs, lastMessage: message });
19046
19113
  return { parked, firstOfMessage: prev?.lastMessage !== message, failures, retryInMs };
19047
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
+ }
19048
19119
  /** A run for `role` spawned: forget its earlier failed starts. */
19049
19120
  recordSuccess(role) {
19050
19121
  this.states.delete(role);
@@ -19172,8 +19243,27 @@ async function runOnePassUnguarded(o) {
19172
19243
  const run2 = spawn9(role, decision, ctx, { ready }).then(() => {
19173
19244
  breaker.recordSuccess(role);
19174
19245
  clearShipAttention(o.state, `role_parked:${role}`);
19246
+ clearShipAttention(o.state, workspaceAuthKey(role));
19247
+ clearShipAttention(o.state, workspaceAllowanceKey(role));
19175
19248
  }).catch(async (e) => {
19176
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
+ }
19177
19267
  if (!(e instanceof AgentSpawnError)) {
19178
19268
  emit.warn(`${role} agent run failed: ${message}`, { step: "agent", role });
19179
19269
  return;
@@ -19440,6 +19530,7 @@ async function runRoleAgentLocked(role, decision, ctx, hooks, held) {
19440
19530
  roster: rosterFor(decision, route2, role),
19441
19531
  environment: await buildEnvironment(route2, ship, ticketHint),
19442
19532
  apiKey: identity.apiKey,
19533
+ seatKind: identity.kind,
19443
19534
  cycle: emit.cycle,
19444
19535
  runId,
19445
19536
  ticket: ticketHint,
@@ -19484,6 +19575,9 @@ async function runRoleAgentLocked(role, decision, ctx, hooks, held) {
19484
19575
  await postRunKilled(route2, tracker, workingId, memberId, result.killed.limitMinutes, emit);
19485
19576
  } else {
19486
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
+ }
19487
19581
  }
19488
19582
  } finally {
19489
19583
  await settleRunEnd({
@@ -25554,6 +25648,14 @@ ${foreign.join("\n")}
25554
25648
  `);
25555
25649
  }
25556
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
+ }
25557
25659
  process.stdout.write(`auth: ${t.name}: ${describeClaudeAuth(t)}
25558
25660
  `);
25559
25661
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tablation/crew",
3
- "version": "0.97.0",
3
+ "version": "0.98.0",
4
4
  "description": "A standing team of headless agents that picks work off a Tablation board, does it on a machine you control, and reports back.",
5
5
  "type": "module",
6
6
  "license": "MIT",