switchroom 0.19.0 → 0.19.2
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 +29 -1
- package/dist/auth-broker/index.js +552 -48
- package/dist/cli/autoaccept-poll.js +29 -1
- package/dist/cli/drive-write-pretool.mjs +30 -2
- package/dist/cli/ms-365-write-pretool.mjs +30 -2
- package/dist/cli/switchroom.js +751 -36
- 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/skills/switchroom-cli/SKILL.md +25 -0
- package/telegram-plugin/auth-snapshot-format.ts +39 -0
- package/telegram-plugin/dist/gateway/gateway.js +363 -25
- package/telegram-plugin/external-spend.ts +135 -0
- package/telegram-plugin/gateway/gateway.ts +83 -67
- package/telegram-plugin/gateway/model-command.ts +106 -0
- package/telegram-plugin/gateway/narrative-lane.ts +23 -9
- package/telegram-plugin/gateway/status-pin-store.ts +64 -4
- 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 +18 -0
- package/telegram-plugin/quota-check.ts +17 -2
- package/telegram-plugin/tests/activity-card-wiring.test.ts +47 -0
- package/telegram-plugin/tests/external-spend.test.ts +168 -0
- package/telegram-plugin/tests/gateway-session-model-relaunch.test.ts +57 -23
- 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/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
|
@@ -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(),
|
|
@@ -20249,6 +20457,18 @@ var SetOverrideDataSchema = exports_external.object({
|
|
|
20249
20457
|
var ClaimNotificationDataSchema = exports_external.object({
|
|
20250
20458
|
granted: exports_external.boolean()
|
|
20251
20459
|
});
|
|
20460
|
+
var GetExternalSpendDataSchema = exports_external.object({
|
|
20461
|
+
available: exports_external.boolean(),
|
|
20462
|
+
day24hUsd: exports_external.number().optional(),
|
|
20463
|
+
day7dUsd: exports_external.number().optional(),
|
|
20464
|
+
top: exports_external.array(exports_external.object({
|
|
20465
|
+
label: exports_external.string(),
|
|
20466
|
+
usd: exports_external.number()
|
|
20467
|
+
})).optional(),
|
|
20468
|
+
capturedAtMs: exports_external.number().int().nonnegative().optional(),
|
|
20469
|
+
served: exports_external.enum(["live", "cache"]).optional(),
|
|
20470
|
+
reason: exports_external.string().optional()
|
|
20471
|
+
});
|
|
20252
20472
|
var GoogleAccountStateSchema = exports_external.object({
|
|
20253
20473
|
account: exports_external.string(),
|
|
20254
20474
|
expiresAt: exports_external.number(),
|
|
@@ -20329,13 +20549,14 @@ function encodeError(id, code, message) {
|
|
|
20329
20549
|
}
|
|
20330
20550
|
|
|
20331
20551
|
// src/auth/broker/server.ts
|
|
20332
|
-
var
|
|
20552
|
+
var AUTH_BROKER_ROOT2 = "/run/switchroom/auth-broker";
|
|
20333
20553
|
var REFRESH_TICK_INTERVAL_MS = 60 * 1000;
|
|
20334
20554
|
var MARK_EXHAUSTED_DEFAULT_MS = 5 * 60 * 60 * 1000;
|
|
20335
20555
|
var MARK_THROTTLED_MAX_MS = 30 * 60 * 1000;
|
|
20336
20556
|
var THROTTLE_ESCALATION_HITS = 3;
|
|
20337
20557
|
var THROTTLE_ESCALATION_WINDOW_MS = 10 * 60 * 1000;
|
|
20338
20558
|
var MARK_THROTTLED_MIN_INTERVAL_MS = 5 * 1000;
|
|
20559
|
+
var MARK_CORROBORATION_PROBE_DELAY_MS = 90 * 1000;
|
|
20339
20560
|
var AUDIT_ROTATE_BYTES = 10 * 1024 * 1024;
|
|
20340
20561
|
var AUDIT_KEEP = 5;
|
|
20341
20562
|
var AUDIT_LINE_MAX = 4000;
|
|
@@ -20412,6 +20633,8 @@ class AuthBroker {
|
|
|
20412
20633
|
refreshTimer = null;
|
|
20413
20634
|
consumerProbeTimer = null;
|
|
20414
20635
|
fleetProbeTimer = null;
|
|
20636
|
+
markCorroborationTimers = new Set;
|
|
20637
|
+
markCorroborationDelayMs = MARK_CORROBORATION_PROBE_DELAY_MS;
|
|
20415
20638
|
fetchQuotaImpl;
|
|
20416
20639
|
stateDir;
|
|
20417
20640
|
socketRoot;
|
|
@@ -20422,6 +20645,8 @@ class AuthBroker {
|
|
|
20422
20645
|
providers;
|
|
20423
20646
|
quota = {};
|
|
20424
20647
|
lastQuotaCache = {};
|
|
20648
|
+
externalSpendCache = null;
|
|
20649
|
+
externalSpendInFlight = null;
|
|
20425
20650
|
probeInFlight = new Map;
|
|
20426
20651
|
shaIndex = {};
|
|
20427
20652
|
thresholdViolations = {};
|
|
@@ -20446,8 +20671,9 @@ class AuthBroker {
|
|
|
20446
20671
|
this.operatorUid = opts.operatorUid;
|
|
20447
20672
|
this.fetcher = opts.fetcher;
|
|
20448
20673
|
this.fetchQuotaImpl = opts._testFetchQuota ?? fetchQuota;
|
|
20674
|
+
this.markCorroborationDelayMs = opts._testCorroborationDelayMs ?? MARK_CORROBORATION_PROBE_DELAY_MS;
|
|
20449
20675
|
this.stateDir = opts.stateDir ?? resolve9(this.homeRoot(), ".switchroom", "state", "auth-broker");
|
|
20450
|
-
this.socketRoot = opts.socketRoot ??
|
|
20676
|
+
this.socketRoot = opts.socketRoot ?? AUTH_BROKER_ROOT2;
|
|
20451
20677
|
this.providers = new ProviderRegistry;
|
|
20452
20678
|
this.providers.register(new AnthropicProvider);
|
|
20453
20679
|
const googleClientId = config.google_workspace?.google_client_id;
|
|
@@ -20547,10 +20773,18 @@ class AuthBroker {
|
|
|
20547
20773
|
clearInterval(this.fleetProbeTimer);
|
|
20548
20774
|
this.fleetProbeTimer = null;
|
|
20549
20775
|
}
|
|
20776
|
+
for (const timer of this.markCorroborationTimers)
|
|
20777
|
+
clearTimeout(timer);
|
|
20778
|
+
this.markCorroborationTimers.clear();
|
|
20550
20779
|
for (const [sock, lis] of this.listeners) {
|
|
20551
20780
|
try {
|
|
20552
20781
|
lis.server.close();
|
|
20553
20782
|
} catch {}
|
|
20783
|
+
for (const s of lis.sockets) {
|
|
20784
|
+
try {
|
|
20785
|
+
s.destroy();
|
|
20786
|
+
} catch {}
|
|
20787
|
+
}
|
|
20554
20788
|
try {
|
|
20555
20789
|
if (existsSync8(sock))
|
|
20556
20790
|
unlinkSync(sock);
|
|
@@ -20579,7 +20813,12 @@ class AuthBroker {
|
|
|
20579
20813
|
if (this.operatorUid !== undefined) {
|
|
20580
20814
|
wanted.add(this.operatorSocketPath());
|
|
20581
20815
|
}
|
|
20816
|
+
const prevShape = configToShape(prev);
|
|
20817
|
+
const nextShape = configToShape(config);
|
|
20582
20818
|
for (const [sock, lis] of [...this.listeners]) {
|
|
20819
|
+
const before = classify(lis.socketPath, prevShape, this.socketRoot);
|
|
20820
|
+
const after = classify(lis.socketPath, nextShape, this.socketRoot);
|
|
20821
|
+
const identityChanged = JSON.stringify(before ?? null) !== JSON.stringify(after ?? null);
|
|
20583
20822
|
if (!wanted.has(sock)) {
|
|
20584
20823
|
try {
|
|
20585
20824
|
lis.server.close();
|
|
@@ -20588,7 +20827,18 @@ class AuthBroker {
|
|
|
20588
20827
|
if (existsSync8(sock))
|
|
20589
20828
|
unlinkSync(sock);
|
|
20590
20829
|
} catch {}
|
|
20830
|
+
for (const s of lis.sockets) {
|
|
20831
|
+
try {
|
|
20832
|
+
s.destroy();
|
|
20833
|
+
} catch {}
|
|
20834
|
+
}
|
|
20591
20835
|
this.listeners.delete(sock);
|
|
20836
|
+
} else if (identityChanged) {
|
|
20837
|
+
for (const s of lis.sockets) {
|
|
20838
|
+
try {
|
|
20839
|
+
s.destroy();
|
|
20840
|
+
} catch {}
|
|
20841
|
+
}
|
|
20592
20842
|
}
|
|
20593
20843
|
}
|
|
20594
20844
|
for (const name of Object.keys(config.agents ?? {})) {
|
|
@@ -20676,12 +20926,24 @@ class AuthBroker {
|
|
|
20676
20926
|
try {
|
|
20677
20927
|
chownSync2(dir, targetUid, targetUid);
|
|
20678
20928
|
} catch {}
|
|
20679
|
-
this.listeners.set(sockPath, {
|
|
20929
|
+
this.listeners.set(sockPath, {
|
|
20930
|
+
server,
|
|
20931
|
+
identity,
|
|
20932
|
+
socketPath: sockPath,
|
|
20933
|
+
sockets: new Set
|
|
20934
|
+
});
|
|
20680
20935
|
resolveP();
|
|
20681
20936
|
});
|
|
20682
20937
|
});
|
|
20683
20938
|
}
|
|
20684
20939
|
handleConnection(socket, sockPath, boundIdentity) {
|
|
20940
|
+
const listener = this.listeners.get(sockPath);
|
|
20941
|
+
if (listener) {
|
|
20942
|
+
listener.sockets.add(socket);
|
|
20943
|
+
socket.on("close", () => {
|
|
20944
|
+
listener.sockets.delete(socket);
|
|
20945
|
+
});
|
|
20946
|
+
}
|
|
20685
20947
|
let buf = "";
|
|
20686
20948
|
socket.on("data", (chunk) => {
|
|
20687
20949
|
buf += chunk.toString("utf-8");
|
|
@@ -20720,7 +20982,17 @@ class AuthBroker {
|
|
|
20720
20982
|
socket.write(encodeError(reqId, "INVALID_ARGS", err.message));
|
|
20721
20983
|
return;
|
|
20722
20984
|
}
|
|
20723
|
-
const identity = classify(sockPath, configToShape(this.config))
|
|
20985
|
+
const identity = classify(sockPath, configToShape(this.config), this.socketRoot);
|
|
20986
|
+
if (!identity) {
|
|
20987
|
+
this.audit({
|
|
20988
|
+
op: "stale-identity-rejected",
|
|
20989
|
+
identity: boundIdentity,
|
|
20990
|
+
ok: false,
|
|
20991
|
+
error: "FORBIDDEN"
|
|
20992
|
+
});
|
|
20993
|
+
socket.end(encodeError(reqId, "FORBIDDEN", "identity no longer configured"));
|
|
20994
|
+
return;
|
|
20995
|
+
}
|
|
20724
20996
|
try {
|
|
20725
20997
|
switch (req.op) {
|
|
20726
20998
|
case "get-credentials": {
|
|
@@ -20840,6 +21112,9 @@ class AuthBroker {
|
|
|
20840
21112
|
case "claim-notification":
|
|
20841
21113
|
this.opClaimNotification(socket, reqId, identity, req.key, req.windowMs);
|
|
20842
21114
|
break;
|
|
21115
|
+
case "get-external-spend":
|
|
21116
|
+
await this.opGetExternalSpend(socket, reqId, identity, req.forceLive);
|
|
21117
|
+
break;
|
|
20843
21118
|
}
|
|
20844
21119
|
} catch (err) {
|
|
20845
21120
|
socket.write(encodeError(reqId, "INTERNAL", err.message));
|
|
@@ -21612,6 +21887,9 @@ class AuthBroker {
|
|
|
21612
21887
|
});
|
|
21613
21888
|
this.quota[account] = { ...this.quota[account], exhausted_until: exhaustedUntil, marked_at: now };
|
|
21614
21889
|
this.persistQuota();
|
|
21890
|
+
if (exhaustedUntil > now + MARK_EXHAUSTED_DEFAULT_MS) {
|
|
21891
|
+
this.scheduleMarkCorroboration(account, now, identity);
|
|
21892
|
+
}
|
|
21615
21893
|
const rolledTo = await this.nextHealthyAccountLive(account, this.config.auth?.fallback_order ?? []);
|
|
21616
21894
|
const rolled = this.fanoutFailoverTo(account, rolledTo);
|
|
21617
21895
|
this.fanoutToAffectedConsumers(account);
|
|
@@ -21628,6 +21906,67 @@ class AuthBroker {
|
|
|
21628
21906
|
}
|
|
21629
21907
|
return { rolled, rolledTo };
|
|
21630
21908
|
}
|
|
21909
|
+
scheduleMarkCorroboration(account, markedAt, identity) {
|
|
21910
|
+
if (this.markCorroborationDelayMs <= 0)
|
|
21911
|
+
return;
|
|
21912
|
+
const timer = setTimeout(() => {
|
|
21913
|
+
this.markCorroborationTimers.delete(timer);
|
|
21914
|
+
this.corroborateExhaustionMark(account, markedAt, identity).catch((err) => {
|
|
21915
|
+
this.logErr(`mark-corroboration ${account}: ${err.message}`);
|
|
21916
|
+
});
|
|
21917
|
+
}, this.markCorroborationDelayMs);
|
|
21918
|
+
timer.unref?.();
|
|
21919
|
+
this.markCorroborationTimers.add(timer);
|
|
21920
|
+
}
|
|
21921
|
+
async corroborateExhaustionMark(account, markedAt, identity) {
|
|
21922
|
+
const entry = this.quota[account];
|
|
21923
|
+
if (!entry || entry.marked_at !== markedAt) {
|
|
21924
|
+
return { clamped: false, reason: "mark-superseded" };
|
|
21925
|
+
}
|
|
21926
|
+
const until = entry.exhausted_until;
|
|
21927
|
+
const now = this.now();
|
|
21928
|
+
if (until == null || until <= now + MARK_EXHAUSTED_DEFAULT_MS) {
|
|
21929
|
+
return { clamped: false, reason: "not-long" };
|
|
21930
|
+
}
|
|
21931
|
+
const creds = readAccountCredentials(account, this.home);
|
|
21932
|
+
const token = creds?.claudeAiOauth?.accessToken;
|
|
21933
|
+
if (!token)
|
|
21934
|
+
return { clamped: false, reason: "no-token" };
|
|
21935
|
+
let result;
|
|
21936
|
+
try {
|
|
21937
|
+
result = await this.fetchQuotaImpl({ accessToken: token });
|
|
21938
|
+
} catch (err) {
|
|
21939
|
+
this.logErr(`mark-corroboration probe ${account}: ${err.message}`);
|
|
21940
|
+
return { clamped: false, reason: "probe-failed" };
|
|
21941
|
+
}
|
|
21942
|
+
this.cacheQuotaSnapshot(account, result);
|
|
21943
|
+
const snapshot = this.lastQuotaCache[account];
|
|
21944
|
+
const clampedUntil = clampMarkExpiry({
|
|
21945
|
+
proposedUntil: until,
|
|
21946
|
+
now,
|
|
21947
|
+
shortMs: MARK_EXHAUSTED_DEFAULT_MS,
|
|
21948
|
+
snapshot
|
|
21949
|
+
});
|
|
21950
|
+
if (clampedUntil >= until) {
|
|
21951
|
+
return { clamped: false, reason: "not-contradicted" };
|
|
21952
|
+
}
|
|
21953
|
+
const live = this.quota[account];
|
|
21954
|
+
if (!live || live.marked_at !== markedAt || (live.exhausted_until ?? 0) <= clampedUntil) {
|
|
21955
|
+
return { clamped: false, reason: "mark-superseded" };
|
|
21956
|
+
}
|
|
21957
|
+
this.quota[account] = { ...live, exhausted_until: clampedUntil };
|
|
21958
|
+
this.persistQuota();
|
|
21959
|
+
this.audit({
|
|
21960
|
+
op: "mark-corroboration-clamp",
|
|
21961
|
+
identity,
|
|
21962
|
+
account,
|
|
21963
|
+
accountKind: "claude",
|
|
21964
|
+
ok: true
|
|
21965
|
+
});
|
|
21966
|
+
process.stdout.write(`auth-broker: mark-corroboration probe CONTRADICTS ${account} weekly wall — ` + `clamped exhausted_until ${new Date(until).toISOString()} → ` + `${new Date(clampedUntil).toISOString()}
|
|
21967
|
+
`);
|
|
21968
|
+
return { clamped: true, reason: "contradicted", until: clampedUntil };
|
|
21969
|
+
}
|
|
21631
21970
|
exhaustionLiveCorroborated(account) {
|
|
21632
21971
|
const snapshot = this.lastQuotaCache[account];
|
|
21633
21972
|
if (!snapshot || !snapshotFresh(snapshot, this.now(), MARK_EXHAUSTED_DEFAULT_MS))
|
|
@@ -21636,6 +21975,105 @@ class AuthBroker {
|
|
|
21636
21975
|
return false;
|
|
21637
21976
|
return !overageLiftsWall(snapshot, this.isOverageAllowed(account));
|
|
21638
21977
|
}
|
|
21978
|
+
async opGetExternalSpend(socket, id, _identity, forceLive) {
|
|
21979
|
+
const nowMs2 = this.now();
|
|
21980
|
+
const ttl = EXTERNAL_SPEND_CACHE_TTL_MS;
|
|
21981
|
+
const fresh = this.externalSpendCache != null && nowMs2 - this.externalSpendCache.capturedAtMs < ttl;
|
|
21982
|
+
if (forceLive || !fresh) {
|
|
21983
|
+
try {
|
|
21984
|
+
await this.refreshExternalSpend(forceLive === true);
|
|
21985
|
+
} catch (err) {
|
|
21986
|
+
this.logErr(`external-spend refresh failed: ${err?.message ?? err}`);
|
|
21987
|
+
}
|
|
21988
|
+
}
|
|
21989
|
+
if (!this.externalSpendCache) {
|
|
21990
|
+
socket.write(encodeSuccess(id, {
|
|
21991
|
+
available: false,
|
|
21992
|
+
reason: this.resolveLitellmMasterKey() ? "litellm_unreachable" : "master_key_unavailable"
|
|
21993
|
+
}));
|
|
21994
|
+
return;
|
|
21995
|
+
}
|
|
21996
|
+
const recently = nowMs2 - this.externalSpendCache.capturedAtMs < 5000;
|
|
21997
|
+
const outServed = recently && (forceLive || !fresh) ? "live" : "cache";
|
|
21998
|
+
const s = this.externalSpendCache.summary;
|
|
21999
|
+
socket.write(encodeSuccess(id, {
|
|
22000
|
+
available: true,
|
|
22001
|
+
day24hUsd: s.day24hUsd,
|
|
22002
|
+
day7dUsd: s.day7dUsd,
|
|
22003
|
+
top: s.top,
|
|
22004
|
+
capturedAtMs: this.externalSpendCache.capturedAtMs,
|
|
22005
|
+
served: outServed
|
|
22006
|
+
}));
|
|
22007
|
+
}
|
|
22008
|
+
resolveLitellmMasterKey() {
|
|
22009
|
+
if (this.opts._testLitellmMasterKey !== undefined) {
|
|
22010
|
+
const v = this.opts._testLitellmMasterKey;
|
|
22011
|
+
return v && v.trim() ? v.trim() : null;
|
|
22012
|
+
}
|
|
22013
|
+
const envKey = (process.env.SWITCHROOM_LITELLM_ADMIN_KEY ?? process.env.LITELLM_MASTER_KEY ?? "").trim();
|
|
22014
|
+
if (envKey)
|
|
22015
|
+
return envKey;
|
|
22016
|
+
try {
|
|
22017
|
+
const path = join8(this.stateDir, LITELLM_MASTER_KEY_STATE_BASENAME);
|
|
22018
|
+
if (!existsSync8(path))
|
|
22019
|
+
return null;
|
|
22020
|
+
const raw = readFileSync7(path, "utf-8").trim();
|
|
22021
|
+
return raw.length > 0 ? raw : null;
|
|
22022
|
+
} catch {
|
|
22023
|
+
return null;
|
|
22024
|
+
}
|
|
22025
|
+
}
|
|
22026
|
+
resolveLitellmBaseUrl() {
|
|
22027
|
+
const fromCfg = this.config.litellm?.base_url;
|
|
22028
|
+
const raw = (process.env.SWITCHROOM_LITELLM_BASE ?? fromCfg ?? DEFAULT_LITELLM_BASE).trim();
|
|
22029
|
+
return raw.replace(/\/+$/, "") || DEFAULT_LITELLM_BASE;
|
|
22030
|
+
}
|
|
22031
|
+
async refreshExternalSpend(forceLive) {
|
|
22032
|
+
while (this.externalSpendInFlight) {
|
|
22033
|
+
await this.externalSpendInFlight;
|
|
22034
|
+
if (!forceLive)
|
|
22035
|
+
return;
|
|
22036
|
+
const justNow = this.now();
|
|
22037
|
+
if (this.externalSpendCache && justNow - this.externalSpendCache.capturedAtMs < 5000) {
|
|
22038
|
+
return;
|
|
22039
|
+
}
|
|
22040
|
+
}
|
|
22041
|
+
const run = (async () => {
|
|
22042
|
+
const nowMs2 = this.now();
|
|
22043
|
+
if (!forceLive && this.externalSpendCache && nowMs2 - this.externalSpendCache.capturedAtMs < EXTERNAL_SPEND_CACHE_TTL_MS) {
|
|
22044
|
+
return;
|
|
22045
|
+
}
|
|
22046
|
+
const key = this.resolveLitellmMasterKey();
|
|
22047
|
+
if (!key)
|
|
22048
|
+
return;
|
|
22049
|
+
const baseUrl = this.resolveLitellmBaseUrl();
|
|
22050
|
+
const now = new Date(nowMs2);
|
|
22051
|
+
let summary = null;
|
|
22052
|
+
if (this.opts._testFetchExternalSpend) {
|
|
22053
|
+
summary = await this.opts._testFetchExternalSpend({
|
|
22054
|
+
adminKey: key,
|
|
22055
|
+
baseUrl,
|
|
22056
|
+
now,
|
|
22057
|
+
forceLive
|
|
22058
|
+
});
|
|
22059
|
+
} else {
|
|
22060
|
+
summary = await fetchAndSummarizeExternalSpend({
|
|
22061
|
+
adminKey: key,
|
|
22062
|
+
baseUrl,
|
|
22063
|
+
now,
|
|
22064
|
+
timeoutMs: EXTERNAL_SPEND_FETCH_TIMEOUT_MS
|
|
22065
|
+
});
|
|
22066
|
+
}
|
|
22067
|
+
if (!summary)
|
|
22068
|
+
return;
|
|
22069
|
+
this.externalSpendCache = { summary, capturedAtMs: this.now() };
|
|
22070
|
+
this.persistExternalSpendCache();
|
|
22071
|
+
})();
|
|
22072
|
+
this.externalSpendInFlight = run.finally(() => {
|
|
22073
|
+
this.externalSpendInFlight = null;
|
|
22074
|
+
});
|
|
22075
|
+
await this.externalSpendInFlight;
|
|
22076
|
+
}
|
|
21639
22077
|
opClaimNotification(socket, id, identity, key, windowMs) {
|
|
21640
22078
|
const now = this.now();
|
|
21641
22079
|
const prev = this.notificationClaims[key];
|
|
@@ -22201,25 +22639,68 @@ class AuthBroker {
|
|
|
22201
22639
|
const mirrorDir = consumer.mirror_dir;
|
|
22202
22640
|
if (!mirrorDir)
|
|
22203
22641
|
return false;
|
|
22204
|
-
const targetPath = join8(mirrorDir, ".credentials.json");
|
|
22205
22642
|
const credsPath = accountCredentialsPath(label, this.home);
|
|
22206
22643
|
if (!existsSync8(credsPath))
|
|
22207
22644
|
return false;
|
|
22208
22645
|
const mirrorContent = enrichMirrorContent(readFileSync7(credsPath, "utf-8"));
|
|
22209
|
-
|
|
22210
|
-
|
|
22211
|
-
|
|
22646
|
+
const safe = this.resolveConsumerMirrorPathsSafe(consumer.name, mirrorDir);
|
|
22647
|
+
if (!safe)
|
|
22648
|
+
return false;
|
|
22649
|
+
return safeMirrorWrite({
|
|
22650
|
+
parentDir: safe.mirrorDir,
|
|
22651
|
+
expectedRealParent: safe.expectedRealParent,
|
|
22652
|
+
targetPath: safe.targetPath,
|
|
22653
|
+
content: mirrorContent,
|
|
22654
|
+
uid: consumer.uid ?? 0,
|
|
22655
|
+
onChownError: (err) => this.warnCapChownMissing(err),
|
|
22656
|
+
refuse: (component, reason) => this.auditMirrorRefused({ kind: "consumer", name: consumer.name }, component, reason),
|
|
22657
|
+
logErr: (msg) => this.logErr(`consumer-mirror ${consumer.name} <- ${label}: ${msg}`)
|
|
22658
|
+
});
|
|
22659
|
+
}
|
|
22660
|
+
resolveConsumerMirrorPathsSafe(consumerName, mirrorDir) {
|
|
22661
|
+
const identity = { kind: "consumer", name: consumerName };
|
|
22662
|
+
const refuse = (component, reason) => {
|
|
22663
|
+
this.auditMirrorRefused(identity, component, reason);
|
|
22664
|
+
return null;
|
|
22665
|
+
};
|
|
22666
|
+
const parent = dirname3(mirrorDir);
|
|
22667
|
+
const mirrorStat = this.lstatOrNull(mirrorDir);
|
|
22668
|
+
if (mirrorStat) {
|
|
22669
|
+
if (mirrorStat.isSymbolicLink())
|
|
22670
|
+
return refuse("mirror_dir", "is-a-symlink");
|
|
22671
|
+
if (!mirrorStat.isDirectory())
|
|
22672
|
+
return refuse("mirror_dir", "is-not-a-directory");
|
|
22673
|
+
} else {
|
|
22674
|
+
const parentStat = this.lstatOrNull(parent);
|
|
22675
|
+
if (!parentStat)
|
|
22676
|
+
return refuse("mirror_dir-parent", "does-not-exist");
|
|
22677
|
+
if (parentStat.isSymbolicLink())
|
|
22678
|
+
return refuse("mirror_dir-parent", "is-a-symlink");
|
|
22679
|
+
if (!parentStat.isDirectory())
|
|
22680
|
+
return refuse("mirror_dir-parent", "is-not-a-directory");
|
|
22212
22681
|
try {
|
|
22213
|
-
|
|
22214
|
-
chownSync2(targetPath, uid, uid);
|
|
22682
|
+
mkdirSync5(mirrorDir, { recursive: false, mode: 448 });
|
|
22215
22683
|
} catch (err) {
|
|
22216
|
-
this.
|
|
22684
|
+
this.logErr(`consumer-mirror ${consumerName}: mkdir ${mirrorDir}: ${err.message}`);
|
|
22685
|
+
return null;
|
|
22217
22686
|
}
|
|
22218
|
-
|
|
22687
|
+
}
|
|
22688
|
+
const targetPath = join8(mirrorDir, ".credentials.json");
|
|
22689
|
+
const targetStat = this.lstatOrNull(targetPath);
|
|
22690
|
+
if (targetStat) {
|
|
22691
|
+
if (targetStat.isSymbolicLink())
|
|
22692
|
+
return refuse(".credentials.json", "is-a-symlink");
|
|
22693
|
+
if (!targetStat.isFile())
|
|
22694
|
+
return refuse(".credentials.json", "is-not-a-regular-file");
|
|
22695
|
+
}
|
|
22696
|
+
let expectedRealParent;
|
|
22697
|
+
try {
|
|
22698
|
+
expectedRealParent = join8(realpathSync3(parent), basename2(mirrorDir));
|
|
22219
22699
|
} catch (err) {
|
|
22220
|
-
this.logErr(`consumer-mirror ${
|
|
22221
|
-
return
|
|
22700
|
+
this.logErr(`consumer-mirror ${consumerName}: realpath ${parent}: ${err.message}`);
|
|
22701
|
+
return null;
|
|
22222
22702
|
}
|
|
22703
|
+
return { mirrorDir, targetPath, expectedRealParent };
|
|
22223
22704
|
}
|
|
22224
22705
|
fanoutFailoverTo(label, next) {
|
|
22225
22706
|
const auth = this.config.auth ?? {};
|
|
@@ -22274,13 +22755,7 @@ class AuthBroker {
|
|
|
22274
22755
|
const claudeDir = join8(agentDir, ".claude");
|
|
22275
22756
|
const targetPath = join8(claudeDir, ".credentials.json");
|
|
22276
22757
|
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
|
-
});
|
|
22758
|
+
this.auditMirrorRefused({ kind: "agent", name: agentName, admin: false }, component, reason);
|
|
22284
22759
|
return null;
|
|
22285
22760
|
};
|
|
22286
22761
|
const agentsStat = this.lstatOrNull(agentsDir);
|
|
@@ -22311,15 +22786,26 @@ class AuthBroker {
|
|
|
22311
22786
|
if (!targetStat.isFile())
|
|
22312
22787
|
return refuse(".credentials.json", "is-not-a-regular-file");
|
|
22313
22788
|
}
|
|
22314
|
-
|
|
22789
|
+
const expectedRealParent = join8(realpathSync3(agentsDir), agentName, ".claude");
|
|
22790
|
+
return { agentDir, claudeDir, targetPath, expectedRealParent };
|
|
22315
22791
|
}
|
|
22316
22792
|
lstatOrNull(path) {
|
|
22317
22793
|
try {
|
|
22318
|
-
return
|
|
22794
|
+
return lstatSync2(path);
|
|
22319
22795
|
} catch {
|
|
22320
22796
|
return null;
|
|
22321
22797
|
}
|
|
22322
22798
|
}
|
|
22799
|
+
auditMirrorRefused(identity, component, reason) {
|
|
22800
|
+
const who = identity.kind === "operator" ? "operator" : identity.name;
|
|
22801
|
+
this.logErr(`mirror ${who}: REFUSING mirror — ${component} ${reason} ` + `(symlink-guard #1393; peer-owned tree may be attacker-poisoned)`);
|
|
22802
|
+
this.audit({
|
|
22803
|
+
op: "mirror-symlink-refused",
|
|
22804
|
+
identity,
|
|
22805
|
+
ok: false,
|
|
22806
|
+
error: `${component}:${reason}`
|
|
22807
|
+
});
|
|
22808
|
+
}
|
|
22323
22809
|
mirrorAccountToAgent(label, agentName) {
|
|
22324
22810
|
const credsPath = accountCredentialsPath(label, this.home);
|
|
22325
22811
|
if (!existsSync8(credsPath))
|
|
@@ -22328,21 +22814,18 @@ class AuthBroker {
|
|
|
22328
22814
|
const safe = this.resolveMirrorPathsSafe(agentName);
|
|
22329
22815
|
if (!safe)
|
|
22330
22816
|
return false;
|
|
22331
|
-
const { claudeDir, targetPath } = safe;
|
|
22817
|
+
const { claudeDir, targetPath, expectedRealParent } = safe;
|
|
22332
22818
|
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
|
-
}
|
|
22819
|
+
return safeMirrorWrite({
|
|
22820
|
+
parentDir: claudeDir,
|
|
22821
|
+
expectedRealParent,
|
|
22822
|
+
targetPath,
|
|
22823
|
+
content: mirrorContent,
|
|
22824
|
+
uid: allocateAgentUid(agentName),
|
|
22825
|
+
onChownError: (err) => this.warnCapChownMissing(err),
|
|
22826
|
+
refuse: (component, reason) => this.auditMirrorRefused({ kind: "agent", name: agentName, admin: false }, component, reason),
|
|
22827
|
+
logErr: (msg) => this.logErr(`fanout ${agentName} <- ${label}: ${msg}`)
|
|
22828
|
+
});
|
|
22346
22829
|
}
|
|
22347
22830
|
loadStateFromDisk() {
|
|
22348
22831
|
this.quota = this.readJson("quota.json") ?? {};
|
|
@@ -22354,6 +22837,22 @@ class AuthBroker {
|
|
|
22354
22837
|
this.lastTierQuotaCache = this.readJson("last-tier-quota.json") ?? {};
|
|
22355
22838
|
this.premiumTierAllWalled = this.readJson("premium-tier-all-walled.json") ?? null;
|
|
22356
22839
|
this.usageLedger = this.readJson("usage-ledger.json") ?? {};
|
|
22840
|
+
{
|
|
22841
|
+
const es = this.readJson("external-spend.json");
|
|
22842
|
+
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))) {
|
|
22843
|
+
this.externalSpendCache = {
|
|
22844
|
+
summary: {
|
|
22845
|
+
day24hUsd: es.summary.day24hUsd,
|
|
22846
|
+
day7dUsd: es.summary.day7dUsd,
|
|
22847
|
+
top: es.summary.top.map((t) => ({
|
|
22848
|
+
label: t.label,
|
|
22849
|
+
usd: t.usd
|
|
22850
|
+
}))
|
|
22851
|
+
},
|
|
22852
|
+
capturedAtMs: es.capturedAtMs
|
|
22853
|
+
};
|
|
22854
|
+
}
|
|
22855
|
+
}
|
|
22357
22856
|
this.applyActiveOverride();
|
|
22358
22857
|
}
|
|
22359
22858
|
applyActiveOverride() {
|
|
@@ -22407,6 +22906,11 @@ class AuthBroker {
|
|
|
22407
22906
|
persistLastQuotaCache() {
|
|
22408
22907
|
atomicWriteJsonSync(join8(this.stateDir, "last-quota.json"), this.lastQuotaCache, 384);
|
|
22409
22908
|
}
|
|
22909
|
+
persistExternalSpendCache() {
|
|
22910
|
+
if (!this.externalSpendCache)
|
|
22911
|
+
return;
|
|
22912
|
+
atomicWriteJsonSync(join8(this.stateDir, "external-spend.json"), this.externalSpendCache, 384);
|
|
22913
|
+
}
|
|
22410
22914
|
persistLastTierQuotaCache() {
|
|
22411
22915
|
atomicWriteJsonSync(join8(this.stateDir, "last-tier-quota.json"), this.lastTierQuotaCache, 384);
|
|
22412
22916
|
}
|