squadrant 0.21.0 → 0.21.1

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.
@@ -3471,6 +3471,11 @@ function assertBackendUsable(o) {
3471
3471
  function shouldBuildRouterService(router, isVitest) {
3472
3472
  return !!router && !isVitest;
3473
3473
  }
3474
+ function claudeEnvShadowsRouter(claudeEnv) {
3475
+ if (!claudeEnv)
3476
+ return [];
3477
+ return Object.keys(claudeEnv).filter((key) => key.startsWith("ANTHROPIC_"));
3478
+ }
3474
3479
  var init_router_resolution = __esm({
3475
3480
  "packages/core/dist/router-resolution.js"() {
3476
3481
  init_dist();
@@ -3497,6 +3502,14 @@ function buildRouterEnv(creds, model) {
3497
3502
  env.ANTHROPIC_MODEL = model;
3498
3503
  return env;
3499
3504
  }
3505
+ function mergeClaudeEnvRouterSettings(routerEnv, claudeEnv) {
3506
+ const merged = { ...routerEnv };
3507
+ for (const [key, value] of Object.entries(claudeEnv ?? {})) {
3508
+ if (!key.startsWith("ANTHROPIC_"))
3509
+ merged[key] = value;
3510
+ }
3511
+ return merged;
3512
+ }
3500
3513
  function formatCustomHeaders(headers) {
3501
3514
  const entries = Object.entries(headers ?? {});
3502
3515
  if (entries.length === 0)
@@ -3810,10 +3823,18 @@ async function runCrewSpawn(input, config, deps) {
3810
3823
  if (agentName === "claude") {
3811
3824
  let routerEnv = {};
3812
3825
  if (backend !== "native") {
3826
+ if (!deps.writeRouterSettings) {
3827
+ throw new Error(`backend '${backend}' requires a per-spawn --settings writer to outrank defaults.claudeEnv, but the spawn path has none \u2014 refusing to launch a routed session that would silently bypass the router shim`);
3828
+ }
3813
3829
  if (!deps.routerCredentials) {
3814
3830
  throw new Error(`backend '${backend}' requires router credentials, but the spawn path has no daemon credentials provider`);
3815
3831
  }
3816
3832
  routerEnv = buildRouterEnv(await deps.routerCredentials({ project: input.project, backend }), crewModel);
3833
+ const shadowed = claudeEnvShadowsRouter(config.defaults.claudeEnv);
3834
+ if (shadowed.length > 0) {
3835
+ process.stderr.write(`\u26A0\uFE0F backend '${backend}' selected but defaults.claudeEnv sets ${shadowed.join(", ")} \u2014 these would shadow the router shim. Overriding via a per-spawn --settings env block (outranks ~/.claude/settings.json).
3836
+ `);
3837
+ }
3817
3838
  }
3818
3839
  ensureSocksDir();
3819
3840
  const messagingSocketPath = path9.join(CC_SOCKS_DIR, `squadrant-${randomUUID3()}.sock`);
@@ -3826,6 +3847,12 @@ async function runCrewSpawn(input, config, deps) {
3826
3847
  name,
3827
3848
  messagingSocketPath
3828
3849
  });
3850
+ const routerSettingsPath = Object.keys(routerEnv).length > 0 ? deps.writeRouterSettings({
3851
+ stateRoot: STATE_ROOT,
3852
+ project: input.project,
3853
+ taskId: rec.id,
3854
+ env: mergeClaudeEnvRouterSettings(routerEnv, config.defaults.claudeEnv)
3855
+ }) : void 0;
3829
3856
  let hooksInstalled = false;
3830
3857
  try {
3831
3858
  deps.writeSettingsLocal(spawnCwd);
@@ -3841,18 +3868,25 @@ async function runCrewSpawn(input, config, deps) {
3841
3868
  messagingSocketPath,
3842
3869
  // Permission mode is config-driven so squadrant can default crews to 'auto'
3843
3870
  // or keep the semi-automatic 'acceptEdits' gate. Falls back to 'acceptEdits'.
3844
- permissionMode: config.defaults.permissions?.crew ?? "acceptEdits",
3871
+ // #772: a router-backed crew MUST use 'default' — 'auto' makes the built-in
3872
+ // Sonnet-5 classifier own PermissionRequest and the gate yields, which is
3873
+ // exactly the fail-closed dead end U7 exists to fix. The rubric owns the
3874
+ // permission mode for a routed spawn; the operator's config cannot override
3875
+ // it back into the broken path.
3876
+ permissionMode: backend !== "native" ? "default" : config.defaults.permissions?.crew ?? "acceptEdits",
3845
3877
  // #708: self-describing name so ListAgents/the registry can tell this
3846
3878
  // crew apart from an unrelated session instead of an auto-derived cwd
3847
3879
  // basename (only the claude driver reads this — other agents ignore it).
3848
3880
  sessionName: crewSessionName(input.project, name),
3849
3881
  ...crewModel ? { model: crewModel } : {},
3850
- ...crewThinking ? { thinking: crewThinking } : {}
3882
+ ...crewThinking ? { thinking: crewThinking } : {},
3883
+ ...routerSettingsPath ? { settingsPath: routerSettingsPath } : {}
3851
3884
  });
3852
3885
  const direction2 = input.direction ?? "tab";
3853
3886
  const title2 = titleFor(input.project, name);
3854
3887
  const pane2 = await deps.runtime.newPane({ workspaceId: captain.id, direction: direction2, title: title2 });
3855
- const envPrefix = `SQUADRANT_CREW_TASK_ID=${rec.id} SQUADRANT_CREW_PROJECT=${input.project}`;
3888
+ const gateEnv = backend !== "native" ? " SQUADRANT_GATE=on" : "";
3889
+ const envPrefix = `SQUADRANT_CREW_TASK_ID=${rec.id} SQUADRANT_CREW_PROJECT=${input.project}${gateEnv}`;
3856
3890
  const routerPrefix = Object.keys(routerEnv).length > 0 ? ` ${renderEnvAssignments(routerEnv)}` : "";
3857
3891
  await deps.runtime.sendToPane(pane2, `cd ${shellQuote(spawnCwd)} && ${envPrefix}${routerPrefix} ${niceCrewCommand(cliCommand2)}`);
3858
3892
  const preLaunchScreen = await deps.runtime.readPaneScreen(pane2) ?? "";
@@ -6760,6 +6794,26 @@ function writeJson(res, status, body) {
6760
6794
  res.writeHead(status, { "content-type": "application/json" });
6761
6795
  res.end(JSON.stringify(body));
6762
6796
  }
6797
+ function pathnameOf(url) {
6798
+ if (!url)
6799
+ return "";
6800
+ const q = url.indexOf("?");
6801
+ return q === -1 ? url : url.slice(0, q);
6802
+ }
6803
+ function modelsList(ids) {
6804
+ const data = ids.map((id) => ({
6805
+ type: "model",
6806
+ id,
6807
+ display_name: id,
6808
+ created_at: "1970-01-01T00:00:00Z"
6809
+ }));
6810
+ return {
6811
+ data,
6812
+ has_more: false,
6813
+ first_id: ids[0] ?? null,
6814
+ last_id: ids[ids.length - 1] ?? null
6815
+ };
6816
+ }
6763
6817
  function buildUpstreamHeaders(req, upstream, stream) {
6764
6818
  const headers = {
6765
6819
  "content-type": "application/json",
@@ -6788,6 +6842,7 @@ function createRouterShim(opts) {
6788
6842
  let lastError;
6789
6843
  let upstreamReachable = true;
6790
6844
  const upstreamUrl = joinUrl(opts.upstream.baseUrl, "/v1/messages");
6845
+ const advertisedModels = new Set(opts.models ?? []);
6791
6846
  function emitUsage(u) {
6792
6847
  try {
6793
6848
  opts.onUsage?.(u);
@@ -6817,6 +6872,8 @@ function createRouterShim(opts) {
6817
6872
  const body = parsed;
6818
6873
  const wantsStream = body.stream === true;
6819
6874
  const model = typeof body.model === "string" ? body.model : void 0;
6875
+ if (model)
6876
+ advertisedModels.add(model);
6820
6877
  const sanitized = sanitizeRequest(body, opts.upstream);
6821
6878
  let upstreamRes;
6822
6879
  try {
@@ -6889,8 +6946,20 @@ function createRouterShim(opts) {
6889
6946
  emitUsage(usage2);
6890
6947
  writeJson(res, upstreamRes.status, upstreamBody);
6891
6948
  }
6949
+ async function handleCountTokens(req, res) {
6950
+ let text;
6951
+ try {
6952
+ text = await readBody(req);
6953
+ } catch {
6954
+ const e = anthropicError(400, "invalid_request_error", "malformed request body");
6955
+ writeJson(res, e.status, e.body);
6956
+ return;
6957
+ }
6958
+ writeJson(res, 200, { input_tokens: Math.max(1, Math.ceil(text.length / 4)) });
6959
+ }
6892
6960
  function handler(req, res) {
6893
- if (req.method === "POST" && req.url === "/v1/messages") {
6961
+ const path23 = pathnameOf(req.url);
6962
+ if (req.method === "POST" && path23 === "/v1/messages") {
6894
6963
  void handleMessages(req, res).catch((err) => {
6895
6964
  log(`router internal error: ${err instanceof Error ? err.message : String(err)}`);
6896
6965
  if (!res.headersSent) {
@@ -6902,7 +6971,18 @@ function createRouterShim(opts) {
6902
6971
  });
6903
6972
  return;
6904
6973
  }
6905
- if (req.method === "GET" && req.url === "/healthz") {
6974
+ if (req.method === "POST" && path23 === "/v1/messages/count_tokens") {
6975
+ void handleCountTokens(req, res).catch(() => {
6976
+ if (!res.headersSent)
6977
+ writeJson(res, 502, anthropicError(502, "api_error", "router internal error").body);
6978
+ });
6979
+ return;
6980
+ }
6981
+ if (req.method === "GET" && path23 === "/v1/models") {
6982
+ writeJson(res, 200, modelsList([...advertisedModels]));
6983
+ return;
6984
+ }
6985
+ if (req.method === "GET" && path23 === "/healthz") {
6906
6986
  writeJson(res, 200, { ready: server?.listening === true, upstreamReachable, lastError });
6907
6987
  return;
6908
6988
  }
@@ -7027,12 +7107,17 @@ function createRouterService(router, projects, deps = {}) {
7027
7107
  tokenByProject.set(p, t);
7028
7108
  projectTokens.set(t, p);
7029
7109
  }
7110
+ const models = [
7111
+ ...deps.models ?? [],
7112
+ ...Object.values(router.models ?? {}).map((m) => m.upstream)
7113
+ ];
7030
7114
  const ledger = createUsageLedger();
7031
7115
  const shim = createRouterShim({
7032
7116
  upstream,
7033
7117
  projectTokens,
7034
7118
  port: router.port ?? 0,
7035
7119
  log,
7120
+ models,
7036
7121
  onUsage: (u) => ledger.record(u),
7037
7122
  ...deps.fetch ? { fetch: deps.fetch } : {}
7038
7123
  });
@@ -7095,12 +7180,53 @@ var init_router = __esm({
7095
7180
  }
7096
7181
  });
7097
7182
 
7183
+ // packages/core/dist/captain-router.js
7184
+ async function prepareCaptainRoute(o) {
7185
+ assertBackendUsable({ backend: o.backend, agent: o.agentName, router: o.config.defaults.router });
7186
+ if (o.backend === "native") {
7187
+ return { backend: "native", model: o.model, permissionMode: o.configuredPermissionMode, env: {} };
7188
+ }
7189
+ if (!o.deps.routerCredentials) {
7190
+ throw new Error(`captain launch: backend '${o.backend}' requires router credentials from the daemon, but this launch path has no credentials provider`);
7191
+ }
7192
+ if (!o.deps.writeRouterSettings) {
7193
+ throw new Error(`captain launch: backend '${o.backend}' requires a per-spawn --settings writer to outrank defaults.claudeEnv, but this launch path has none \u2014 refusing to launch a routed captain that would silently bypass the router shim`);
7194
+ }
7195
+ const model = resolveRouterModel(o.model, o.agentName, o.config.defaults.router);
7196
+ const creds = await o.deps.routerCredentials({ project: o.project, backend: o.backend });
7197
+ const routerEnv = buildRouterEnv(creds, model);
7198
+ const shadowed = claudeEnvShadowsRouter(o.config.defaults.claudeEnv);
7199
+ if (shadowed.length > 0) {
7200
+ o.warn?.(`backend '${o.backend}' selected but defaults.claudeEnv sets ${shadowed.join(", ")} \u2014 these would shadow the router shim. Overriding via a per-spawn --settings env block (outranks ~/.claude/settings.json).`);
7201
+ }
7202
+ const settingsPath = o.deps.writeRouterSettings({
7203
+ stateRoot: o.stateRoot,
7204
+ project: o.project,
7205
+ taskId: "captain",
7206
+ env: mergeClaudeEnvRouterSettings(routerEnv, o.config.defaults.claudeEnv)
7207
+ });
7208
+ return {
7209
+ backend: o.backend,
7210
+ model,
7211
+ permissionMode: "default",
7212
+ env: { ...routerEnv, SQUADRANT_GATE: "on" },
7213
+ settingsPath
7214
+ };
7215
+ }
7216
+ var init_captain_router = __esm({
7217
+ "packages/core/dist/captain-router.js"() {
7218
+ init_dist();
7219
+ init_router_resolution();
7220
+ init_env();
7221
+ }
7222
+ });
7223
+
7098
7224
  // packages/core/dist/permission-gate.js
7099
7225
  import { createHash } from "crypto";
7100
7226
  import { mkdirSync as mkdirSync7, readFileSync as readFileSync11, renameSync as renameSync4, writeFileSync as writeFileSync10, unlinkSync as unlinkSync4 } from "fs";
7101
7227
  import { dirname as dirname5, join as join15 } from "path";
7102
7228
  function isGateSession(env) {
7103
- return Boolean(env.SQUADRANT_CREW_TASK_ID) || env[SIDE_SESSION_ENV] === "1";
7229
+ return Boolean(env.SQUADRANT_CREW_TASK_ID) || env[SIDE_SESSION_ENV] === "1" || env.SQUADRANT_ROLE === "captain";
7104
7230
  }
7105
7231
  function resolveGateMode(env, gate) {
7106
7232
  const fromEnv = env.SQUADRANT_GATE;
@@ -7296,7 +7422,15 @@ async function callClassifier(o) {
7296
7422
  return "ask";
7297
7423
  }
7298
7424
  const data = await res.json();
7425
+ if (data?.stop_reason === "max_tokens") {
7426
+ o.log?.("gate: classifier exhausted max_tokens before a verdict \u2014 asking");
7427
+ return "ask";
7428
+ }
7299
7429
  const text = Array.isArray(data?.content) ? data.content.filter((b) => b?.type === "text" && typeof b.text === "string").map((b) => b.text).join(" ") : "";
7430
+ if (!text.trim()) {
7431
+ o.log?.("gate: classifier returned no text verdict \u2014 asking");
7432
+ return "ask";
7433
+ }
7300
7434
  return parseClassifierVerdict(text);
7301
7435
  } catch (e) {
7302
7436
  o.log?.(`gate: classifier unavailable (${e.message}) \u2014 asking`);
@@ -7315,7 +7449,7 @@ ${toolPayload}`).digest("hex");
7315
7449
  }
7316
7450
  async function evaluatePermissionRequest(input) {
7317
7451
  if (!isGateSession(input.env)) {
7318
- return { decision: "yield", reason: "not a squadrant crew/side session" };
7452
+ return { decision: "yield", reason: "not a squadrant crew/side/captain session" };
7319
7453
  }
7320
7454
  const gate = input.config.defaults.gate;
7321
7455
  const mode = resolveGateMode(input.env, gate);
@@ -7401,7 +7535,7 @@ var init_permission_gate = __esm({
7401
7535
  DEFAULT_CACHE_TTL_MS = 10 * 60 * 1e3;
7402
7536
  DEFAULT_CACHE_MAX_ENTRIES = 500;
7403
7537
  DEFAULT_CLASSIFIER_TIMEOUT_MS = 5e3;
7404
- CLASSIFIER_MAX_TOKENS = 8;
7538
+ CLASSIFIER_MAX_TOKENS = 256;
7405
7539
  MAX_PAYLOAD_CHARS = 2e3;
7406
7540
  DEFAULT_GATE_DENY_RULES = [
7407
7541
  // Flag-order-agnostic: both recursive and force must be present, in any order,
@@ -8328,6 +8462,7 @@ __export(dist_exports, {
8328
8462
  captainSocketPath: () => captainSocketPath,
8329
8463
  checkFact: () => checkFact,
8330
8464
  classifyHealth: () => classifyHealth,
8465
+ claudeEnvShadowsRouter: () => claudeEnvShadowsRouter,
8331
8466
  closeWorkItem: () => closeWorkItem,
8332
8467
  computeTemplateHash: () => computeTemplateHash,
8333
8468
  consumeExitMarker: () => consumeExitMarker,
@@ -8428,6 +8563,7 @@ __export(dist_exports, {
8428
8563
  makeGate: () => makeGate,
8429
8564
  maskToken: () => maskToken,
8430
8565
  matchTier1Deny: () => matchTier1Deny,
8566
+ mergeClaudeEnvRouterSettings: () => mergeClaudeEnvRouterSettings,
8431
8567
  mintToken: () => mintToken,
8432
8568
  nameFromTitle: () => nameFromTitle,
8433
8569
  newestSessionInDirectory: () => newestSessionInDirectory,
@@ -8442,6 +8578,7 @@ __export(dist_exports, {
8442
8578
  parseProgramArgs: () => parseProgramArgs,
8443
8579
  plistPath: () => plistPath,
8444
8580
  pollNewestSessionInDirectory: () => pollNewestSessionInDirectory,
8581
+ prepareCaptainRoute: () => prepareCaptainRoute,
8445
8582
  printForeignInstallError: () => printForeignInstallError,
8446
8583
  printVersionUpgradeNotice: () => printVersionUpgradeNotice,
8447
8584
  probeCaptainChannel: () => probeCaptainChannel,
@@ -8574,6 +8711,7 @@ var init_dist2 = __esm({
8574
8711
  init_router();
8575
8712
  init_crew_routing();
8576
8713
  init_router_resolution();
8714
+ init_captain_router();
8577
8715
  init_permission_gate();
8578
8716
  init_restart_daemon();
8579
8717
  init_group_dispatch();