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.
- package/dist/index.js +205 -17
- package/dist/index.js.map +1 -1
- package/dist/squadrantd.js +146 -8
- package/dist/squadrantd.js.map +1 -1
- package/package.json +1 -1
package/dist/squadrantd.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
-
|
|
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 === "
|
|
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 =
|
|
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();
|