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.
@@ -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().optional().describe("Opt-in to the host-control daemon. Default: false. " + "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. " + "Since Phase 2 (#1175 PR γ) the gateway's /restart, /new, /reset, " + "and /update apply slash-commands automatically dispatch through " + "hostd when enabled — replacing the in-container " + "`spawnSwitchroomDetached` shellout that requires docker access. " + "Set enabled: true on docker-mode installs to make those verbs work " + "(they otherwise fail because the agent container has no docker " + "binary/socket).")
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.optional().describe("Optional host-control daemon configuration. See RFC C " + "(docs/rfcs/host-control-daemon.md) and the field-level help on " + "`enabled` for the Phase 1 scope."),
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().optional().describe("Opt-in to the host-control daemon. Default: false. " + "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. " + "Since Phase 2 (#1175 PR γ) the gateway's /restart, /new, /reset, " + "and /update apply slash-commands automatically dispatch through " + "hostd when enabled — replacing the in-container " + "`spawnSwitchroomDetached` shellout that requires docker access. " + "Set enabled: true on docker-mode installs to make those verbs work " + "(they otherwise fail because the agent container has no docker " + "binary/socket).")
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.optional().describe("Optional host-control daemon configuration. See RFC C " + "(docs/rfcs/host-control-daemon.md) and the field-level help on " + "`enabled` for the Phase 1 scope."),
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,
@@ -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().optional().describe("Opt-in to the host-control daemon. Default: false. " + "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. " + "Since Phase 2 (#1175 PR \u03b3) the gateway's /restart, /new, /reset, " + "and /update apply slash-commands automatically dispatch through " + "hostd when enabled \u2014 replacing the in-container " + "`spawnSwitchroomDetached` shellout that requires docker access. " + "Set enabled: true on docker-mode installs to make those verbs work " + "(they otherwise fail because the agent container has no docker " + "binary/socket).")
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.optional().describe("Optional host-control daemon configuration. See RFC C " + "(docs/rfcs/host-control-daemon.md) and the field-level help on " + "`enabled` for the Phase 1 scope."),
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 === true;
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.0";
45317
- var COMMIT_SHA = "abff20c7";
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().optional().describe("Opt-in to the host-control daemon. Default: false. " + "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. " + "Since Phase 2 (#1175 PR γ) the gateway's /restart, /new, /reset, " + "and /update apply slash-commands automatically dispatch through " + "hostd when enabled — replacing the in-container " + "`spawnSwitchroomDetached` shellout that requires docker access. " + "Set enabled: true on docker-mode installs to make those verbs work " + "(they otherwise fail because the agent container has no docker " + "binary/socket).")
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.optional().describe("Optional host-control daemon configuration. See RFC C " + "(docs/rfcs/host-control-daemon.md) and the field-level help on " + "`enabled` for the Phase 1 scope."),
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().optional().describe("Opt-in to the host-control daemon. Default: false. " + "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. " + "Since Phase 2 (#1175 PR γ) the gateway's /restart, /new, /reset, " + "and /update apply slash-commands automatically dispatch through " + "hostd when enabled — replacing the in-container " + "`spawnSwitchroomDetached` shellout that requires docker access. " + "Set enabled: true on docker-mode installs to make those verbs work " + "(they otherwise fail because the agent container has no docker " + "binary/socket).")
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.optional().describe("Optional host-control daemon configuration. See RFC C " + "(docs/rfcs/host-control-daemon.md) and the field-level help on " + "`enabled` for the Phase 1 scope."),
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().optional().describe("Opt-in to the host-control daemon. Default: false. " + "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. " + "Since Phase 2 (#1175 PR γ) the gateway's /restart, /new, /reset, " + "and /update apply slash-commands automatically dispatch through " + "hostd when enabled — replacing the in-container " + "`spawnSwitchroomDetached` shellout that requires docker access. " + "Set enabled: true on docker-mode installs to make those verbs work " + "(they otherwise fail because the agent container has no docker " + "binary/socket).")
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.optional().describe("Optional host-control daemon configuration. See RFC C " + "(docs/rfcs/host-control-daemon.md) and the field-level help on " + "`enabled` for the Phase 1 scope."),
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "switchroom",
3
- "version": "0.11.0",
3
+ "version": "0.11.1",
4
4
  "description": "Run Claude Code 24/7 on your Claude Pro/Max subscription over Telegram. Open-source alternative to OpenClaw and NanoClaw — no API keys.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -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
- * (`fetchAccountQuota({force: true})`) so we pick the best
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
- * Use `Promise.all(state.accounts.map(a =>
83
- * fetchAccountQuota(a.label, {force: true})))`. */
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[] }>;