switchroom 0.11.0 → 0.11.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/agent-scheduler/index.js +2 -2
- package/dist/auth-broker/index.js +125 -3
- package/dist/cli/drive-write-pretool.mjs +20 -2
- package/dist/cli/switchroom.js +32 -7
- package/dist/host-control/main.js +2 -2
- package/dist/vault/approvals/kernel-server.js +2 -2
- package/dist/vault/broker/server.js +2 -2
- package/package.json +1 -1
- package/telegram-plugin/auto-fallback-fleet.ts +4 -4
- package/telegram-plugin/dist/gateway/gateway.js +242 -226
- package/telegram-plugin/gateway/auth-broker-client.ts +2 -0
- package/telegram-plugin/gateway/auth-command.ts +10 -0
- package/telegram-plugin/gateway/gateway.ts +51 -24
- package/telegram-plugin/gateway/hostd-dispatch.ts +10 -2
|
@@ -11286,7 +11286,7 @@ var QuotaConfigSchema = exports_external.object({
|
|
|
11286
11286
|
monthly_budget_usd: exports_external.number().positive().optional().describe("Monthly USD spend budget. If unset, the greeting shows raw usage only.")
|
|
11287
11287
|
});
|
|
11288
11288
|
var HostControlConfigSchema = exports_external.object({
|
|
11289
|
-
enabled: exports_external.boolean().
|
|
11289
|
+
enabled: exports_external.boolean().default(true).describe("Whether the host-control daemon is in use. Default: true (since " + "RFC C Phase 2 default-flip — the gateway's /restart, /new, /reset, " + "and /update apply slash-commands all dispatch through hostd, and " + "without it those verbs fail on docker-mode installs because the " + "agent container has no docker binary/socket). " + "When true, the compose generator emits per-agent bind mounts " + "at `~/.switchroom/hostd/<name>/sock` for every admin-flagged " + "agent. Install the daemon with `switchroom hostd install` — " + "it runs as a docker container in its own compose project " + "(`switchroom-hostd`), separate from the agent fleet's compose " + "project so `up -d --remove-orphans` cycles of the fleet " + "can't recreate the daemon mid-RPC. See RFC C §5.1. " + "Set enabled: false only on legacy systemd-mode installs that " + "still rely on the in-container `spawnSwitchroomDetached` " + "shellout (removal is tracked as RFC C Phase 3).")
|
|
11290
11290
|
});
|
|
11291
11291
|
var SwitchroomConfigSchema = exports_external.object({
|
|
11292
11292
|
switchroom: exports_external.object({
|
|
@@ -11312,7 +11312,7 @@ var SwitchroomConfigSchema = exports_external.object({
|
|
|
11312
11312
|
drive: GoogleWorkspaceConfigSchema.describe("RFC D legacy key — use `google_workspace:` instead. Optional Google " + "Workspace onboarding configuration. When set, supplies Google OAuth " + "client credentials, the approver allowlist for `switchroom drive " + "connect`, and the optional tier knob. Env vars " + "(SWITCHROOM_GOOGLE_CLIENT_ID, SWITCHROOM_GOOGLE_CLIENT_SECRET, " + "SWITCHROOM_APPROVER_USER_ID) take precedence over this block when " + "set, preserving back-compat with the env-only flow shipped in #766."),
|
|
11313
11313
|
google_workspace: GoogleWorkspaceConfigSchema.describe("RFC G canonical key. Top-level Google Workspace configuration — " + "OAuth client credentials, approver allowlist, and tier knob (`core` " + "| `extended` | `complete`, default `core`). Mutually exclusive with " + "`drive:` at the top level (loader fails fast if both are set)."),
|
|
11314
11314
|
quota: QuotaConfigSchema.optional().describe("Optional weekly/monthly USD spend budgets rendered in the session " + "greeting. Usage is read from ccusage at runtime; no network calls."),
|
|
11315
|
-
host_control: HostControlConfigSchema.
|
|
11315
|
+
host_control: HostControlConfigSchema.default({}).describe("Host-control daemon configuration. Defaults to enabled=true since " + "RFC C Phase 2 (docs/rfcs/host-control-daemon.md). Omit the block " + "to accept defaults; set `enabled: false` only on legacy systemd-" + "mode installs (removal tracked as RFC C Phase 3)."),
|
|
11316
11316
|
google_accounts: exports_external.record(exports_external.string().regex(/^[^@\s:]+@[^@\s:]+\.[^@\s:]+$/, {
|
|
11317
11317
|
message: "Account key must be a Google account email like 'alice@example.com' (colons not allowed)"
|
|
11318
11318
|
}).transform((v) => v.trim().toLowerCase()), exports_external.object({
|
|
@@ -11286,7 +11286,7 @@ var QuotaConfigSchema = exports_external.object({
|
|
|
11286
11286
|
monthly_budget_usd: exports_external.number().positive().optional().describe("Monthly USD spend budget. If unset, the greeting shows raw usage only.")
|
|
11287
11287
|
});
|
|
11288
11288
|
var HostControlConfigSchema = exports_external.object({
|
|
11289
|
-
enabled: exports_external.boolean().
|
|
11289
|
+
enabled: exports_external.boolean().default(true).describe("Whether the host-control daemon is in use. Default: true (since " + "RFC C Phase 2 default-flip — the gateway's /restart, /new, /reset, " + "and /update apply slash-commands all dispatch through hostd, and " + "without it those verbs fail on docker-mode installs because the " + "agent container has no docker binary/socket). " + "When true, the compose generator emits per-agent bind mounts " + "at `~/.switchroom/hostd/<name>/sock` for every admin-flagged " + "agent. Install the daemon with `switchroom hostd install` — " + "it runs as a docker container in its own compose project " + "(`switchroom-hostd`), separate from the agent fleet's compose " + "project so `up -d --remove-orphans` cycles of the fleet " + "can't recreate the daemon mid-RPC. See RFC C §5.1. " + "Set enabled: false only on legacy systemd-mode installs that " + "still rely on the in-container `spawnSwitchroomDetached` " + "shellout (removal is tracked as RFC C Phase 3).")
|
|
11290
11290
|
});
|
|
11291
11291
|
var SwitchroomConfigSchema = exports_external.object({
|
|
11292
11292
|
switchroom: exports_external.object({
|
|
@@ -11312,7 +11312,7 @@ var SwitchroomConfigSchema = exports_external.object({
|
|
|
11312
11312
|
drive: GoogleWorkspaceConfigSchema.describe("RFC D legacy key — use `google_workspace:` instead. Optional Google " + "Workspace onboarding configuration. When set, supplies Google OAuth " + "client credentials, the approver allowlist for `switchroom drive " + "connect`, and the optional tier knob. Env vars " + "(SWITCHROOM_GOOGLE_CLIENT_ID, SWITCHROOM_GOOGLE_CLIENT_SECRET, " + "SWITCHROOM_APPROVER_USER_ID) take precedence over this block when " + "set, preserving back-compat with the env-only flow shipped in #766."),
|
|
11313
11313
|
google_workspace: GoogleWorkspaceConfigSchema.describe("RFC G canonical key. Top-level Google Workspace configuration — " + "OAuth client credentials, approver allowlist, and tier knob (`core` " + "| `extended` | `complete`, default `core`). Mutually exclusive with " + "`drive:` at the top level (loader fails fast if both are set)."),
|
|
11314
11314
|
quota: QuotaConfigSchema.optional().describe("Optional weekly/monthly USD spend budgets rendered in the session " + "greeting. Usage is read from ccusage at runtime; no network calls."),
|
|
11315
|
-
host_control: HostControlConfigSchema.
|
|
11315
|
+
host_control: HostControlConfigSchema.default({}).describe("Host-control daemon configuration. Defaults to enabled=true since " + "RFC C Phase 2 (docs/rfcs/host-control-daemon.md). Omit the block " + "to accept defaults; set `enabled: false` only on legacy systemd-" + "mode installs (removal tracked as RFC C Phase 3)."),
|
|
11316
11316
|
google_accounts: exports_external.record(exports_external.string().regex(/^[^@\s:]+@[^@\s:]+\.[^@\s:]+$/, {
|
|
11317
11317
|
message: "Account key must be a Google account email like 'alice@example.com' (colons not allowed)"
|
|
11318
11318
|
}).transform((v) => v.trim().toLowerCase()), exports_external.object({
|
|
@@ -11947,6 +11947,93 @@ function allocateAgentUid(name) {
|
|
|
11947
11947
|
}
|
|
11948
11948
|
var BIND_MOUNT_EXACT_SOURCE_DENY = new Set(["/var/run/docker.sock"]);
|
|
11949
11949
|
|
|
11950
|
+
// src/auth/quota.ts
|
|
11951
|
+
var OAUTH_BETA = "oauth-2025-04-20";
|
|
11952
|
+
var DEFAULT_USER_AGENT = "claude-cli/1.0.0 (external, cli)";
|
|
11953
|
+
var DEFAULT_PROBE_MODEL = "claude-haiku-4-5-20251001";
|
|
11954
|
+
function parseFloatHeader(headers, name) {
|
|
11955
|
+
const v = headers.get(name);
|
|
11956
|
+
if (v == null || v.trim().length === 0)
|
|
11957
|
+
return null;
|
|
11958
|
+
const n = Number(v);
|
|
11959
|
+
return Number.isFinite(n) ? n : null;
|
|
11960
|
+
}
|
|
11961
|
+
function parseEpochHeader(headers, name) {
|
|
11962
|
+
const v = headers.get(name);
|
|
11963
|
+
if (v == null)
|
|
11964
|
+
return null;
|
|
11965
|
+
const n = Number(v);
|
|
11966
|
+
if (!Number.isFinite(n) || n <= 0)
|
|
11967
|
+
return null;
|
|
11968
|
+
return new Date(n * 1000);
|
|
11969
|
+
}
|
|
11970
|
+
function parseQuotaHeaders(headers) {
|
|
11971
|
+
const fiveHour = parseFloatHeader(headers, "anthropic-ratelimit-unified-5h-utilization");
|
|
11972
|
+
const sevenDay = parseFloatHeader(headers, "anthropic-ratelimit-unified-7d-utilization");
|
|
11973
|
+
if (fiveHour == null && sevenDay == null) {
|
|
11974
|
+
return {
|
|
11975
|
+
ok: false,
|
|
11976
|
+
reason: "no unified rate-limit headers in response (API token, not OAuth?)"
|
|
11977
|
+
};
|
|
11978
|
+
}
|
|
11979
|
+
return {
|
|
11980
|
+
ok: true,
|
|
11981
|
+
data: {
|
|
11982
|
+
fiveHourUtilizationPct: (fiveHour ?? 0) * 100,
|
|
11983
|
+
sevenDayUtilizationPct: (sevenDay ?? 0) * 100,
|
|
11984
|
+
fiveHourResetAt: parseEpochHeader(headers, "anthropic-ratelimit-unified-5h-reset"),
|
|
11985
|
+
sevenDayResetAt: parseEpochHeader(headers, "anthropic-ratelimit-unified-7d-reset"),
|
|
11986
|
+
representativeClaim: headers.get("anthropic-ratelimit-unified-representative-claim"),
|
|
11987
|
+
overageStatus: headers.get("anthropic-ratelimit-unified-overage-status"),
|
|
11988
|
+
overageDisabledReason: headers.get("anthropic-ratelimit-unified-overage-disabled-reason")
|
|
11989
|
+
}
|
|
11990
|
+
};
|
|
11991
|
+
}
|
|
11992
|
+
async function fetchQuota(opts) {
|
|
11993
|
+
const token = opts.accessToken?.trim();
|
|
11994
|
+
if (!token || token.length === 0) {
|
|
11995
|
+
return { ok: false, reason: "fetchQuota requires a non-empty accessToken" };
|
|
11996
|
+
}
|
|
11997
|
+
const controller = new AbortController;
|
|
11998
|
+
const timeout = setTimeout(() => controller.abort(), opts.timeoutMs ?? 1e4);
|
|
11999
|
+
const fetchFn = opts.fetchImpl ?? fetch;
|
|
12000
|
+
let resp;
|
|
12001
|
+
try {
|
|
12002
|
+
resp = await fetchFn("https://api.anthropic.com/v1/messages", {
|
|
12003
|
+
method: "POST",
|
|
12004
|
+
headers: {
|
|
12005
|
+
"anthropic-version": "2023-06-01",
|
|
12006
|
+
"anthropic-beta": OAUTH_BETA,
|
|
12007
|
+
authorization: `Bearer ${token}`,
|
|
12008
|
+
"x-app": "cli",
|
|
12009
|
+
"user-agent": DEFAULT_USER_AGENT,
|
|
12010
|
+
"content-type": "application/json"
|
|
12011
|
+
},
|
|
12012
|
+
body: JSON.stringify({
|
|
12013
|
+
model: opts.model ?? DEFAULT_PROBE_MODEL,
|
|
12014
|
+
max_tokens: 1,
|
|
12015
|
+
messages: [{ role: "user", content: "hi" }]
|
|
12016
|
+
}),
|
|
12017
|
+
signal: controller.signal
|
|
12018
|
+
});
|
|
12019
|
+
} catch (err) {
|
|
12020
|
+
const msg = err?.message ?? String(err);
|
|
12021
|
+
clearTimeout(timeout);
|
|
12022
|
+
if (msg.includes("aborted")) {
|
|
12023
|
+
return { ok: false, reason: `quota probe timed out after ${opts.timeoutMs ?? 1e4}ms` };
|
|
12024
|
+
}
|
|
12025
|
+
return { ok: false, reason: `quota probe network error: ${msg}` };
|
|
12026
|
+
}
|
|
12027
|
+
clearTimeout(timeout);
|
|
12028
|
+
const parsed = parseQuotaHeaders(resp.headers);
|
|
12029
|
+
if (parsed.ok)
|
|
12030
|
+
return parsed;
|
|
12031
|
+
if (!resp.ok) {
|
|
12032
|
+
return { ok: false, reason: `HTTP ${resp.status} from Anthropic (${parsed.reason})` };
|
|
12033
|
+
}
|
|
12034
|
+
return parsed;
|
|
12035
|
+
}
|
|
12036
|
+
|
|
11950
12037
|
// src/util/atomic.ts
|
|
11951
12038
|
import { randomBytes } from "node:crypto";
|
|
11952
12039
|
import { closeSync, fsyncSync, openSync, renameSync, rmSync, writeSync } from "node:fs";
|
|
@@ -12634,6 +12721,13 @@ var ListGoogleAccountsRequestSchema = exports_external.object({
|
|
|
12634
12721
|
op: exports_external.literal("list-google-accounts"),
|
|
12635
12722
|
id: exports_external.string().min(1)
|
|
12636
12723
|
});
|
|
12724
|
+
var ProbeQuotaRequestSchema = exports_external.object({
|
|
12725
|
+
v: exports_external.literal(PROTOCOL_VERSION),
|
|
12726
|
+
op: exports_external.literal("probe-quota"),
|
|
12727
|
+
id: exports_external.string().min(1),
|
|
12728
|
+
accounts: exports_external.array(exports_external.string().min(1)).min(1).max(32),
|
|
12729
|
+
timeoutMs: exports_external.number().int().positive().max(60000).optional()
|
|
12730
|
+
});
|
|
12637
12731
|
var RequestSchema = exports_external.discriminatedUnion("op", [
|
|
12638
12732
|
GetCredentialsRequestSchema,
|
|
12639
12733
|
ListStateRequestSchema,
|
|
@@ -12643,7 +12737,8 @@ var RequestSchema = exports_external.discriminatedUnion("op", [
|
|
|
12643
12737
|
AddAccountRequestSchema,
|
|
12644
12738
|
RmAccountRequestSchema,
|
|
12645
12739
|
SetOverrideRequestSchema,
|
|
12646
|
-
ListGoogleAccountsRequestSchema
|
|
12740
|
+
ListGoogleAccountsRequestSchema,
|
|
12741
|
+
ProbeQuotaRequestSchema
|
|
12647
12742
|
]);
|
|
12648
12743
|
var GetCredentialsDataSchema = exports_external.object({
|
|
12649
12744
|
account: exports_external.string(),
|
|
@@ -13167,6 +13262,9 @@ class AuthBroker {
|
|
|
13167
13262
|
case "list-google-accounts":
|
|
13168
13263
|
await this.opListGoogleAccounts(socket, reqId, identity2);
|
|
13169
13264
|
break;
|
|
13265
|
+
case "probe-quota":
|
|
13266
|
+
await this.opProbeQuota(socket, reqId, identity2, req.accounts, req.timeoutMs);
|
|
13267
|
+
break;
|
|
13170
13268
|
}
|
|
13171
13269
|
} catch (err) {
|
|
13172
13270
|
socket.write(encodeError(reqId, "INTERNAL", err.message));
|
|
@@ -13265,6 +13363,30 @@ class AuthBroker {
|
|
|
13265
13363
|
this.audit({ op: "list-google-accounts", identity: identity2, ok: true });
|
|
13266
13364
|
socket.write(encodeSuccess(id, { accounts }));
|
|
13267
13365
|
}
|
|
13366
|
+
async opProbeQuota(socket, id, identity2, accounts, timeoutMs) {
|
|
13367
|
+
const results = await Promise.all(accounts.map(async (label) => {
|
|
13368
|
+
const creds = readAccountCredentials(label, this.home);
|
|
13369
|
+
const token = creds?.claudeAiOauth?.accessToken;
|
|
13370
|
+
if (!token) {
|
|
13371
|
+
const result2 = {
|
|
13372
|
+
ok: false,
|
|
13373
|
+
reason: "no credentials for account in broker store"
|
|
13374
|
+
};
|
|
13375
|
+
this.audit({ op: "probe-quota", identity: identity2, account: label, ok: false, error: "missing-credentials" });
|
|
13376
|
+
return { label, result: result2 };
|
|
13377
|
+
}
|
|
13378
|
+
const result = await fetchQuota({ accessToken: token, timeoutMs });
|
|
13379
|
+
this.audit({
|
|
13380
|
+
op: "probe-quota",
|
|
13381
|
+
identity: identity2,
|
|
13382
|
+
account: label,
|
|
13383
|
+
ok: result.ok,
|
|
13384
|
+
error: result.ok ? undefined : result.reason
|
|
13385
|
+
});
|
|
13386
|
+
return { label, result };
|
|
13387
|
+
}));
|
|
13388
|
+
socket.write(encodeSuccess(id, { results }));
|
|
13389
|
+
}
|
|
13268
13390
|
async opSetActive(socket, id, identity2, account) {
|
|
13269
13391
|
if (!this.isAdmin(identity2)) {
|
|
13270
13392
|
this.audit({ op: "set-active", identity: identity2, account, ok: false, error: "FORBIDDEN" });
|
|
@@ -4000,7 +4000,7 @@ function decodeResponse(line) {
|
|
|
4000
4000
|
}
|
|
4001
4001
|
return ResponseSchema.parse(parsed);
|
|
4002
4002
|
}
|
|
4003
|
-
var MAX_FRAME_BYTES, PROTOCOL_VERSION = 1, ProviderNameSchema, GetCredentialsRequestSchema, ListStateRequestSchema, SetActiveRequestSchema, MarkExhaustedRequestSchema, RefreshAccountRequestSchema, AnthropicCredentialsSchema, GoogleCredentialsSchema, ProviderCredentialsSchema, AddAccountRequestSchema, RmAccountRequestSchema, SetOverrideRequestSchema, ListGoogleAccountsRequestSchema, RequestSchema, GetCredentialsDataSchema, AccountStateSchema, AgentStateSchema, ConsumerStateSchema, ListStateDataSchema, SetActiveDataSchema, MarkExhaustedDataSchema, RefreshAccountDataSchema, AddAccountDataSchema, RmAccountDataSchema, SetOverrideDataSchema, GoogleAccountStateSchema, ListGoogleAccountsDataSchema, ErrorBodySchema, SuccessResponseSchema, ErrorResponseSchema, ResponseSchema;
|
|
4003
|
+
var MAX_FRAME_BYTES, PROTOCOL_VERSION = 1, ProviderNameSchema, GetCredentialsRequestSchema, ListStateRequestSchema, SetActiveRequestSchema, MarkExhaustedRequestSchema, RefreshAccountRequestSchema, AnthropicCredentialsSchema, GoogleCredentialsSchema, ProviderCredentialsSchema, AddAccountRequestSchema, RmAccountRequestSchema, SetOverrideRequestSchema, ListGoogleAccountsRequestSchema, ProbeQuotaRequestSchema, RequestSchema, GetCredentialsDataSchema, AccountStateSchema, AgentStateSchema, ConsumerStateSchema, ListStateDataSchema, SetActiveDataSchema, MarkExhaustedDataSchema, RefreshAccountDataSchema, AddAccountDataSchema, RmAccountDataSchema, SetOverrideDataSchema, GoogleAccountStateSchema, ListGoogleAccountsDataSchema, ErrorBodySchema, SuccessResponseSchema, ErrorResponseSchema, ResponseSchema;
|
|
4004
4004
|
var init_protocol = __esm(() => {
|
|
4005
4005
|
init_zod();
|
|
4006
4006
|
MAX_FRAME_BYTES = 64 * 1024;
|
|
@@ -4089,6 +4089,13 @@ var init_protocol = __esm(() => {
|
|
|
4089
4089
|
op: exports_external.literal("list-google-accounts"),
|
|
4090
4090
|
id: exports_external.string().min(1)
|
|
4091
4091
|
});
|
|
4092
|
+
ProbeQuotaRequestSchema = exports_external.object({
|
|
4093
|
+
v: exports_external.literal(PROTOCOL_VERSION),
|
|
4094
|
+
op: exports_external.literal("probe-quota"),
|
|
4095
|
+
id: exports_external.string().min(1),
|
|
4096
|
+
accounts: exports_external.array(exports_external.string().min(1)).min(1).max(32),
|
|
4097
|
+
timeoutMs: exports_external.number().int().positive().max(60000).optional()
|
|
4098
|
+
});
|
|
4092
4099
|
RequestSchema = exports_external.discriminatedUnion("op", [
|
|
4093
4100
|
GetCredentialsRequestSchema,
|
|
4094
4101
|
ListStateRequestSchema,
|
|
@@ -4098,7 +4105,8 @@ var init_protocol = __esm(() => {
|
|
|
4098
4105
|
AddAccountRequestSchema,
|
|
4099
4106
|
RmAccountRequestSchema,
|
|
4100
4107
|
SetOverrideRequestSchema,
|
|
4101
|
-
ListGoogleAccountsRequestSchema
|
|
4108
|
+
ListGoogleAccountsRequestSchema,
|
|
4109
|
+
ProbeQuotaRequestSchema
|
|
4102
4110
|
]);
|
|
4103
4111
|
GetCredentialsDataSchema = exports_external.object({
|
|
4104
4112
|
account: exports_external.string(),
|
|
@@ -4277,6 +4285,16 @@ class AuthBrokerClient {
|
|
|
4277
4285
|
});
|
|
4278
4286
|
return data;
|
|
4279
4287
|
}
|
|
4288
|
+
async probeQuota(accounts, timeoutMs) {
|
|
4289
|
+
const data = await this.send({
|
|
4290
|
+
v: PROTOCOL_VERSION,
|
|
4291
|
+
id: randomUUID(),
|
|
4292
|
+
op: "probe-quota",
|
|
4293
|
+
accounts: [...accounts],
|
|
4294
|
+
...timeoutMs !== undefined ? { timeoutMs } : {}
|
|
4295
|
+
});
|
|
4296
|
+
return data;
|
|
4297
|
+
}
|
|
4280
4298
|
async setActive(account) {
|
|
4281
4299
|
const data = await this.send({
|
|
4282
4300
|
v: PROTOCOL_VERSION,
|
package/dist/cli/switchroom.js
CHANGED
|
@@ -13850,7 +13850,7 @@ var init_schema = __esm(() => {
|
|
|
13850
13850
|
monthly_budget_usd: exports_external.number().positive().optional().describe("Monthly USD spend budget. If unset, the greeting shows raw usage only.")
|
|
13851
13851
|
});
|
|
13852
13852
|
HostControlConfigSchema = exports_external.object({
|
|
13853
|
-
enabled: exports_external.boolean().
|
|
13853
|
+
enabled: exports_external.boolean().default(true).describe("Whether the host-control daemon is in use. Default: true (since " + "RFC C Phase 2 default-flip \u2014 the gateway's /restart, /new, /reset, " + "and /update apply slash-commands all dispatch through hostd, and " + "without it those verbs fail on docker-mode installs because the " + "agent container has no docker binary/socket). " + "When true, the compose generator emits per-agent bind mounts " + "at `~/.switchroom/hostd/<name>/sock` for every admin-flagged " + "agent. Install the daemon with `switchroom hostd install` \u2014 " + "it runs as a docker container in its own compose project " + "(`switchroom-hostd`), separate from the agent fleet's compose " + "project so `up -d --remove-orphans` cycles of the fleet " + "can't recreate the daemon mid-RPC. See RFC C \u00a75.1. " + "Set enabled: false only on legacy systemd-mode installs that " + "still rely on the in-container `spawnSwitchroomDetached` " + "shellout (removal is tracked as RFC C Phase 3).")
|
|
13854
13854
|
});
|
|
13855
13855
|
SwitchroomConfigSchema = exports_external.object({
|
|
13856
13856
|
switchroom: exports_external.object({
|
|
@@ -13876,7 +13876,7 @@ var init_schema = __esm(() => {
|
|
|
13876
13876
|
drive: GoogleWorkspaceConfigSchema.describe("RFC D legacy key \u2014 use `google_workspace:` instead. Optional Google " + "Workspace onboarding configuration. When set, supplies Google OAuth " + "client credentials, the approver allowlist for `switchroom drive " + "connect`, and the optional tier knob. Env vars " + "(SWITCHROOM_GOOGLE_CLIENT_ID, SWITCHROOM_GOOGLE_CLIENT_SECRET, " + "SWITCHROOM_APPROVER_USER_ID) take precedence over this block when " + "set, preserving back-compat with the env-only flow shipped in #766."),
|
|
13877
13877
|
google_workspace: GoogleWorkspaceConfigSchema.describe("RFC G canonical key. Top-level Google Workspace configuration \u2014 " + "OAuth client credentials, approver allowlist, and tier knob (`core` " + "| `extended` | `complete`, default `core`). Mutually exclusive with " + "`drive:` at the top level (loader fails fast if both are set)."),
|
|
13878
13878
|
quota: QuotaConfigSchema.optional().describe("Optional weekly/monthly USD spend budgets rendered in the session " + "greeting. Usage is read from ccusage at runtime; no network calls."),
|
|
13879
|
-
host_control: HostControlConfigSchema.
|
|
13879
|
+
host_control: HostControlConfigSchema.default({}).describe("Host-control daemon configuration. Defaults to enabled=true since " + "RFC C Phase 2 (docs/rfcs/host-control-daemon.md). Omit the block " + "to accept defaults; set `enabled: false` only on legacy systemd-" + "mode installs (removal tracked as RFC C Phase 3)."),
|
|
13880
13880
|
google_accounts: exports_external.record(exports_external.string().regex(/^[^@\s:]+@[^@\s:]+\.[^@\s:]+$/, {
|
|
13881
13881
|
message: "Account key must be a Google account email like 'alice@example.com' (colons not allowed)"
|
|
13882
13882
|
}).transform((v) => v.trim().toLowerCase()), exports_external.object({
|
|
@@ -22669,7 +22669,7 @@ function generateCompose(opts) {
|
|
|
22669
22669
|
const buildContext = opts.buildContext;
|
|
22670
22670
|
const homePrefix = opts.homeDir ?? "${HOME}";
|
|
22671
22671
|
const containerNamePrefix = opts.containerNamePrefix ?? "switchroom";
|
|
22672
|
-
const hostControlEnabled = config.host_control?.enabled
|
|
22672
|
+
const hostControlEnabled = config.host_control?.enabled !== false;
|
|
22673
22673
|
const hostHomeForChecks = opts.homeDir ?? process.env.HOME ?? "";
|
|
22674
22674
|
const switchroomConfigPath = opts.switchroomConfigPath;
|
|
22675
22675
|
const bundledSkillsPoolDir = opts.bundledSkillsPoolDir ?? getBundledSkillsPoolDir();
|
|
@@ -22952,6 +22952,9 @@ function emitAgentService(lines, a, imageTag, buildMode, buildContext, homePrefi
|
|
|
22952
22952
|
if (existsSync13(`${hostHomeForChecks}/.switchroom/vault-audit.log`)) {
|
|
22953
22953
|
lines.push(` - ${homePrefix}/.switchroom/vault-audit.log:/state/agent/home/.switchroom/vault-audit.log:ro`);
|
|
22954
22954
|
}
|
|
22955
|
+
if (existsSync13(`${hostHomeForChecks}/.switchroom/host-control-audit.log`)) {
|
|
22956
|
+
lines.push(` - ${homePrefix}/.switchroom/host-control-audit.log:/state/agent/home/.switchroom/host-control-audit.log:ro`);
|
|
22957
|
+
}
|
|
22955
22958
|
if (hostControlEnabled && existsSync13(`${hostHomeForChecks}/.switchroom/hostd/${a.name}`)) {
|
|
22956
22959
|
lines.push(` - ${homePrefix}/.switchroom/hostd/${a.name}:/run/switchroom/hostd/${a.name}`);
|
|
22957
22960
|
}
|
|
@@ -27058,7 +27061,7 @@ function decodeResponse2(line) {
|
|
|
27058
27061
|
}
|
|
27059
27062
|
return ResponseSchema2.parse(parsed);
|
|
27060
27063
|
}
|
|
27061
|
-
var MAX_FRAME_BYTES2, PROTOCOL_VERSION = 1, ProviderNameSchema, GetCredentialsRequestSchema, ListStateRequestSchema, SetActiveRequestSchema, MarkExhaustedRequestSchema, RefreshAccountRequestSchema, AnthropicCredentialsSchema, GoogleCredentialsSchema, ProviderCredentialsSchema, AddAccountRequestSchema, RmAccountRequestSchema, SetOverrideRequestSchema, ListGoogleAccountsRequestSchema, RequestSchema2, GetCredentialsDataSchema, AccountStateSchema, AgentStateSchema, ConsumerStateSchema, ListStateDataSchema, SetActiveDataSchema, MarkExhaustedDataSchema, RefreshAccountDataSchema, AddAccountDataSchema, RmAccountDataSchema, SetOverrideDataSchema, GoogleAccountStateSchema, ListGoogleAccountsDataSchema, ErrorBodySchema, SuccessResponseSchema, ErrorResponseSchema2, ResponseSchema2;
|
|
27064
|
+
var MAX_FRAME_BYTES2, PROTOCOL_VERSION = 1, ProviderNameSchema, GetCredentialsRequestSchema, ListStateRequestSchema, SetActiveRequestSchema, MarkExhaustedRequestSchema, RefreshAccountRequestSchema, AnthropicCredentialsSchema, GoogleCredentialsSchema, ProviderCredentialsSchema, AddAccountRequestSchema, RmAccountRequestSchema, SetOverrideRequestSchema, ListGoogleAccountsRequestSchema, ProbeQuotaRequestSchema, RequestSchema2, GetCredentialsDataSchema, AccountStateSchema, AgentStateSchema, ConsumerStateSchema, ListStateDataSchema, SetActiveDataSchema, MarkExhaustedDataSchema, RefreshAccountDataSchema, AddAccountDataSchema, RmAccountDataSchema, SetOverrideDataSchema, GoogleAccountStateSchema, ListGoogleAccountsDataSchema, ErrorBodySchema, SuccessResponseSchema, ErrorResponseSchema2, ResponseSchema2;
|
|
27062
27065
|
var init_protocol2 = __esm(() => {
|
|
27063
27066
|
init_zod();
|
|
27064
27067
|
MAX_FRAME_BYTES2 = 64 * 1024;
|
|
@@ -27147,6 +27150,13 @@ var init_protocol2 = __esm(() => {
|
|
|
27147
27150
|
op: exports_external.literal("list-google-accounts"),
|
|
27148
27151
|
id: exports_external.string().min(1)
|
|
27149
27152
|
});
|
|
27153
|
+
ProbeQuotaRequestSchema = exports_external.object({
|
|
27154
|
+
v: exports_external.literal(PROTOCOL_VERSION),
|
|
27155
|
+
op: exports_external.literal("probe-quota"),
|
|
27156
|
+
id: exports_external.string().min(1),
|
|
27157
|
+
accounts: exports_external.array(exports_external.string().min(1)).min(1).max(32),
|
|
27158
|
+
timeoutMs: exports_external.number().int().positive().max(60000).optional()
|
|
27159
|
+
});
|
|
27150
27160
|
RequestSchema2 = exports_external.discriminatedUnion("op", [
|
|
27151
27161
|
GetCredentialsRequestSchema,
|
|
27152
27162
|
ListStateRequestSchema,
|
|
@@ -27156,7 +27166,8 @@ var init_protocol2 = __esm(() => {
|
|
|
27156
27166
|
AddAccountRequestSchema,
|
|
27157
27167
|
RmAccountRequestSchema,
|
|
27158
27168
|
SetOverrideRequestSchema,
|
|
27159
|
-
ListGoogleAccountsRequestSchema
|
|
27169
|
+
ListGoogleAccountsRequestSchema,
|
|
27170
|
+
ProbeQuotaRequestSchema
|
|
27160
27171
|
]);
|
|
27161
27172
|
GetCredentialsDataSchema = exports_external.object({
|
|
27162
27173
|
account: exports_external.string(),
|
|
@@ -27325,6 +27336,16 @@ class AuthBrokerClient {
|
|
|
27325
27336
|
});
|
|
27326
27337
|
return data;
|
|
27327
27338
|
}
|
|
27339
|
+
async probeQuota(accounts, timeoutMs) {
|
|
27340
|
+
const data = await this.send({
|
|
27341
|
+
v: PROTOCOL_VERSION,
|
|
27342
|
+
id: randomUUID2(),
|
|
27343
|
+
op: "probe-quota",
|
|
27344
|
+
accounts: [...accounts],
|
|
27345
|
+
...timeoutMs !== undefined ? { timeoutMs } : {}
|
|
27346
|
+
});
|
|
27347
|
+
return data;
|
|
27348
|
+
}
|
|
27328
27349
|
async setActive(account) {
|
|
27329
27350
|
const data = await this.send({
|
|
27330
27351
|
v: PROTOCOL_VERSION,
|
|
@@ -45313,8 +45334,8 @@ var {
|
|
|
45313
45334
|
} = import__.default;
|
|
45314
45335
|
|
|
45315
45336
|
// src/build-info.ts
|
|
45316
|
-
var VERSION = "0.11.
|
|
45317
|
-
var COMMIT_SHA = "
|
|
45337
|
+
var VERSION = "0.11.1";
|
|
45338
|
+
var COMMIT_SHA = "f5d84dfb";
|
|
45318
45339
|
|
|
45319
45340
|
// src/cli/deprecated.ts
|
|
45320
45341
|
init_source();
|
|
@@ -53242,6 +53263,10 @@ async function ensureHostMountSources(config) {
|
|
|
53242
53263
|
if (!existsSync23(auditLogPath)) {
|
|
53243
53264
|
writeFileSync13(auditLogPath, "", { mode: 420 });
|
|
53244
53265
|
}
|
|
53266
|
+
const hostdAuditLogPath = join18(home2, ".switchroom", "host-control-audit.log");
|
|
53267
|
+
if (!existsSync23(hostdAuditLogPath)) {
|
|
53268
|
+
writeFileSync13(hostdAuditLogPath, "", { mode: 420 });
|
|
53269
|
+
}
|
|
53245
53270
|
}
|
|
53246
53271
|
function detectComposeV2() {
|
|
53247
53272
|
try {
|
|
@@ -11286,7 +11286,7 @@ var QuotaConfigSchema = exports_external.object({
|
|
|
11286
11286
|
monthly_budget_usd: exports_external.number().positive().optional().describe("Monthly USD spend budget. If unset, the greeting shows raw usage only.")
|
|
11287
11287
|
});
|
|
11288
11288
|
var HostControlConfigSchema = exports_external.object({
|
|
11289
|
-
enabled: exports_external.boolean().
|
|
11289
|
+
enabled: exports_external.boolean().default(true).describe("Whether the host-control daemon is in use. Default: true (since " + "RFC C Phase 2 default-flip — the gateway's /restart, /new, /reset, " + "and /update apply slash-commands all dispatch through hostd, and " + "without it those verbs fail on docker-mode installs because the " + "agent container has no docker binary/socket). " + "When true, the compose generator emits per-agent bind mounts " + "at `~/.switchroom/hostd/<name>/sock` for every admin-flagged " + "agent. Install the daemon with `switchroom hostd install` — " + "it runs as a docker container in its own compose project " + "(`switchroom-hostd`), separate from the agent fleet's compose " + "project so `up -d --remove-orphans` cycles of the fleet " + "can't recreate the daemon mid-RPC. See RFC C §5.1. " + "Set enabled: false only on legacy systemd-mode installs that " + "still rely on the in-container `spawnSwitchroomDetached` " + "shellout (removal is tracked as RFC C Phase 3).")
|
|
11290
11290
|
});
|
|
11291
11291
|
var SwitchroomConfigSchema = exports_external.object({
|
|
11292
11292
|
switchroom: exports_external.object({
|
|
@@ -11312,7 +11312,7 @@ var SwitchroomConfigSchema = exports_external.object({
|
|
|
11312
11312
|
drive: GoogleWorkspaceConfigSchema.describe("RFC D legacy key — use `google_workspace:` instead. Optional Google " + "Workspace onboarding configuration. When set, supplies Google OAuth " + "client credentials, the approver allowlist for `switchroom drive " + "connect`, and the optional tier knob. Env vars " + "(SWITCHROOM_GOOGLE_CLIENT_ID, SWITCHROOM_GOOGLE_CLIENT_SECRET, " + "SWITCHROOM_APPROVER_USER_ID) take precedence over this block when " + "set, preserving back-compat with the env-only flow shipped in #766."),
|
|
11313
11313
|
google_workspace: GoogleWorkspaceConfigSchema.describe("RFC G canonical key. Top-level Google Workspace configuration — " + "OAuth client credentials, approver allowlist, and tier knob (`core` " + "| `extended` | `complete`, default `core`). Mutually exclusive with " + "`drive:` at the top level (loader fails fast if both are set)."),
|
|
11314
11314
|
quota: QuotaConfigSchema.optional().describe("Optional weekly/monthly USD spend budgets rendered in the session " + "greeting. Usage is read from ccusage at runtime; no network calls."),
|
|
11315
|
-
host_control: HostControlConfigSchema.
|
|
11315
|
+
host_control: HostControlConfigSchema.default({}).describe("Host-control daemon configuration. Defaults to enabled=true since " + "RFC C Phase 2 (docs/rfcs/host-control-daemon.md). Omit the block " + "to accept defaults; set `enabled: false` only on legacy systemd-" + "mode installs (removal tracked as RFC C Phase 3)."),
|
|
11316
11316
|
google_accounts: exports_external.record(exports_external.string().regex(/^[^@\s:]+@[^@\s:]+\.[^@\s:]+$/, {
|
|
11317
11317
|
message: "Account key must be a Google account email like 'alice@example.com' (colons not allowed)"
|
|
11318
11318
|
}).transform((v) => v.trim().toLowerCase()), exports_external.object({
|
|
@@ -11278,7 +11278,7 @@ var init_schema = __esm(() => {
|
|
|
11278
11278
|
monthly_budget_usd: exports_external.number().positive().optional().describe("Monthly USD spend budget. If unset, the greeting shows raw usage only.")
|
|
11279
11279
|
});
|
|
11280
11280
|
HostControlConfigSchema = exports_external.object({
|
|
11281
|
-
enabled: exports_external.boolean().
|
|
11281
|
+
enabled: exports_external.boolean().default(true).describe("Whether the host-control daemon is in use. Default: true (since " + "RFC C Phase 2 default-flip — the gateway's /restart, /new, /reset, " + "and /update apply slash-commands all dispatch through hostd, and " + "without it those verbs fail on docker-mode installs because the " + "agent container has no docker binary/socket). " + "When true, the compose generator emits per-agent bind mounts " + "at `~/.switchroom/hostd/<name>/sock` for every admin-flagged " + "agent. Install the daemon with `switchroom hostd install` — " + "it runs as a docker container in its own compose project " + "(`switchroom-hostd`), separate from the agent fleet's compose " + "project so `up -d --remove-orphans` cycles of the fleet " + "can't recreate the daemon mid-RPC. See RFC C §5.1. " + "Set enabled: false only on legacy systemd-mode installs that " + "still rely on the in-container `spawnSwitchroomDetached` " + "shellout (removal is tracked as RFC C Phase 3).")
|
|
11282
11282
|
});
|
|
11283
11283
|
SwitchroomConfigSchema = exports_external.object({
|
|
11284
11284
|
switchroom: exports_external.object({
|
|
@@ -11304,7 +11304,7 @@ var init_schema = __esm(() => {
|
|
|
11304
11304
|
drive: GoogleWorkspaceConfigSchema.describe("RFC D legacy key — use `google_workspace:` instead. Optional Google " + "Workspace onboarding configuration. When set, supplies Google OAuth " + "client credentials, the approver allowlist for `switchroom drive " + "connect`, and the optional tier knob. Env vars " + "(SWITCHROOM_GOOGLE_CLIENT_ID, SWITCHROOM_GOOGLE_CLIENT_SECRET, " + "SWITCHROOM_APPROVER_USER_ID) take precedence over this block when " + "set, preserving back-compat with the env-only flow shipped in #766."),
|
|
11305
11305
|
google_workspace: GoogleWorkspaceConfigSchema.describe("RFC G canonical key. Top-level Google Workspace configuration — " + "OAuth client credentials, approver allowlist, and tier knob (`core` " + "| `extended` | `complete`, default `core`). Mutually exclusive with " + "`drive:` at the top level (loader fails fast if both are set)."),
|
|
11306
11306
|
quota: QuotaConfigSchema.optional().describe("Optional weekly/monthly USD spend budgets rendered in the session " + "greeting. Usage is read from ccusage at runtime; no network calls."),
|
|
11307
|
-
host_control: HostControlConfigSchema.
|
|
11307
|
+
host_control: HostControlConfigSchema.default({}).describe("Host-control daemon configuration. Defaults to enabled=true since " + "RFC C Phase 2 (docs/rfcs/host-control-daemon.md). Omit the block " + "to accept defaults; set `enabled: false` only on legacy systemd-" + "mode installs (removal tracked as RFC C Phase 3)."),
|
|
11308
11308
|
google_accounts: exports_external.record(exports_external.string().regex(/^[^@\s:]+@[^@\s:]+\.[^@\s:]+$/, {
|
|
11309
11309
|
message: "Account key must be a Google account email like 'alice@example.com' (colons not allowed)"
|
|
11310
11310
|
}).transform((v) => v.trim().toLowerCase()), exports_external.object({
|
|
@@ -11278,7 +11278,7 @@ var init_schema = __esm(() => {
|
|
|
11278
11278
|
monthly_budget_usd: exports_external.number().positive().optional().describe("Monthly USD spend budget. If unset, the greeting shows raw usage only.")
|
|
11279
11279
|
});
|
|
11280
11280
|
HostControlConfigSchema = exports_external.object({
|
|
11281
|
-
enabled: exports_external.boolean().
|
|
11281
|
+
enabled: exports_external.boolean().default(true).describe("Whether the host-control daemon is in use. Default: true (since " + "RFC C Phase 2 default-flip — the gateway's /restart, /new, /reset, " + "and /update apply slash-commands all dispatch through hostd, and " + "without it those verbs fail on docker-mode installs because the " + "agent container has no docker binary/socket). " + "When true, the compose generator emits per-agent bind mounts " + "at `~/.switchroom/hostd/<name>/sock` for every admin-flagged " + "agent. Install the daemon with `switchroom hostd install` — " + "it runs as a docker container in its own compose project " + "(`switchroom-hostd`), separate from the agent fleet's compose " + "project so `up -d --remove-orphans` cycles of the fleet " + "can't recreate the daemon mid-RPC. See RFC C §5.1. " + "Set enabled: false only on legacy systemd-mode installs that " + "still rely on the in-container `spawnSwitchroomDetached` " + "shellout (removal is tracked as RFC C Phase 3).")
|
|
11282
11282
|
});
|
|
11283
11283
|
SwitchroomConfigSchema = exports_external.object({
|
|
11284
11284
|
switchroom: exports_external.object({
|
|
@@ -11304,7 +11304,7 @@ var init_schema = __esm(() => {
|
|
|
11304
11304
|
drive: GoogleWorkspaceConfigSchema.describe("RFC D legacy key — use `google_workspace:` instead. Optional Google " + "Workspace onboarding configuration. When set, supplies Google OAuth " + "client credentials, the approver allowlist for `switchroom drive " + "connect`, and the optional tier knob. Env vars " + "(SWITCHROOM_GOOGLE_CLIENT_ID, SWITCHROOM_GOOGLE_CLIENT_SECRET, " + "SWITCHROOM_APPROVER_USER_ID) take precedence over this block when " + "set, preserving back-compat with the env-only flow shipped in #766."),
|
|
11305
11305
|
google_workspace: GoogleWorkspaceConfigSchema.describe("RFC G canonical key. Top-level Google Workspace configuration — " + "OAuth client credentials, approver allowlist, and tier knob (`core` " + "| `extended` | `complete`, default `core`). Mutually exclusive with " + "`drive:` at the top level (loader fails fast if both are set)."),
|
|
11306
11306
|
quota: QuotaConfigSchema.optional().describe("Optional weekly/monthly USD spend budgets rendered in the session " + "greeting. Usage is read from ccusage at runtime; no network calls."),
|
|
11307
|
-
host_control: HostControlConfigSchema.
|
|
11307
|
+
host_control: HostControlConfigSchema.default({}).describe("Host-control daemon configuration. Defaults to enabled=true since " + "RFC C Phase 2 (docs/rfcs/host-control-daemon.md). Omit the block " + "to accept defaults; set `enabled: false` only on legacy systemd-" + "mode installs (removal tracked as RFC C Phase 3)."),
|
|
11308
11308
|
google_accounts: exports_external.record(exports_external.string().regex(/^[^@\s:]+@[^@\s:]+\.[^@\s:]+$/, {
|
|
11309
11309
|
message: "Account key must be a Google account email like 'alice@example.com' (colons not allowed)"
|
|
11310
11310
|
}).transform((v) => v.trim().toLowerCase()), exports_external.object({
|
package/package.json
CHANGED
|
@@ -18,8 +18,8 @@
|
|
|
18
18
|
*
|
|
19
19
|
* What this module does:
|
|
20
20
|
*
|
|
21
|
-
* 1. Probe live quota for every account in parallel
|
|
22
|
-
* (`
|
|
21
|
+
* 1. Probe live quota for every account in parallel via the
|
|
22
|
+
* broker (`client.probeQuota(...)`, #1336) so we pick the best
|
|
23
23
|
* target with current data, not stale broker disk-cache.
|
|
24
24
|
* 2. Skip blocked accounts entirely; pick the lowest-utilization
|
|
25
25
|
* healthy candidate (or, if none, the lowest throttling one).
|
|
@@ -79,8 +79,8 @@ export interface FleetFallbackDeps {
|
|
|
79
79
|
* is testable without spinning up a UDS. */
|
|
80
80
|
state: ListStateData;
|
|
81
81
|
/** Parallel array of live quota probes, same order as `state.accounts`.
|
|
82
|
-
*
|
|
83
|
-
*
|
|
82
|
+
* Get via `client.probeQuota(state.accounts.map(a => a.label))`
|
|
83
|
+
* and map the response back to per-account results (#1336). */
|
|
84
84
|
quotas: QuotaResult[];
|
|
85
85
|
/** Broker `setActive` invoker. Returns the result for logging. */
|
|
86
86
|
setActive: (label: string) => Promise<{ active: string; fanned: string[] }>;
|