switchroom 0.19.1 → 0.19.3
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/agent-scheduler/index.js +31 -1
- package/dist/auth-broker/index.js +565 -48
- package/dist/cli/autoaccept-poll.js +31 -1
- package/dist/cli/drive-write-pretool.mjs +32 -2
- package/dist/cli/ms-365-write-pretool.mjs +32 -2
- package/dist/cli/switchroom.js +1148 -274
- package/dist/host-control/main.js +3 -3
- package/dist/vault/approvals/kernel-server.js +2 -2
- package/dist/vault/broker/server.js +2 -2
- package/package.json +3 -2
- package/profiles/_base/start.sh.hbs +1 -0
- package/profiles/default/CLAUDE.md.hbs +8 -0
- package/skills/mental-model-curator/SKILL.md +68 -2
- package/skills/switchroom-cli/SKILL.md +25 -0
- package/telegram-plugin/auth-snapshot-format.ts +143 -12
- package/telegram-plugin/dist/bridge/bridge.js +8 -2
- package/telegram-plugin/dist/gateway/gateway.js +1427 -689
- package/telegram-plugin/dist/server.js +8 -2
- package/telegram-plugin/external-spend.ts +135 -0
- package/telegram-plugin/flushed-turn-supersede.ts +117 -13
- package/telegram-plugin/gateway/auth-add-flow.ts +215 -6
- package/telegram-plugin/gateway/auth-command.ts +138 -5
- package/telegram-plugin/gateway/gateway.ts +141 -158
- package/telegram-plugin/gateway/inbound-interceptors.ts +13 -3
- package/telegram-plugin/gateway/model-command.ts +309 -1
- package/telegram-plugin/gateway/narrative-lane.ts +23 -9
- package/telegram-plugin/gateway/outbound-send-path.ts +68 -15
- package/telegram-plugin/gateway/session-model-source.ts +90 -10
- package/telegram-plugin/gateway/status-pin-store.ts +64 -4
- package/telegram-plugin/gateway/stream-render.ts +22 -5
- package/telegram-plugin/gateway/usage-mask.ts +29 -0
- package/telegram-plugin/hooks/subagent-tracker-pretool.mjs +19 -2
- package/telegram-plugin/quota-bar-format.ts +78 -12
- package/telegram-plugin/quota-check.ts +17 -2
- package/telegram-plugin/reply-owner-resolve.ts +76 -11
- package/telegram-plugin/session-tail.ts +27 -3
- package/telegram-plugin/tests/activity-card-wiring.test.ts +47 -0
- package/telegram-plugin/tests/auth-add-flow.test.ts +367 -5
- package/telegram-plugin/tests/auth-snapshot-format.test.ts +41 -0
- package/telegram-plugin/tests/external-spend.test.ts +168 -0
- package/telegram-plugin/tests/flushed-turn-supersede.test.ts +117 -0
- package/telegram-plugin/tests/gateway-session-model-relaunch.test.ts +219 -29
- package/telegram-plugin/tests/model-command.test.ts +220 -0
- package/telegram-plugin/tests/quota-bar-format.test.ts +43 -0
- package/telegram-plugin/tests/quota-check.test.ts +57 -0
- package/telegram-plugin/tests/reply-owner-resolve.test.ts +257 -13
- package/telegram-plugin/tests/send-reply-golden.test.ts +154 -0
- package/telegram-plugin/tests/session-model-source.test.ts +142 -0
- package/telegram-plugin/tests/session-tail-first-attach.test.ts +115 -2
- package/telegram-plugin/tests/status-pin-store.test.ts +198 -0
- package/telegram-plugin/tests/subagent-tracker-hooks.test.ts +50 -0
- package/telegram-plugin/tests/usage-footer-freshness.test.ts +141 -0
- package/telegram-plugin/tests/usage-mask.test.ts +35 -0
- package/telegram-plugin/tests/worker-feed-dispatch.test.ts +27 -0
- package/telegram-plugin/tests/worker-feed-pin-persistence.test.ts +131 -1
- package/vendor/hindsight-memory/CHANGELOG.md +102 -0
- package/vendor/hindsight-memory/README.md +2 -1
- package/vendor/hindsight-memory/hooks/hooks.json +12 -0
- package/vendor/hindsight-memory/scripts/directive_verify.py +100 -3
- package/vendor/hindsight-memory/scripts/lib/config.py +150 -1
- package/vendor/hindsight-memory/scripts/lib/content.py +55 -5
- package/vendor/hindsight-memory/scripts/lib/directives.py +152 -15
- package/vendor/hindsight-memory/scripts/lib/parallel_recall.py +142 -0
- package/vendor/hindsight-memory/scripts/lib/state.py +31 -0
- package/vendor/hindsight-memory/scripts/recall.py +789 -143
- package/vendor/hindsight-memory/scripts/reconcile_tail.py +22 -1
- package/vendor/hindsight-memory/scripts/retain.py +71 -2
- package/vendor/hindsight-memory/scripts/subagent_retain.py +501 -0
- package/vendor/hindsight-memory/scripts/tests/test_directive_verify.py +169 -0
- package/vendor/hindsight-memory/scripts/tests/test_directives.py +177 -0
- package/vendor/hindsight-memory/scripts/tests/test_lesson_tagging.py +200 -0
- package/vendor/hindsight-memory/scripts/tests/test_recall_context_turns_default.py +200 -0
- package/vendor/hindsight-memory/scripts/tests/test_recall_envelope_strip_telemetry.py +477 -0
- package/vendor/hindsight-memory/scripts/tests/test_recall_integration.py +51 -0
- package/vendor/hindsight-memory/scripts/tests/test_recall_parallel_deadline.py +409 -0
- package/vendor/hindsight-memory/scripts/tests/test_recall_tag_weights.py +96 -0
- package/vendor/hindsight-memory/scripts/tests/test_recall_transcript_fallback.py +413 -0
- package/vendor/hindsight-memory/scripts/tests/test_reconcile_durability.py +49 -0
- package/vendor/hindsight-memory/scripts/tests/test_subagent_retain.py +439 -0
- package/vendor/hindsight-memory/settings.json +3 -1
|
@@ -7012,14 +7012,27 @@ var require_dist = __commonJS((exports) => {
|
|
|
7012
7012
|
|
|
7013
7013
|
// src/util/atomic.ts
|
|
7014
7014
|
import { randomBytes } from "node:crypto";
|
|
7015
|
-
import { closeSync, constants, fsyncSync, openSync, renameSync, rmSync, writeSync } from "node:fs";
|
|
7016
|
-
function atomicWriteFileSync(destPath, contents,
|
|
7015
|
+
import { closeSync, constants, fchmodSync, fchownSync, fsyncSync, openSync, renameSync, rmSync, writeSync } from "node:fs";
|
|
7016
|
+
function atomicWriteFileSync(destPath, contents, modeOrOpts = 384) {
|
|
7017
|
+
const opts = typeof modeOrOpts === "number" ? { mode: modeOrOpts } : modeOrOpts;
|
|
7018
|
+
const mode = opts.mode ?? 384;
|
|
7017
7019
|
const tmp = `${destPath}.tmp-${process.pid}-${randomBytes(4).toString("hex")}`;
|
|
7018
7020
|
const buf = typeof contents === "string" ? Buffer.from(contents, "utf-8") : contents;
|
|
7019
7021
|
let fd = null;
|
|
7020
7022
|
try {
|
|
7021
7023
|
fd = openSync(tmp, TMP_OPEN_FLAGS, mode);
|
|
7022
7024
|
writeSync(fd, buf, 0, buf.length, 0);
|
|
7025
|
+
fchmodSync(fd, mode);
|
|
7026
|
+
if (opts.uid !== undefined) {
|
|
7027
|
+
try {
|
|
7028
|
+
fchownSync(fd, opts.uid, opts.gid ?? opts.uid);
|
|
7029
|
+
} catch (chownErr) {
|
|
7030
|
+
if (opts.onChownError)
|
|
7031
|
+
opts.onChownError(chownErr);
|
|
7032
|
+
else
|
|
7033
|
+
throw chownErr;
|
|
7034
|
+
}
|
|
7035
|
+
}
|
|
7023
7036
|
fsyncSync(fd);
|
|
7024
7037
|
closeSync(fd);
|
|
7025
7038
|
fd = null;
|
|
@@ -18221,9 +18234,10 @@ import {
|
|
|
18221
18234
|
chmodSync,
|
|
18222
18235
|
chownSync as chownSync2,
|
|
18223
18236
|
existsSync as existsSync8,
|
|
18224
|
-
lstatSync,
|
|
18237
|
+
lstatSync as lstatSync2,
|
|
18225
18238
|
mkdirSync as mkdirSync5,
|
|
18226
18239
|
readFileSync as readFileSync7,
|
|
18240
|
+
realpathSync as realpathSync3,
|
|
18227
18241
|
renameSync as renameSync3,
|
|
18228
18242
|
rmSync as rmSync5,
|
|
18229
18243
|
statSync as statSync6,
|
|
@@ -18233,7 +18247,7 @@ import {
|
|
|
18233
18247
|
import { closeSync as closeSync2, openSync as openSync2, writeSync as writeSync2 } from "node:fs";
|
|
18234
18248
|
import * as constants2 from "node:constants";
|
|
18235
18249
|
import { createHash as createHash2 } from "node:crypto";
|
|
18236
|
-
import { dirname as dirname3, join as join8, resolve as resolve9 } from "node:path";
|
|
18250
|
+
import { basename as basename2, dirname as dirname3, join as join8, resolve as resolve9 } from "node:path";
|
|
18237
18251
|
|
|
18238
18252
|
// src/agents/scaffold.ts
|
|
18239
18253
|
import { join as join4, resolve as resolve5 } from "node:path";
|
|
@@ -18343,7 +18357,7 @@ var HINDSIGHT_HEALTHCHECK_PY = 'import urllib.request,sys; sys.exit(0 if urllib.
|
|
|
18343
18357
|
var HINDSIGHT_HEALTHCHECK_CMD = `python3 -c '${HINDSIGHT_HEALTHCHECK_PY}'`;
|
|
18344
18358
|
|
|
18345
18359
|
// src/memory/hindsight.ts
|
|
18346
|
-
var DEFAULT_RETAIN_MISSION = "Extract user preferences, ongoing projects, recurring commitments, " + "important context, and durable facts that should help across future " + "conversations. Skip one-off chatter and temporary task noise.";
|
|
18360
|
+
var DEFAULT_RETAIN_MISSION = "Extract user preferences, ongoing projects, recurring commitments, " + "important context, and durable facts that should help across future " + "conversations. Skip one-off chatter and temporary task noise, " + "including in-flight workflow/process narration (a sub-task started, " + "paused, or is still running) — only retain the outcome once a task " + "actually completes or a decision is made.";
|
|
18347
18361
|
var PROFILE_MEMORY_DEFAULTS = {
|
|
18348
18362
|
"health-coach": {
|
|
18349
18363
|
disposition: { skepticism: 2, literalism: 2, empathy: 5 },
|
|
@@ -19198,6 +19212,187 @@ function bestPremiumHeadroomAccount(ledger, candidates, now) {
|
|
|
19198
19212
|
// src/auth/broker/server.ts
|
|
19199
19213
|
init_atomic();
|
|
19200
19214
|
|
|
19215
|
+
// src/litellm/external-spend.ts
|
|
19216
|
+
var EXTERNAL_SPEND_TOP_N = 3;
|
|
19217
|
+
var DEFAULT_LITELLM_BASE = "http://127.0.0.1:4010";
|
|
19218
|
+
var EXTERNAL_SPEND_CACHE_TTL_MS = 90000;
|
|
19219
|
+
var EXTERNAL_SPEND_FETCH_TIMEOUT_MS = 5000;
|
|
19220
|
+
var LITELLM_MASTER_KEY_STATE_BASENAME = "litellm-master-key";
|
|
19221
|
+
var BARE_EXTERNAL_NEEDLES = [
|
|
19222
|
+
"gpt-oss",
|
|
19223
|
+
"grok",
|
|
19224
|
+
"gemini",
|
|
19225
|
+
"deepseek",
|
|
19226
|
+
"kimi",
|
|
19227
|
+
"glm-",
|
|
19228
|
+
"qwen",
|
|
19229
|
+
"llama"
|
|
19230
|
+
];
|
|
19231
|
+
function isExternalModel(model) {
|
|
19232
|
+
const m = model.trim().toLowerCase();
|
|
19233
|
+
if (!m)
|
|
19234
|
+
return false;
|
|
19235
|
+
if (m.startsWith("claude"))
|
|
19236
|
+
return false;
|
|
19237
|
+
if (m.includes("anthropic/claude"))
|
|
19238
|
+
return false;
|
|
19239
|
+
if (m.startsWith("openrouter/"))
|
|
19240
|
+
return true;
|
|
19241
|
+
if (m.startsWith("sr-"))
|
|
19242
|
+
return true;
|
|
19243
|
+
for (const needle of BARE_EXTERNAL_NEEDLES) {
|
|
19244
|
+
if (m.includes(needle))
|
|
19245
|
+
return true;
|
|
19246
|
+
}
|
|
19247
|
+
return false;
|
|
19248
|
+
}
|
|
19249
|
+
function shortModelLabel(model) {
|
|
19250
|
+
let m = model.trim();
|
|
19251
|
+
if (m.toLowerCase().startsWith("openrouter/")) {
|
|
19252
|
+
m = m.slice("openrouter/".length);
|
|
19253
|
+
}
|
|
19254
|
+
const parts = m.split("/").filter(Boolean);
|
|
19255
|
+
if (parts.length >= 2) {
|
|
19256
|
+
m = parts[parts.length - 1];
|
|
19257
|
+
}
|
|
19258
|
+
if (m.toLowerCase().startsWith("sr-")) {
|
|
19259
|
+
m = m.slice(3);
|
|
19260
|
+
}
|
|
19261
|
+
return m || model.trim();
|
|
19262
|
+
}
|
|
19263
|
+
function utcDateString(d) {
|
|
19264
|
+
return d.toISOString().slice(0, 10);
|
|
19265
|
+
}
|
|
19266
|
+
function addUtcDays(dateStr, days) {
|
|
19267
|
+
const [y, m, d] = dateStr.split("-").map(Number);
|
|
19268
|
+
const dt = new Date(Date.UTC(y, m - 1, d));
|
|
19269
|
+
dt.setUTCDate(dt.getUTCDate() + days);
|
|
19270
|
+
return utcDateString(dt);
|
|
19271
|
+
}
|
|
19272
|
+
function rowDay(row) {
|
|
19273
|
+
const st = row.startTime;
|
|
19274
|
+
if (!st || typeof st !== "string")
|
|
19275
|
+
return null;
|
|
19276
|
+
return st.length >= 10 ? st.slice(0, 10) : null;
|
|
19277
|
+
}
|
|
19278
|
+
function externalModelsFromRow(row) {
|
|
19279
|
+
const out = {};
|
|
19280
|
+
const models = row.models ?? {};
|
|
19281
|
+
for (const [name, raw] of Object.entries(models)) {
|
|
19282
|
+
if (!isExternalModel(name))
|
|
19283
|
+
continue;
|
|
19284
|
+
const n = typeof raw === "number" ? raw : Number(raw);
|
|
19285
|
+
if (!Number.isFinite(n) || n === 0)
|
|
19286
|
+
continue;
|
|
19287
|
+
out[name] = (out[name] ?? 0) + n;
|
|
19288
|
+
}
|
|
19289
|
+
return out;
|
|
19290
|
+
}
|
|
19291
|
+
function summarizeExternalSpend(days, now = new Date) {
|
|
19292
|
+
const today = utcDateString(now);
|
|
19293
|
+
const start7 = addUtcDays(today, -6);
|
|
19294
|
+
let day24hUsd = 0;
|
|
19295
|
+
let day7dUsd = 0;
|
|
19296
|
+
const byModel = {};
|
|
19297
|
+
for (const row of days) {
|
|
19298
|
+
const day = rowDay(row);
|
|
19299
|
+
if (!day)
|
|
19300
|
+
continue;
|
|
19301
|
+
if (day < start7 || day > today)
|
|
19302
|
+
continue;
|
|
19303
|
+
const ext = externalModelsFromRow(row);
|
|
19304
|
+
let rowSum = 0;
|
|
19305
|
+
for (const [name, usd] of Object.entries(ext)) {
|
|
19306
|
+
rowSum += usd;
|
|
19307
|
+
byModel[name] = (byModel[name] ?? 0) + usd;
|
|
19308
|
+
}
|
|
19309
|
+
day7dUsd += rowSum;
|
|
19310
|
+
if (day === today)
|
|
19311
|
+
day24hUsd += rowSum;
|
|
19312
|
+
}
|
|
19313
|
+
const top = Object.entries(byModel).filter(([, usd]) => usd > 0).sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0])).slice(0, EXTERNAL_SPEND_TOP_N).map(([name, usd]) => ({ label: shortModelLabel(name), usd }));
|
|
19314
|
+
return { day24hUsd, day7dUsd, top };
|
|
19315
|
+
}
|
|
19316
|
+
function normalizeSpendLogRows(body) {
|
|
19317
|
+
if (Array.isArray(body))
|
|
19318
|
+
return body;
|
|
19319
|
+
if (body && typeof body === "object") {
|
|
19320
|
+
const data = body.data;
|
|
19321
|
+
if (Array.isArray(data))
|
|
19322
|
+
return data;
|
|
19323
|
+
}
|
|
19324
|
+
return [];
|
|
19325
|
+
}
|
|
19326
|
+
async function fetchAndSummarizeExternalSpend(opts) {
|
|
19327
|
+
const now = opts.now ?? new Date;
|
|
19328
|
+
const base = (opts.baseUrl ?? DEFAULT_LITELLM_BASE).replace(/\/+$/, "");
|
|
19329
|
+
const today = utcDateString(now);
|
|
19330
|
+
const start = addUtcDays(today, -6);
|
|
19331
|
+
const end = addUtcDays(today, 1);
|
|
19332
|
+
const url = `${base}/spend/logs?start_date=${encodeURIComponent(start)}` + `&end_date=${encodeURIComponent(end)}`;
|
|
19333
|
+
const fetchImpl = opts.fetchImpl ?? fetch;
|
|
19334
|
+
const timeoutMs = opts.timeoutMs ?? EXTERNAL_SPEND_FETCH_TIMEOUT_MS;
|
|
19335
|
+
const ac = new AbortController;
|
|
19336
|
+
const timer = setTimeout(() => ac.abort(), timeoutMs);
|
|
19337
|
+
try {
|
|
19338
|
+
const res = await fetchImpl(url, {
|
|
19339
|
+
method: "GET",
|
|
19340
|
+
headers: {
|
|
19341
|
+
Authorization: `Bearer ${opts.adminKey}`,
|
|
19342
|
+
Accept: "application/json"
|
|
19343
|
+
},
|
|
19344
|
+
signal: ac.signal
|
|
19345
|
+
});
|
|
19346
|
+
if (!res.ok)
|
|
19347
|
+
return null;
|
|
19348
|
+
let body;
|
|
19349
|
+
try {
|
|
19350
|
+
body = await res.json();
|
|
19351
|
+
} catch {
|
|
19352
|
+
return null;
|
|
19353
|
+
}
|
|
19354
|
+
return summarizeExternalSpend(normalizeSpendLogRows(body), now);
|
|
19355
|
+
} catch {
|
|
19356
|
+
return null;
|
|
19357
|
+
} finally {
|
|
19358
|
+
clearTimeout(timer);
|
|
19359
|
+
}
|
|
19360
|
+
}
|
|
19361
|
+
|
|
19362
|
+
// src/auth/broker/mirror-write.ts
|
|
19363
|
+
init_atomic();
|
|
19364
|
+
import { lstatSync, realpathSync as realpathSync2 } from "node:fs";
|
|
19365
|
+
function safeMirrorWrite(input) {
|
|
19366
|
+
let realParent;
|
|
19367
|
+
try {
|
|
19368
|
+
realParent = realpathSync2(input.parentDir);
|
|
19369
|
+
} catch (err) {
|
|
19370
|
+
input.logErr(`mirror parent realpath failed: ${err.message}`);
|
|
19371
|
+
return false;
|
|
19372
|
+
}
|
|
19373
|
+
if (realParent !== input.expectedRealParent) {
|
|
19374
|
+
input.refuse("parentDir", "realpath-mismatch");
|
|
19375
|
+
return false;
|
|
19376
|
+
}
|
|
19377
|
+
try {
|
|
19378
|
+
atomicWriteFileSync(input.targetPath, input.content, {
|
|
19379
|
+
mode: input.mode ?? 384,
|
|
19380
|
+
uid: input.uid,
|
|
19381
|
+
gid: input.gid ?? input.uid,
|
|
19382
|
+
onChownError: input.onChownError
|
|
19383
|
+
});
|
|
19384
|
+
return true;
|
|
19385
|
+
} catch (err) {
|
|
19386
|
+
const e = err;
|
|
19387
|
+
if (e.code === "ELOOP" || e.code === "EEXIST") {
|
|
19388
|
+
input.refuse("targetPath", `open-${e.code}`);
|
|
19389
|
+
return false;
|
|
19390
|
+
}
|
|
19391
|
+
input.logErr(`mirror write failed: ${e.message}`);
|
|
19392
|
+
return false;
|
|
19393
|
+
}
|
|
19394
|
+
}
|
|
19395
|
+
|
|
19201
19396
|
// src/auth/account-store.ts
|
|
19202
19397
|
init_atomic();
|
|
19203
19398
|
import {
|
|
@@ -19998,14 +20193,20 @@ class ProviderRegistry {
|
|
|
19998
20193
|
}
|
|
19999
20194
|
|
|
20000
20195
|
// src/auth/broker/peercred.ts
|
|
20001
|
-
var
|
|
20196
|
+
var AUTH_BROKER_ROOT = "/run/switchroom/auth-broker";
|
|
20197
|
+
function authBrokerSocketRe(socketRoot) {
|
|
20198
|
+
const escaped = socketRoot.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
20199
|
+
return new RegExp(`^${escaped}/([a-zA-Z0-9][a-zA-Z0-9_-]*)/sock$`);
|
|
20200
|
+
}
|
|
20201
|
+
var AUTH_BROKER_SOCKET_PATH_RE = authBrokerSocketRe(AUTH_BROKER_ROOT);
|
|
20002
20202
|
var AUTH_BROKER_MAX_NAME_LEN = 63;
|
|
20003
20203
|
var RESERVED_NAMES = new Set(["operator"]);
|
|
20004
|
-
function socketPathToName(socketPath) {
|
|
20005
|
-
|
|
20204
|
+
function socketPathToName(socketPath, socketRoot) {
|
|
20205
|
+
const pattern = socketRoot === undefined || socketRoot === AUTH_BROKER_ROOT ? AUTH_BROKER_SOCKET_PATH_RE : authBrokerSocketRe(socketRoot);
|
|
20206
|
+
return matchSocketName(socketPath, [pattern], AUTH_BROKER_MAX_NAME_LEN);
|
|
20006
20207
|
}
|
|
20007
|
-
function classify(socketPath, config) {
|
|
20008
|
-
const name = socketPathToName(socketPath);
|
|
20208
|
+
function classify(socketPath, config, socketRoot) {
|
|
20209
|
+
const name = socketPathToName(socketPath, socketRoot);
|
|
20009
20210
|
if (!name)
|
|
20010
20211
|
return null;
|
|
20011
20212
|
if (name === "operator")
|
|
@@ -20169,6 +20370,12 @@ var ClaimNotificationRequestSchema = exports_external.object({
|
|
|
20169
20370
|
key: exports_external.string().min(1).max(512),
|
|
20170
20371
|
windowMs: exports_external.number().int().positive().max(86400000)
|
|
20171
20372
|
});
|
|
20373
|
+
var GetExternalSpendRequestSchema = exports_external.object({
|
|
20374
|
+
v: exports_external.literal(PROTOCOL_VERSION),
|
|
20375
|
+
op: exports_external.literal("get-external-spend"),
|
|
20376
|
+
id: exports_external.string().min(1),
|
|
20377
|
+
forceLive: exports_external.boolean().optional()
|
|
20378
|
+
});
|
|
20172
20379
|
var RequestSchema2 = exports_external.discriminatedUnion("op", [
|
|
20173
20380
|
GetCredentialsRequestSchema,
|
|
20174
20381
|
ListStateRequestSchema,
|
|
@@ -20182,7 +20389,8 @@ var RequestSchema2 = exports_external.discriminatedUnion("op", [
|
|
|
20182
20389
|
ListGoogleAccountsRequestSchema,
|
|
20183
20390
|
ListMicrosoftAccountsRequestSchema,
|
|
20184
20391
|
ProbeQuotaRequestSchema,
|
|
20185
|
-
ClaimNotificationRequestSchema
|
|
20392
|
+
ClaimNotificationRequestSchema,
|
|
20393
|
+
GetExternalSpendRequestSchema
|
|
20186
20394
|
]);
|
|
20187
20395
|
var GetCredentialsDataSchema = exports_external.object({
|
|
20188
20396
|
account: exports_external.string(),
|
|
@@ -20193,6 +20401,8 @@ var AccountStateSchema = exports_external.object({
|
|
|
20193
20401
|
label: exports_external.string(),
|
|
20194
20402
|
expiresAt: exports_external.number().optional(),
|
|
20195
20403
|
exhausted: exports_external.boolean(),
|
|
20404
|
+
in_service: exports_external.boolean().optional(),
|
|
20405
|
+
entitlement_blocked: exports_external.boolean().optional(),
|
|
20196
20406
|
exhausted_until: exports_external.number().optional(),
|
|
20197
20407
|
throttled_until: exports_external.number().optional(),
|
|
20198
20408
|
threshold_violations: exports_external.number().int().nonnegative().optional(),
|
|
@@ -20249,6 +20459,18 @@ var SetOverrideDataSchema = exports_external.object({
|
|
|
20249
20459
|
var ClaimNotificationDataSchema = exports_external.object({
|
|
20250
20460
|
granted: exports_external.boolean()
|
|
20251
20461
|
});
|
|
20462
|
+
var GetExternalSpendDataSchema = exports_external.object({
|
|
20463
|
+
available: exports_external.boolean(),
|
|
20464
|
+
day24hUsd: exports_external.number().optional(),
|
|
20465
|
+
day7dUsd: exports_external.number().optional(),
|
|
20466
|
+
top: exports_external.array(exports_external.object({
|
|
20467
|
+
label: exports_external.string(),
|
|
20468
|
+
usd: exports_external.number()
|
|
20469
|
+
})).optional(),
|
|
20470
|
+
capturedAtMs: exports_external.number().int().nonnegative().optional(),
|
|
20471
|
+
served: exports_external.enum(["live", "cache"]).optional(),
|
|
20472
|
+
reason: exports_external.string().optional()
|
|
20473
|
+
});
|
|
20252
20474
|
var GoogleAccountStateSchema = exports_external.object({
|
|
20253
20475
|
account: exports_external.string(),
|
|
20254
20476
|
expiresAt: exports_external.number(),
|
|
@@ -20329,13 +20551,14 @@ function encodeError(id, code, message) {
|
|
|
20329
20551
|
}
|
|
20330
20552
|
|
|
20331
20553
|
// src/auth/broker/server.ts
|
|
20332
|
-
var
|
|
20554
|
+
var AUTH_BROKER_ROOT2 = "/run/switchroom/auth-broker";
|
|
20333
20555
|
var REFRESH_TICK_INTERVAL_MS = 60 * 1000;
|
|
20334
20556
|
var MARK_EXHAUSTED_DEFAULT_MS = 5 * 60 * 60 * 1000;
|
|
20335
20557
|
var MARK_THROTTLED_MAX_MS = 30 * 60 * 1000;
|
|
20336
20558
|
var THROTTLE_ESCALATION_HITS = 3;
|
|
20337
20559
|
var THROTTLE_ESCALATION_WINDOW_MS = 10 * 60 * 1000;
|
|
20338
20560
|
var MARK_THROTTLED_MIN_INTERVAL_MS = 5 * 1000;
|
|
20561
|
+
var MARK_CORROBORATION_PROBE_DELAY_MS = 90 * 1000;
|
|
20339
20562
|
var AUDIT_ROTATE_BYTES = 10 * 1024 * 1024;
|
|
20340
20563
|
var AUDIT_KEEP = 5;
|
|
20341
20564
|
var AUDIT_LINE_MAX = 4000;
|
|
@@ -20412,6 +20635,8 @@ class AuthBroker {
|
|
|
20412
20635
|
refreshTimer = null;
|
|
20413
20636
|
consumerProbeTimer = null;
|
|
20414
20637
|
fleetProbeTimer = null;
|
|
20638
|
+
markCorroborationTimers = new Set;
|
|
20639
|
+
markCorroborationDelayMs = MARK_CORROBORATION_PROBE_DELAY_MS;
|
|
20415
20640
|
fetchQuotaImpl;
|
|
20416
20641
|
stateDir;
|
|
20417
20642
|
socketRoot;
|
|
@@ -20422,6 +20647,8 @@ class AuthBroker {
|
|
|
20422
20647
|
providers;
|
|
20423
20648
|
quota = {};
|
|
20424
20649
|
lastQuotaCache = {};
|
|
20650
|
+
externalSpendCache = null;
|
|
20651
|
+
externalSpendInFlight = null;
|
|
20425
20652
|
probeInFlight = new Map;
|
|
20426
20653
|
shaIndex = {};
|
|
20427
20654
|
thresholdViolations = {};
|
|
@@ -20446,8 +20673,9 @@ class AuthBroker {
|
|
|
20446
20673
|
this.operatorUid = opts.operatorUid;
|
|
20447
20674
|
this.fetcher = opts.fetcher;
|
|
20448
20675
|
this.fetchQuotaImpl = opts._testFetchQuota ?? fetchQuota;
|
|
20676
|
+
this.markCorroborationDelayMs = opts._testCorroborationDelayMs ?? MARK_CORROBORATION_PROBE_DELAY_MS;
|
|
20449
20677
|
this.stateDir = opts.stateDir ?? resolve9(this.homeRoot(), ".switchroom", "state", "auth-broker");
|
|
20450
|
-
this.socketRoot = opts.socketRoot ??
|
|
20678
|
+
this.socketRoot = opts.socketRoot ?? AUTH_BROKER_ROOT2;
|
|
20451
20679
|
this.providers = new ProviderRegistry;
|
|
20452
20680
|
this.providers.register(new AnthropicProvider);
|
|
20453
20681
|
const googleClientId = config.google_workspace?.google_client_id;
|
|
@@ -20547,10 +20775,18 @@ class AuthBroker {
|
|
|
20547
20775
|
clearInterval(this.fleetProbeTimer);
|
|
20548
20776
|
this.fleetProbeTimer = null;
|
|
20549
20777
|
}
|
|
20778
|
+
for (const timer of this.markCorroborationTimers)
|
|
20779
|
+
clearTimeout(timer);
|
|
20780
|
+
this.markCorroborationTimers.clear();
|
|
20550
20781
|
for (const [sock, lis] of this.listeners) {
|
|
20551
20782
|
try {
|
|
20552
20783
|
lis.server.close();
|
|
20553
20784
|
} catch {}
|
|
20785
|
+
for (const s of lis.sockets) {
|
|
20786
|
+
try {
|
|
20787
|
+
s.destroy();
|
|
20788
|
+
} catch {}
|
|
20789
|
+
}
|
|
20554
20790
|
try {
|
|
20555
20791
|
if (existsSync8(sock))
|
|
20556
20792
|
unlinkSync(sock);
|
|
@@ -20579,7 +20815,12 @@ class AuthBroker {
|
|
|
20579
20815
|
if (this.operatorUid !== undefined) {
|
|
20580
20816
|
wanted.add(this.operatorSocketPath());
|
|
20581
20817
|
}
|
|
20818
|
+
const prevShape = configToShape(prev);
|
|
20819
|
+
const nextShape = configToShape(config);
|
|
20582
20820
|
for (const [sock, lis] of [...this.listeners]) {
|
|
20821
|
+
const before = classify(lis.socketPath, prevShape, this.socketRoot);
|
|
20822
|
+
const after = classify(lis.socketPath, nextShape, this.socketRoot);
|
|
20823
|
+
const identityChanged = JSON.stringify(before ?? null) !== JSON.stringify(after ?? null);
|
|
20583
20824
|
if (!wanted.has(sock)) {
|
|
20584
20825
|
try {
|
|
20585
20826
|
lis.server.close();
|
|
@@ -20588,7 +20829,18 @@ class AuthBroker {
|
|
|
20588
20829
|
if (existsSync8(sock))
|
|
20589
20830
|
unlinkSync(sock);
|
|
20590
20831
|
} catch {}
|
|
20832
|
+
for (const s of lis.sockets) {
|
|
20833
|
+
try {
|
|
20834
|
+
s.destroy();
|
|
20835
|
+
} catch {}
|
|
20836
|
+
}
|
|
20591
20837
|
this.listeners.delete(sock);
|
|
20838
|
+
} else if (identityChanged) {
|
|
20839
|
+
for (const s of lis.sockets) {
|
|
20840
|
+
try {
|
|
20841
|
+
s.destroy();
|
|
20842
|
+
} catch {}
|
|
20843
|
+
}
|
|
20592
20844
|
}
|
|
20593
20845
|
}
|
|
20594
20846
|
for (const name of Object.keys(config.agents ?? {})) {
|
|
@@ -20676,12 +20928,24 @@ class AuthBroker {
|
|
|
20676
20928
|
try {
|
|
20677
20929
|
chownSync2(dir, targetUid, targetUid);
|
|
20678
20930
|
} catch {}
|
|
20679
|
-
this.listeners.set(sockPath, {
|
|
20931
|
+
this.listeners.set(sockPath, {
|
|
20932
|
+
server,
|
|
20933
|
+
identity,
|
|
20934
|
+
socketPath: sockPath,
|
|
20935
|
+
sockets: new Set
|
|
20936
|
+
});
|
|
20680
20937
|
resolveP();
|
|
20681
20938
|
});
|
|
20682
20939
|
});
|
|
20683
20940
|
}
|
|
20684
20941
|
handleConnection(socket, sockPath, boundIdentity) {
|
|
20942
|
+
const listener = this.listeners.get(sockPath);
|
|
20943
|
+
if (listener) {
|
|
20944
|
+
listener.sockets.add(socket);
|
|
20945
|
+
socket.on("close", () => {
|
|
20946
|
+
listener.sockets.delete(socket);
|
|
20947
|
+
});
|
|
20948
|
+
}
|
|
20685
20949
|
let buf = "";
|
|
20686
20950
|
socket.on("data", (chunk) => {
|
|
20687
20951
|
buf += chunk.toString("utf-8");
|
|
@@ -20720,7 +20984,17 @@ class AuthBroker {
|
|
|
20720
20984
|
socket.write(encodeError(reqId, "INVALID_ARGS", err.message));
|
|
20721
20985
|
return;
|
|
20722
20986
|
}
|
|
20723
|
-
const identity = classify(sockPath, configToShape(this.config))
|
|
20987
|
+
const identity = classify(sockPath, configToShape(this.config), this.socketRoot);
|
|
20988
|
+
if (!identity) {
|
|
20989
|
+
this.audit({
|
|
20990
|
+
op: "stale-identity-rejected",
|
|
20991
|
+
identity: boundIdentity,
|
|
20992
|
+
ok: false,
|
|
20993
|
+
error: "FORBIDDEN"
|
|
20994
|
+
});
|
|
20995
|
+
socket.end(encodeError(reqId, "FORBIDDEN", "identity no longer configured"));
|
|
20996
|
+
return;
|
|
20997
|
+
}
|
|
20724
20998
|
try {
|
|
20725
20999
|
switch (req.op) {
|
|
20726
21000
|
case "get-credentials": {
|
|
@@ -20840,6 +21114,9 @@ class AuthBroker {
|
|
|
20840
21114
|
case "claim-notification":
|
|
20841
21115
|
this.opClaimNotification(socket, reqId, identity, req.key, req.windowMs);
|
|
20842
21116
|
break;
|
|
21117
|
+
case "get-external-spend":
|
|
21118
|
+
await this.opGetExternalSpend(socket, reqId, identity, req.forceLive);
|
|
21119
|
+
break;
|
|
20843
21120
|
}
|
|
20844
21121
|
} catch (err) {
|
|
20845
21122
|
socket.write(encodeError(reqId, "INTERNAL", err.message));
|
|
@@ -21166,6 +21443,7 @@ class AuthBroker {
|
|
|
21166
21443
|
}
|
|
21167
21444
|
async opListState(socket, id, identity) {
|
|
21168
21445
|
const auth = this.config.auth ?? {};
|
|
21446
|
+
const inService = this.inServiceSet();
|
|
21169
21447
|
const accounts = listAccounts(this.home).map((label) => {
|
|
21170
21448
|
const creds = readAccountCredentials(label, this.home);
|
|
21171
21449
|
const meta = readAccountMeta(label, this.home);
|
|
@@ -21176,6 +21454,8 @@ class AuthBroker {
|
|
|
21176
21454
|
label,
|
|
21177
21455
|
expiresAt: creds?.claudeAiOauth?.expiresAt,
|
|
21178
21456
|
exhausted,
|
|
21457
|
+
in_service: inService.has(label),
|
|
21458
|
+
entitlement_blocked: false,
|
|
21179
21459
|
exhausted_until: q?.exhausted_until,
|
|
21180
21460
|
throttled_until: q?.throttled_until,
|
|
21181
21461
|
threshold_violations: this.thresholdViolations[label] ?? 0,
|
|
@@ -21425,6 +21705,14 @@ class AuthBroker {
|
|
|
21425
21705
|
set.add(acct);
|
|
21426
21706
|
return set;
|
|
21427
21707
|
}
|
|
21708
|
+
inServiceSet() {
|
|
21709
|
+
const set = this.premiumCanarySet();
|
|
21710
|
+
for (const c of this.config.auth?.consumers ?? []) {
|
|
21711
|
+
if (c.account)
|
|
21712
|
+
set.add(c.account);
|
|
21713
|
+
}
|
|
21714
|
+
return set;
|
|
21715
|
+
}
|
|
21428
21716
|
pinnedAgentAccounts() {
|
|
21429
21717
|
return new Set(Object.values(this.config.agents ?? {}).map((a) => a.auth?.override).filter((x) => typeof x === "string" && x.length > 0));
|
|
21430
21718
|
}
|
|
@@ -21612,6 +21900,9 @@ class AuthBroker {
|
|
|
21612
21900
|
});
|
|
21613
21901
|
this.quota[account] = { ...this.quota[account], exhausted_until: exhaustedUntil, marked_at: now };
|
|
21614
21902
|
this.persistQuota();
|
|
21903
|
+
if (exhaustedUntil > now + MARK_EXHAUSTED_DEFAULT_MS) {
|
|
21904
|
+
this.scheduleMarkCorroboration(account, now, identity);
|
|
21905
|
+
}
|
|
21615
21906
|
const rolledTo = await this.nextHealthyAccountLive(account, this.config.auth?.fallback_order ?? []);
|
|
21616
21907
|
const rolled = this.fanoutFailoverTo(account, rolledTo);
|
|
21617
21908
|
this.fanoutToAffectedConsumers(account);
|
|
@@ -21628,6 +21919,67 @@ class AuthBroker {
|
|
|
21628
21919
|
}
|
|
21629
21920
|
return { rolled, rolledTo };
|
|
21630
21921
|
}
|
|
21922
|
+
scheduleMarkCorroboration(account, markedAt, identity) {
|
|
21923
|
+
if (this.markCorroborationDelayMs <= 0)
|
|
21924
|
+
return;
|
|
21925
|
+
const timer = setTimeout(() => {
|
|
21926
|
+
this.markCorroborationTimers.delete(timer);
|
|
21927
|
+
this.corroborateExhaustionMark(account, markedAt, identity).catch((err) => {
|
|
21928
|
+
this.logErr(`mark-corroboration ${account}: ${err.message}`);
|
|
21929
|
+
});
|
|
21930
|
+
}, this.markCorroborationDelayMs);
|
|
21931
|
+
timer.unref?.();
|
|
21932
|
+
this.markCorroborationTimers.add(timer);
|
|
21933
|
+
}
|
|
21934
|
+
async corroborateExhaustionMark(account, markedAt, identity) {
|
|
21935
|
+
const entry = this.quota[account];
|
|
21936
|
+
if (!entry || entry.marked_at !== markedAt) {
|
|
21937
|
+
return { clamped: false, reason: "mark-superseded" };
|
|
21938
|
+
}
|
|
21939
|
+
const until = entry.exhausted_until;
|
|
21940
|
+
const now = this.now();
|
|
21941
|
+
if (until == null || until <= now + MARK_EXHAUSTED_DEFAULT_MS) {
|
|
21942
|
+
return { clamped: false, reason: "not-long" };
|
|
21943
|
+
}
|
|
21944
|
+
const creds = readAccountCredentials(account, this.home);
|
|
21945
|
+
const token = creds?.claudeAiOauth?.accessToken;
|
|
21946
|
+
if (!token)
|
|
21947
|
+
return { clamped: false, reason: "no-token" };
|
|
21948
|
+
let result;
|
|
21949
|
+
try {
|
|
21950
|
+
result = await this.fetchQuotaImpl({ accessToken: token });
|
|
21951
|
+
} catch (err) {
|
|
21952
|
+
this.logErr(`mark-corroboration probe ${account}: ${err.message}`);
|
|
21953
|
+
return { clamped: false, reason: "probe-failed" };
|
|
21954
|
+
}
|
|
21955
|
+
this.cacheQuotaSnapshot(account, result);
|
|
21956
|
+
const snapshot = this.lastQuotaCache[account];
|
|
21957
|
+
const clampedUntil = clampMarkExpiry({
|
|
21958
|
+
proposedUntil: until,
|
|
21959
|
+
now,
|
|
21960
|
+
shortMs: MARK_EXHAUSTED_DEFAULT_MS,
|
|
21961
|
+
snapshot
|
|
21962
|
+
});
|
|
21963
|
+
if (clampedUntil >= until) {
|
|
21964
|
+
return { clamped: false, reason: "not-contradicted" };
|
|
21965
|
+
}
|
|
21966
|
+
const live = this.quota[account];
|
|
21967
|
+
if (!live || live.marked_at !== markedAt || (live.exhausted_until ?? 0) <= clampedUntil) {
|
|
21968
|
+
return { clamped: false, reason: "mark-superseded" };
|
|
21969
|
+
}
|
|
21970
|
+
this.quota[account] = { ...live, exhausted_until: clampedUntil };
|
|
21971
|
+
this.persistQuota();
|
|
21972
|
+
this.audit({
|
|
21973
|
+
op: "mark-corroboration-clamp",
|
|
21974
|
+
identity,
|
|
21975
|
+
account,
|
|
21976
|
+
accountKind: "claude",
|
|
21977
|
+
ok: true
|
|
21978
|
+
});
|
|
21979
|
+
process.stdout.write(`auth-broker: mark-corroboration probe CONTRADICTS ${account} weekly wall — ` + `clamped exhausted_until ${new Date(until).toISOString()} → ` + `${new Date(clampedUntil).toISOString()}
|
|
21980
|
+
`);
|
|
21981
|
+
return { clamped: true, reason: "contradicted", until: clampedUntil };
|
|
21982
|
+
}
|
|
21631
21983
|
exhaustionLiveCorroborated(account) {
|
|
21632
21984
|
const snapshot = this.lastQuotaCache[account];
|
|
21633
21985
|
if (!snapshot || !snapshotFresh(snapshot, this.now(), MARK_EXHAUSTED_DEFAULT_MS))
|
|
@@ -21636,6 +21988,105 @@ class AuthBroker {
|
|
|
21636
21988
|
return false;
|
|
21637
21989
|
return !overageLiftsWall(snapshot, this.isOverageAllowed(account));
|
|
21638
21990
|
}
|
|
21991
|
+
async opGetExternalSpend(socket, id, _identity, forceLive) {
|
|
21992
|
+
const nowMs2 = this.now();
|
|
21993
|
+
const ttl = EXTERNAL_SPEND_CACHE_TTL_MS;
|
|
21994
|
+
const fresh = this.externalSpendCache != null && nowMs2 - this.externalSpendCache.capturedAtMs < ttl;
|
|
21995
|
+
if (forceLive || !fresh) {
|
|
21996
|
+
try {
|
|
21997
|
+
await this.refreshExternalSpend(forceLive === true);
|
|
21998
|
+
} catch (err) {
|
|
21999
|
+
this.logErr(`external-spend refresh failed: ${err?.message ?? err}`);
|
|
22000
|
+
}
|
|
22001
|
+
}
|
|
22002
|
+
if (!this.externalSpendCache) {
|
|
22003
|
+
socket.write(encodeSuccess(id, {
|
|
22004
|
+
available: false,
|
|
22005
|
+
reason: this.resolveLitellmMasterKey() ? "litellm_unreachable" : "master_key_unavailable"
|
|
22006
|
+
}));
|
|
22007
|
+
return;
|
|
22008
|
+
}
|
|
22009
|
+
const recently = nowMs2 - this.externalSpendCache.capturedAtMs < 5000;
|
|
22010
|
+
const outServed = recently && (forceLive || !fresh) ? "live" : "cache";
|
|
22011
|
+
const s = this.externalSpendCache.summary;
|
|
22012
|
+
socket.write(encodeSuccess(id, {
|
|
22013
|
+
available: true,
|
|
22014
|
+
day24hUsd: s.day24hUsd,
|
|
22015
|
+
day7dUsd: s.day7dUsd,
|
|
22016
|
+
top: s.top,
|
|
22017
|
+
capturedAtMs: this.externalSpendCache.capturedAtMs,
|
|
22018
|
+
served: outServed
|
|
22019
|
+
}));
|
|
22020
|
+
}
|
|
22021
|
+
resolveLitellmMasterKey() {
|
|
22022
|
+
if (this.opts._testLitellmMasterKey !== undefined) {
|
|
22023
|
+
const v = this.opts._testLitellmMasterKey;
|
|
22024
|
+
return v && v.trim() ? v.trim() : null;
|
|
22025
|
+
}
|
|
22026
|
+
const envKey = (process.env.SWITCHROOM_LITELLM_ADMIN_KEY ?? process.env.LITELLM_MASTER_KEY ?? "").trim();
|
|
22027
|
+
if (envKey)
|
|
22028
|
+
return envKey;
|
|
22029
|
+
try {
|
|
22030
|
+
const path = join8(this.stateDir, LITELLM_MASTER_KEY_STATE_BASENAME);
|
|
22031
|
+
if (!existsSync8(path))
|
|
22032
|
+
return null;
|
|
22033
|
+
const raw = readFileSync7(path, "utf-8").trim();
|
|
22034
|
+
return raw.length > 0 ? raw : null;
|
|
22035
|
+
} catch {
|
|
22036
|
+
return null;
|
|
22037
|
+
}
|
|
22038
|
+
}
|
|
22039
|
+
resolveLitellmBaseUrl() {
|
|
22040
|
+
const fromCfg = this.config.litellm?.base_url;
|
|
22041
|
+
const raw = (process.env.SWITCHROOM_LITELLM_BASE ?? fromCfg ?? DEFAULT_LITELLM_BASE).trim();
|
|
22042
|
+
return raw.replace(/\/+$/, "") || DEFAULT_LITELLM_BASE;
|
|
22043
|
+
}
|
|
22044
|
+
async refreshExternalSpend(forceLive) {
|
|
22045
|
+
while (this.externalSpendInFlight) {
|
|
22046
|
+
await this.externalSpendInFlight;
|
|
22047
|
+
if (!forceLive)
|
|
22048
|
+
return;
|
|
22049
|
+
const justNow = this.now();
|
|
22050
|
+
if (this.externalSpendCache && justNow - this.externalSpendCache.capturedAtMs < 5000) {
|
|
22051
|
+
return;
|
|
22052
|
+
}
|
|
22053
|
+
}
|
|
22054
|
+
const run = (async () => {
|
|
22055
|
+
const nowMs2 = this.now();
|
|
22056
|
+
if (!forceLive && this.externalSpendCache && nowMs2 - this.externalSpendCache.capturedAtMs < EXTERNAL_SPEND_CACHE_TTL_MS) {
|
|
22057
|
+
return;
|
|
22058
|
+
}
|
|
22059
|
+
const key = this.resolveLitellmMasterKey();
|
|
22060
|
+
if (!key)
|
|
22061
|
+
return;
|
|
22062
|
+
const baseUrl = this.resolveLitellmBaseUrl();
|
|
22063
|
+
const now = new Date(nowMs2);
|
|
22064
|
+
let summary = null;
|
|
22065
|
+
if (this.opts._testFetchExternalSpend) {
|
|
22066
|
+
summary = await this.opts._testFetchExternalSpend({
|
|
22067
|
+
adminKey: key,
|
|
22068
|
+
baseUrl,
|
|
22069
|
+
now,
|
|
22070
|
+
forceLive
|
|
22071
|
+
});
|
|
22072
|
+
} else {
|
|
22073
|
+
summary = await fetchAndSummarizeExternalSpend({
|
|
22074
|
+
adminKey: key,
|
|
22075
|
+
baseUrl,
|
|
22076
|
+
now,
|
|
22077
|
+
timeoutMs: EXTERNAL_SPEND_FETCH_TIMEOUT_MS
|
|
22078
|
+
});
|
|
22079
|
+
}
|
|
22080
|
+
if (!summary)
|
|
22081
|
+
return;
|
|
22082
|
+
this.externalSpendCache = { summary, capturedAtMs: this.now() };
|
|
22083
|
+
this.persistExternalSpendCache();
|
|
22084
|
+
})();
|
|
22085
|
+
this.externalSpendInFlight = run.finally(() => {
|
|
22086
|
+
this.externalSpendInFlight = null;
|
|
22087
|
+
});
|
|
22088
|
+
await this.externalSpendInFlight;
|
|
22089
|
+
}
|
|
21639
22090
|
opClaimNotification(socket, id, identity, key, windowMs) {
|
|
21640
22091
|
const now = this.now();
|
|
21641
22092
|
const prev = this.notificationClaims[key];
|
|
@@ -22201,25 +22652,68 @@ class AuthBroker {
|
|
|
22201
22652
|
const mirrorDir = consumer.mirror_dir;
|
|
22202
22653
|
if (!mirrorDir)
|
|
22203
22654
|
return false;
|
|
22204
|
-
const targetPath = join8(mirrorDir, ".credentials.json");
|
|
22205
22655
|
const credsPath = accountCredentialsPath(label, this.home);
|
|
22206
22656
|
if (!existsSync8(credsPath))
|
|
22207
22657
|
return false;
|
|
22208
22658
|
const mirrorContent = enrichMirrorContent(readFileSync7(credsPath, "utf-8"));
|
|
22209
|
-
|
|
22210
|
-
|
|
22211
|
-
|
|
22659
|
+
const safe = this.resolveConsumerMirrorPathsSafe(consumer.name, mirrorDir);
|
|
22660
|
+
if (!safe)
|
|
22661
|
+
return false;
|
|
22662
|
+
return safeMirrorWrite({
|
|
22663
|
+
parentDir: safe.mirrorDir,
|
|
22664
|
+
expectedRealParent: safe.expectedRealParent,
|
|
22665
|
+
targetPath: safe.targetPath,
|
|
22666
|
+
content: mirrorContent,
|
|
22667
|
+
uid: consumer.uid ?? 0,
|
|
22668
|
+
onChownError: (err) => this.warnCapChownMissing(err),
|
|
22669
|
+
refuse: (component, reason) => this.auditMirrorRefused({ kind: "consumer", name: consumer.name }, component, reason),
|
|
22670
|
+
logErr: (msg) => this.logErr(`consumer-mirror ${consumer.name} <- ${label}: ${msg}`)
|
|
22671
|
+
});
|
|
22672
|
+
}
|
|
22673
|
+
resolveConsumerMirrorPathsSafe(consumerName, mirrorDir) {
|
|
22674
|
+
const identity = { kind: "consumer", name: consumerName };
|
|
22675
|
+
const refuse = (component, reason) => {
|
|
22676
|
+
this.auditMirrorRefused(identity, component, reason);
|
|
22677
|
+
return null;
|
|
22678
|
+
};
|
|
22679
|
+
const parent = dirname3(mirrorDir);
|
|
22680
|
+
const mirrorStat = this.lstatOrNull(mirrorDir);
|
|
22681
|
+
if (mirrorStat) {
|
|
22682
|
+
if (mirrorStat.isSymbolicLink())
|
|
22683
|
+
return refuse("mirror_dir", "is-a-symlink");
|
|
22684
|
+
if (!mirrorStat.isDirectory())
|
|
22685
|
+
return refuse("mirror_dir", "is-not-a-directory");
|
|
22686
|
+
} else {
|
|
22687
|
+
const parentStat = this.lstatOrNull(parent);
|
|
22688
|
+
if (!parentStat)
|
|
22689
|
+
return refuse("mirror_dir-parent", "does-not-exist");
|
|
22690
|
+
if (parentStat.isSymbolicLink())
|
|
22691
|
+
return refuse("mirror_dir-parent", "is-a-symlink");
|
|
22692
|
+
if (!parentStat.isDirectory())
|
|
22693
|
+
return refuse("mirror_dir-parent", "is-not-a-directory");
|
|
22212
22694
|
try {
|
|
22213
|
-
|
|
22214
|
-
chownSync2(targetPath, uid, uid);
|
|
22695
|
+
mkdirSync5(mirrorDir, { recursive: false, mode: 448 });
|
|
22215
22696
|
} catch (err) {
|
|
22216
|
-
this.
|
|
22697
|
+
this.logErr(`consumer-mirror ${consumerName}: mkdir ${mirrorDir}: ${err.message}`);
|
|
22698
|
+
return null;
|
|
22217
22699
|
}
|
|
22218
|
-
|
|
22700
|
+
}
|
|
22701
|
+
const targetPath = join8(mirrorDir, ".credentials.json");
|
|
22702
|
+
const targetStat = this.lstatOrNull(targetPath);
|
|
22703
|
+
if (targetStat) {
|
|
22704
|
+
if (targetStat.isSymbolicLink())
|
|
22705
|
+
return refuse(".credentials.json", "is-a-symlink");
|
|
22706
|
+
if (!targetStat.isFile())
|
|
22707
|
+
return refuse(".credentials.json", "is-not-a-regular-file");
|
|
22708
|
+
}
|
|
22709
|
+
let expectedRealParent;
|
|
22710
|
+
try {
|
|
22711
|
+
expectedRealParent = join8(realpathSync3(parent), basename2(mirrorDir));
|
|
22219
22712
|
} catch (err) {
|
|
22220
|
-
this.logErr(`consumer-mirror ${
|
|
22221
|
-
return
|
|
22713
|
+
this.logErr(`consumer-mirror ${consumerName}: realpath ${parent}: ${err.message}`);
|
|
22714
|
+
return null;
|
|
22222
22715
|
}
|
|
22716
|
+
return { mirrorDir, targetPath, expectedRealParent };
|
|
22223
22717
|
}
|
|
22224
22718
|
fanoutFailoverTo(label, next) {
|
|
22225
22719
|
const auth = this.config.auth ?? {};
|
|
@@ -22274,13 +22768,7 @@ class AuthBroker {
|
|
|
22274
22768
|
const claudeDir = join8(agentDir, ".claude");
|
|
22275
22769
|
const targetPath = join8(claudeDir, ".credentials.json");
|
|
22276
22770
|
const refuse = (component, reason) => {
|
|
22277
|
-
this.
|
|
22278
|
-
this.audit({
|
|
22279
|
-
op: "mirror-symlink-refused",
|
|
22280
|
-
identity: { kind: "agent", name: agentName, admin: false },
|
|
22281
|
-
ok: false,
|
|
22282
|
-
error: `${component}:${reason}`
|
|
22283
|
-
});
|
|
22771
|
+
this.auditMirrorRefused({ kind: "agent", name: agentName, admin: false }, component, reason);
|
|
22284
22772
|
return null;
|
|
22285
22773
|
};
|
|
22286
22774
|
const agentsStat = this.lstatOrNull(agentsDir);
|
|
@@ -22311,15 +22799,26 @@ class AuthBroker {
|
|
|
22311
22799
|
if (!targetStat.isFile())
|
|
22312
22800
|
return refuse(".credentials.json", "is-not-a-regular-file");
|
|
22313
22801
|
}
|
|
22314
|
-
|
|
22802
|
+
const expectedRealParent = join8(realpathSync3(agentsDir), agentName, ".claude");
|
|
22803
|
+
return { agentDir, claudeDir, targetPath, expectedRealParent };
|
|
22315
22804
|
}
|
|
22316
22805
|
lstatOrNull(path) {
|
|
22317
22806
|
try {
|
|
22318
|
-
return
|
|
22807
|
+
return lstatSync2(path);
|
|
22319
22808
|
} catch {
|
|
22320
22809
|
return null;
|
|
22321
22810
|
}
|
|
22322
22811
|
}
|
|
22812
|
+
auditMirrorRefused(identity, component, reason) {
|
|
22813
|
+
const who = identity.kind === "operator" ? "operator" : identity.name;
|
|
22814
|
+
this.logErr(`mirror ${who}: REFUSING mirror — ${component} ${reason} ` + `(symlink-guard #1393; peer-owned tree may be attacker-poisoned)`);
|
|
22815
|
+
this.audit({
|
|
22816
|
+
op: "mirror-symlink-refused",
|
|
22817
|
+
identity,
|
|
22818
|
+
ok: false,
|
|
22819
|
+
error: `${component}:${reason}`
|
|
22820
|
+
});
|
|
22821
|
+
}
|
|
22323
22822
|
mirrorAccountToAgent(label, agentName) {
|
|
22324
22823
|
const credsPath = accountCredentialsPath(label, this.home);
|
|
22325
22824
|
if (!existsSync8(credsPath))
|
|
@@ -22328,21 +22827,18 @@ class AuthBroker {
|
|
|
22328
22827
|
const safe = this.resolveMirrorPathsSafe(agentName);
|
|
22329
22828
|
if (!safe)
|
|
22330
22829
|
return false;
|
|
22331
|
-
const { claudeDir, targetPath } = safe;
|
|
22830
|
+
const { claudeDir, targetPath, expectedRealParent } = safe;
|
|
22332
22831
|
mkdirSync5(claudeDir, { recursive: true, mode: 448 });
|
|
22333
|
-
|
|
22334
|
-
|
|
22335
|
-
|
|
22336
|
-
|
|
22337
|
-
|
|
22338
|
-
|
|
22339
|
-
|
|
22340
|
-
}
|
|
22341
|
-
|
|
22342
|
-
}
|
|
22343
|
-
this.logErr(`fanout ${agentName} <- ${label}: ${err.message}`);
|
|
22344
|
-
return false;
|
|
22345
|
-
}
|
|
22832
|
+
return safeMirrorWrite({
|
|
22833
|
+
parentDir: claudeDir,
|
|
22834
|
+
expectedRealParent,
|
|
22835
|
+
targetPath,
|
|
22836
|
+
content: mirrorContent,
|
|
22837
|
+
uid: allocateAgentUid(agentName),
|
|
22838
|
+
onChownError: (err) => this.warnCapChownMissing(err),
|
|
22839
|
+
refuse: (component, reason) => this.auditMirrorRefused({ kind: "agent", name: agentName, admin: false }, component, reason),
|
|
22840
|
+
logErr: (msg) => this.logErr(`fanout ${agentName} <- ${label}: ${msg}`)
|
|
22841
|
+
});
|
|
22346
22842
|
}
|
|
22347
22843
|
loadStateFromDisk() {
|
|
22348
22844
|
this.quota = this.readJson("quota.json") ?? {};
|
|
@@ -22354,6 +22850,22 @@ class AuthBroker {
|
|
|
22354
22850
|
this.lastTierQuotaCache = this.readJson("last-tier-quota.json") ?? {};
|
|
22355
22851
|
this.premiumTierAllWalled = this.readJson("premium-tier-all-walled.json") ?? null;
|
|
22356
22852
|
this.usageLedger = this.readJson("usage-ledger.json") ?? {};
|
|
22853
|
+
{
|
|
22854
|
+
const es = this.readJson("external-spend.json");
|
|
22855
|
+
if (es && es.summary && typeof es.capturedAtMs === "number" && Number.isFinite(es.summary.day24hUsd) && Number.isFinite(es.summary.day7dUsd) && Array.isArray(es.summary.top) && es.summary.top.every((t) => t && typeof t.label === "string" && typeof t.usd === "number" && Number.isFinite(t.usd))) {
|
|
22856
|
+
this.externalSpendCache = {
|
|
22857
|
+
summary: {
|
|
22858
|
+
day24hUsd: es.summary.day24hUsd,
|
|
22859
|
+
day7dUsd: es.summary.day7dUsd,
|
|
22860
|
+
top: es.summary.top.map((t) => ({
|
|
22861
|
+
label: t.label,
|
|
22862
|
+
usd: t.usd
|
|
22863
|
+
}))
|
|
22864
|
+
},
|
|
22865
|
+
capturedAtMs: es.capturedAtMs
|
|
22866
|
+
};
|
|
22867
|
+
}
|
|
22868
|
+
}
|
|
22357
22869
|
this.applyActiveOverride();
|
|
22358
22870
|
}
|
|
22359
22871
|
applyActiveOverride() {
|
|
@@ -22407,6 +22919,11 @@ class AuthBroker {
|
|
|
22407
22919
|
persistLastQuotaCache() {
|
|
22408
22920
|
atomicWriteJsonSync(join8(this.stateDir, "last-quota.json"), this.lastQuotaCache, 384);
|
|
22409
22921
|
}
|
|
22922
|
+
persistExternalSpendCache() {
|
|
22923
|
+
if (!this.externalSpendCache)
|
|
22924
|
+
return;
|
|
22925
|
+
atomicWriteJsonSync(join8(this.stateDir, "external-spend.json"), this.externalSpendCache, 384);
|
|
22926
|
+
}
|
|
22410
22927
|
persistLastTierQuotaCache() {
|
|
22411
22928
|
atomicWriteJsonSync(join8(this.stateDir, "last-tier-quota.json"), this.lastTierQuotaCache, 384);
|
|
22412
22929
|
}
|