switchroom 0.20.9 → 0.20.11

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.
Files changed (59) hide show
  1. package/bin/handoff-briefing.sh +57 -5
  2. package/bin/working-state-reload-hook.sh +262 -0
  3. package/dist/agent-scheduler/index.js +65 -2
  4. package/dist/auth-broker/index.js +204 -24
  5. package/dist/cli/notion-write-pretool.mjs +65 -2
  6. package/dist/cli/self-improve-apply-guard-pretool.mjs +357 -92
  7. package/dist/cli/self-improve-stop.mjs +889 -7
  8. package/dist/cli/skill-validate-pretool.mjs +82 -3
  9. package/dist/cli/switchroom.js +3699 -2110
  10. package/dist/host-control/main.js +67 -4
  11. package/dist/vault/approvals/kernel-server.js +66 -3
  12. package/dist/vault/broker/server.js +66 -3
  13. package/package.json +1 -1
  14. package/profiles/_base/start.sh.hbs +49 -0
  15. package/profiles/_shared/agent-self-service.md.hbs +15 -22
  16. package/profiles/_shared/delegation-golden-rule.md.hbs +1 -1
  17. package/profiles/_shared/dev-protocol.md.hbs +1 -1
  18. package/profiles/_shared/execution-discipline.md.hbs +4 -4
  19. package/profiles/_shared/vault-protocol.md.hbs +2 -18
  20. package/profiles/default/CLAUDE.md.hbs +3 -5
  21. package/telegram-plugin/auto-fallback-fleet.ts +37 -2
  22. package/telegram-plugin/dist/gateway/gateway.js +1414 -918
  23. package/telegram-plugin/fallback-card-collapse.ts +1 -0
  24. package/telegram-plugin/gateway/auth-command.ts +11 -1
  25. package/telegram-plugin/gateway/callback-query-handlers.ts +100 -0
  26. package/telegram-plugin/gateway/eval-case-proposal-card.ts +86 -0
  27. package/telegram-plugin/gateway/fleet-fallback-notice-cooldown.test.ts +74 -0
  28. package/telegram-plugin/gateway/fleet-fallback-notice-cooldown.ts +71 -0
  29. package/telegram-plugin/gateway/gateway.ts +85 -90
  30. package/telegram-plugin/gateway/ipc-protocol.ts +43 -0
  31. package/telegram-plugin/gateway/ipc-server.ts +28 -0
  32. package/telegram-plugin/gateway/narrative-lane.ts +33 -2
  33. package/telegram-plugin/gateway/privacy-reset.test.ts +216 -0
  34. package/telegram-plugin/gateway/privacy-reset.ts +87 -0
  35. package/telegram-plugin/gateway/privacy-state.test.ts +165 -0
  36. package/telegram-plugin/gateway/privacy-state.ts +206 -0
  37. package/telegram-plugin/gateway/self-improve-proposal-wiring.ts +176 -0
  38. package/telegram-plugin/gateway/stale-pin-sweep-wiring.ts +24 -14
  39. package/telegram-plugin/gateway/stale-pin-sweep.test.ts +123 -26
  40. package/telegram-plugin/gateway/stale-pin-sweep.ts +48 -32
  41. package/telegram-plugin/gateway/throttle-tier-wiring.ts +15 -4
  42. package/telegram-plugin/slot-banner-driver.ts +42 -5
  43. package/telegram-plugin/tests/auto-fallback-fleet.test.ts +24 -0
  44. package/telegram-plugin/tests/gateway-handler-registration-wiring.test.ts +2 -0
  45. package/telegram-plugin/tests/narrative-lane-golden.test.ts +97 -0
  46. package/telegram-plugin/tests/privacy-reset-call-sites.test.ts +120 -0
  47. package/telegram-plugin/tests/status-pin-store.test.ts +25 -0
  48. package/telegram-plugin/tests/throttle-tier.test.ts +16 -0
  49. package/telegram-plugin/tests/turn-flush-safety.test.ts +67 -0
  50. package/telegram-plugin/throttle-tier.ts +12 -3
  51. package/telegram-plugin/turn-flush-safety.ts +97 -0
  52. package/vendor/hindsight-memory/CHANGELOG.md +31 -0
  53. package/vendor/hindsight-memory/hooks/hooks.json +2 -1
  54. package/vendor/hindsight-memory/scripts/retain.py +306 -0
  55. package/vendor/hindsight-memory/scripts/session_start.py +35 -8
  56. package/vendor/hindsight-memory/scripts/subagent_retain.py +29 -1
  57. package/vendor/hindsight-memory/scripts/tests/test_private_mode.py +415 -0
  58. package/vendor/hindsight-memory/scripts/tests/test_self_improve_correction_tag.py +167 -0
  59. package/vendor/hindsight-memory/scripts/tests/test_session_start_durability.py +107 -0
@@ -10994,7 +10994,7 @@ var init_observation_scopes = __esm(() => {
10994
10994
  });
10995
10995
 
10996
10996
  // src/config/schema.ts
10997
- var CodeRepoEntrySchema, AgentBindMountSchema, HttpDiffPollSchema, PollSpecSchema, TelegramMessageActionSchema, WebhookActionSchema, ActionSpecSchema, ScheduleEntrySchema, AgentSoulSchema, AgentToolsSchema, ObservationScopesSchema, ObservationScopeStrategySchema, AntiConfabulationDirectiveSchema, AgentMemorySchema, HookEntrySchema, AgentHooksSchema, SubagentSchema, SessionSchema, SessionContinuitySchema, webhookDispatchRule, TelegramChannelSchema, BuzzChannelSchema, ChannelsSchema, TIMEZONE_REGEX, ApproverIdSchema, GoogleWorkspaceTierSchema, GoogleServiceTokenSchema, GoogleWorkspaceConfigSchema, LiteLLMConfigSchema, HindsightPerOpLlmSchema, HindsightConfigSchema, MicrosoftWorkspaceConfigSchema, NotionWorkspaceConfigSchema, AgentGoogleWorkspaceConfigSchema, MicrosoftAccountEmailSchema, MicrosoftToolTokenSchema, MicrosoftAccountBindingSchema, AgentMicrosoftWorkspaceConfigSchema, AgentNotionWorkspaceConfigSchema, ReactionsSchema, ReactionDispatchSchema, releaseBlockFields, ReleaseBlock, RootReleaseBlock, NetworkIsolationSchema, servesField, knowsField, profileFields, ProfileSchema, _omitExtends, defaultsFields, AgentDefaultsSchema, AgentSchema, TelegramConfigSchema, MemoryBackendConfigSchema, VaultConfigSchema, QuotaConfigSchema, AutoReleaseCheckSchema, HostControlConfigSchema, WebServiceConfigSchema, FleetHealthConfigSchema, HostdConfigSchema, CronEgressSchema, CronConfigSchema, UserSchema, SwitchroomConfigSchema;
10997
+ var CodeRepoEntrySchema, AgentBindMountSchema, HttpDiffPollSchema, PollSpecSchema, TelegramMessageActionSchema, WebhookActionSchema, ActionSpecSchema, ScheduleEntrySchema, AgentSoulSchema, AgentToolsSchema, ObservationScopesSchema, ObservationScopeStrategySchema, AntiConfabulationDirectiveSchema, AgentMemorySchema, HookEntrySchema, AgentHooksSchema, SubagentSchema, SessionSchema, SessionContinuitySchema, webhookDispatchRule, TelegramChannelSchema, BuzzChannelSchema, ChannelsSchema, TIMEZONE_REGEX, ApproverIdSchema, GoogleWorkspaceTierSchema, GoogleServiceTokenSchema, GoogleWorkspaceConfigSchema, LiteLLMConfigSchema, HindsightPerOpLlmSchema, HindsightConfigSchema, MicrosoftWorkspaceConfigSchema, NotionWorkspaceConfigSchema, AgentGoogleWorkspaceConfigSchema, MicrosoftAccountEmailSchema, MicrosoftToolTokenSchema, MicrosoftAccountBindingSchema, AgentMicrosoftWorkspaceConfigSchema, AgentNotionWorkspaceConfigSchema, ReactionsSchema, ReactionDispatchSchema, releaseBlockFields, ReleaseBlock, RootReleaseBlock, NetworkIsolationSchema, servesField, knowsField, profileFields, ProfileSchema, _omitExtends, defaultsFields, AgentDefaultsSchema, AgentSchema, TelegramConfigSchema, MemoryBackendConfigSchema, VaultConfigSchema, QuotaConfigSchema, AutoReleaseCheckSchema, HostControlConfigSchema, WebServiceConfigSchema, FleetHealthConfigSchema, HostdConfigSchema, CronEgressSchema, CronConfigSchema, UserSchema, ConfigRepoConfigSchema, SwitchroomConfigSchema;
10998
10998
  var init_schema = __esm(() => {
10999
10999
  init_zod();
11000
11000
  init_observation_scopes();
@@ -11691,8 +11691,10 @@ var init_schema = __esm(() => {
11691
11691
  bot_username: exports_external.string().optional().describe("Per-agent Telegram bot username (without leading @) when it doesn't " + "contain the agent slug. Replaces the default 'username includes slug' " + "preflight check with an exact (case-insensitive) match. Use when an " + "agent and its bot have intentionally divergent names (e.g. agent " + "'lawgpt' paired with bot '@meken_law_bot')."),
11692
11692
  timezone: exports_external.string().regex(TIMEZONE_REGEX, "timezone must be an IANA zone name like 'Australia/Melbourne' or 'UTC' " + "(three-letter aliases like EST/PST and bare offsets like UTC+10 are not accepted)").optional().describe("Per-agent IANA timezone override. Wins over any profile/defaults " + "value and over the top-level switchroom.timezone global. Controls " + "the UserPromptSubmit timezone hook's emitted local time and the " + "agent container's `environment.TZ` in compose."),
11693
11693
  auth: exports_external.object({
11694
- override: exports_external.string().min(1).optional().describe("Per-agent override of the fleet-wide `auth.active`. Edge-case use only — " + "this agent talks to the named account regardless of fleet active. See RFC H §4.5.")
11695
- }).optional().describe("Account routing for switchroom-auth-broker. RFC H schema uses " + "fleet-wide `auth.active` plus per-agent `override:` for edge cases. " + "Pre-RFC-H `auth.accounts: [..]` and `auth_label:` are migrated in-place " + "on first apply (see src/auth/migrate-schema.ts)."),
11694
+ override: exports_external.string().min(1).optional().describe("Per-agent override of the fleet-wide `auth.active`. Edge-case use only — " + "this agent talks to the named account regardless of fleet active. See RFC H §4.5."),
11695
+ strict: exports_external.boolean().optional().describe("Requires `override`. When true, the pin is a hard binding, not a " + "routing preference: the broker NEVER serves this agent from any " + "other account — no failover to `auth.fallback_order` while the " + "pinned account is quota-walled or exhausted. The agent rides out " + "the wall on its own account (surfacing the normal 429/quota " + "cards) instead of silently borrowing fleet quota. Use for " + "accounts that must never cross a billing/compliance boundary " + "(e.g. an employer-provided subscription)."),
11696
+ exclusive: exports_external.boolean().optional().describe("Requires `override`. When true, the pinned account belongs to " + "THIS agent alone: the broker refuses to serve it to any other " + "agent or consumer, refuses `auth use <label>` (set-active) onto " + "it, and refuses pinning another agent to it. Config that routes " + "others to the account (`auth.active`, `auth.fallback_order`, " + "another agent's `override`, a consumer pin) is rejected at load. " + "Usually paired with `strict: true` for full two-way isolation.")
11697
+ }).optional().describe("Account routing for switchroom-auth-broker. RFC H schema uses " + "fleet-wide `auth.active` plus per-agent `override:` for edge cases, " + "with optional `strict:` (never borrow another account) and " + "`exclusive:` (no one else may use the pinned account) hardening. " + "Pre-RFC-H `auth.accounts: [..]` and `auth_label:` are migrated in-place " + "on first apply (see src/auth/migrate-schema.ts)."),
11696
11698
  dm_only: exports_external.boolean().optional().describe("Mark this agent as a DM-only bot — has its own bot_token and lives " + "exclusively in a private chat with the operator. Suppresses " + "scaffolding's default behavior of inheriting the global " + "telegram.forum_chat_id into the agent's access.json `groups` entry " + "(the forum chat the bot isn't a member of, which would otherwise " + "trigger a 'boot-probe-failed: 400 chat not found' warning every " + "restart). topic_name is still schema-required but unused — set it " + "to a display label like 'DM' for /switchroom status output."),
11697
11699
  topic_name: exports_external.string().describe("Telegram forum topic display name"),
11698
11700
  topic_emoji: exports_external.string().optional().describe("Emoji for the topic (e.g., '\uD83C\uDFCB️')"),
@@ -11767,6 +11769,17 @@ var init_schema = __esm(() => {
11767
11769
  tmp_size: exports_external.string().regex(/^\d+(\.\d+)?[kmgKMG]?$/, "tmp_size must be a Docker size string like '1g', '4g', '512m'").refine((v) => parseFloat(v) > 0, "tmp_size must be greater than zero").optional()
11768
11770
  }).optional()
11769
11771
  }).superRefine((agent, ctx) => {
11772
+ if (agent.auth && !agent.auth.override) {
11773
+ for (const flag of ["strict", "exclusive"]) {
11774
+ if (agent.auth[flag]) {
11775
+ ctx.addIssue({
11776
+ code: exports_external.ZodIssueCode.custom,
11777
+ message: `\`auth.${flag}: true\` requires \`auth.override: <account>\` — there is no pinned account for it to apply to.`,
11778
+ path: ["auth", flag]
11779
+ });
11780
+ }
11781
+ }
11782
+ }
11770
11783
  if (agent.dm_only !== true)
11771
11784
  return;
11772
11785
  const tg = agent.channels?.telegram;
@@ -11879,6 +11892,13 @@ var init_schema = __esm(() => {
11879
11892
  profile_bank: exports_external.string().describe("Hindsight bank holding this user's memory profile (author via " + "`switchroom memory profile add <bank> ...`)."),
11880
11893
  person_id: exports_external.string().optional().describe("Free-text display name projected into the inbound `<channel>` " + 'tag\'s `user` attribute (e.g. "Lisa") so an agent can greet by ' + "name instead of only seeing the raw Telegram id/username. NOT a " + "stable identity system — just a label. Resolution is boot-time-only " + "(no hot-reload; a config change needs an agent restart) and " + "chat-scoped (a resolved name is only shown in a chat/group the " + "person is actually a member of, per that chat's access.json " + "membership — never broadcast into every chat the agent operates " + "in). Keep this broadly safe to show, the same discipline as " + "picking a `profile_bank` name: there is no automated enforcement " + "that a `person_id` stays safe if a group's membership changes " + "later — see docs/configuration.md.")
11881
11894
  });
11895
+ ConfigRepoConfigSchema = exports_external.object({
11896
+ enabled: exports_external.boolean().default(false).describe("Master switch for config-repo change control. Default false (opt-in). " + "When false the `config-repo sync` verb still runs on demand but the " + "doctor check and (later) the scheduled tick treat the feature as off."),
11897
+ path: exports_external.string().regex(/^[a-zA-Z0-9~._\-/]+$/, "config_repo.path must not contain shell-special characters ($, `, \", ', \\, etc.)").default("~/.switchroom-config").describe("Filesystem path to the operator's private config repo. Must be a git " + "repo (else `switchroom doctor` FAILs). Tilde-expanded at read time."),
11898
+ push: exports_external.boolean().default(true).describe("When true, `config-repo sync` pushes after committing (subject to the " + "`require_private` gate). When false, it commits locally only — useful " + "for offline hosts or a review-before-push workflow."),
11899
+ remote: exports_external.string().min(1).default("origin").describe("Git remote name to push to. Default `origin`."),
11900
+ require_private: exports_external.boolean().default(true).describe("Refuse to PUSH unless the GitHub API confirms the remote repo is " + "private. On a public remote — or when the API is unreachable — the " + "push is skipped (commits still land locally) and a WARN is emitted. " + "Fail-safe against exfiltrating memory files / workspace state to a " + "repo that has been flipped public. Default true.")
11901
+ });
11882
11902
  SwitchroomConfigSchema = exports_external.object({
11883
11903
  switchroom: exports_external.object({
11884
11904
  version: exports_external.literal(1).describe("Config schema version"),
@@ -11891,6 +11911,7 @@ var init_schema = __esm(() => {
11891
11911
  memory: MemoryBackendConfigSchema.optional(),
11892
11912
  hindsight: HindsightConfigSchema.optional().describe("Fleet-singleton Hindsight (memory backend) configuration. Currently " + "just the LLM knob (provider + model) used for retain/reflect/" + "consolidation. Both fields optional; when unset the container falls " + "back to provider=claude-code + the hard-coded HINDSIGHT_DEFAULT_MODEL " + "so nothing changes for operators who don't set it. Read at container " + "launch by startHindsight() (src/setup/hindsight.ts) — takes effect on " + "the next `switchroom apply` / `memory setup --recreate`."),
11893
11913
  vault: VaultConfigSchema.optional(),
11914
+ config_repo: ConfigRepoConfigSchema.optional().describe("Change control for the operator's private ~/.switchroom-config git repo " + "(backup + audit trail of live host config, agent workspace state, and " + "mirrored personal skills). Consumed by `switchroom config-repo sync` and " + "the `config repo` doctor check. Optional and OFF by default."),
11894
11915
  auth: exports_external.object({
11895
11916
  active: exports_external.string().min(1).optional().describe("Fleet-wide active Anthropic account label. Every agent without " + "an explicit `agent.auth.override` uses this account. See " + "docs/auth.md for the full model. Set by `switchroom auth use <label>`."),
11896
11917
  fallback_order: exports_external.array(exports_external.string().min(1)).optional().describe("Ordered list of account labels for `switchroom auth rotate` to cycle " + "through when the active account hits a quota event. First entry is " + "normally the same as `auth.active`. When unset, `rotate` is a no-op."),
@@ -11958,6 +11979,48 @@ var init_schema = __esm(() => {
11958
11979
  for (const [name, a] of Object.entries(cfg.agents ?? {})) {
11959
11980
  checkServes(a.serves, ["agents", name, "serves"]);
11960
11981
  }
11982
+ for (const [name, a] of Object.entries(cfg.agents ?? {})) {
11983
+ const agentAuth = a.auth;
11984
+ if (!agentAuth?.exclusive || !agentAuth.override)
11985
+ continue;
11986
+ const acct = agentAuth.override;
11987
+ if (cfg.auth?.active === acct) {
11988
+ ctx.addIssue({
11989
+ code: exports_external.ZodIssueCode.custom,
11990
+ message: `account '${acct}' is exclusive to agent '${name}' ` + `(agents.${name}.auth.exclusive) — it cannot be the fleet \`auth.active\``,
11991
+ path: ["auth", "active"]
11992
+ });
11993
+ }
11994
+ const fbIdx = (cfg.auth?.fallback_order ?? []).indexOf(acct);
11995
+ if (fbIdx !== -1) {
11996
+ ctx.addIssue({
11997
+ code: exports_external.ZodIssueCode.custom,
11998
+ message: `account '${acct}' is exclusive to agent '${name}' ` + `(agents.${name}.auth.exclusive) — it cannot appear in \`auth.fallback_order\``,
11999
+ path: ["auth", "fallback_order", fbIdx]
12000
+ });
12001
+ }
12002
+ for (const [other, oa] of Object.entries(cfg.agents ?? {})) {
12003
+ if (other === name)
12004
+ continue;
12005
+ const otherOverride = oa.auth?.override;
12006
+ if (otherOverride === acct) {
12007
+ ctx.addIssue({
12008
+ code: exports_external.ZodIssueCode.custom,
12009
+ message: `account '${acct}' is exclusive to agent '${name}' ` + `(agents.${name}.auth.exclusive) — agent '${other}' cannot pin it`,
12010
+ path: ["agents", other, "auth", "override"]
12011
+ });
12012
+ }
12013
+ }
12014
+ (cfg.auth?.consumers ?? []).forEach((c, i) => {
12015
+ if (c.account === acct) {
12016
+ ctx.addIssue({
12017
+ code: exports_external.ZodIssueCode.custom,
12018
+ message: `account '${acct}' is exclusive to agent '${name}' ` + `(agents.${name}.auth.exclusive) — consumer '${c.name}' cannot pin it`,
12019
+ path: ["auth", "consumers", i, "account"]
12020
+ });
12021
+ }
12022
+ });
12023
+ }
11961
12024
  const microsoftAccounts = cfg.microsoft_accounts;
11962
12025
  for (const [name, a] of Object.entries(cfg.agents ?? {})) {
11963
12026
  const mw = a.microsoft_workspace;
@@ -19820,6 +19883,23 @@ function extractApiErrorMessage(bodyText) {
19820
19883
  } catch {}
19821
19884
  return bodyText.trim().slice(0, 500);
19822
19885
  }
19886
+ function extractApiErrorCode(bodyText) {
19887
+ if (!bodyText || bodyText.trim().length === 0)
19888
+ return null;
19889
+ try {
19890
+ const parsed = JSON.parse(bodyText);
19891
+ const code = parsed?.error?.details?.error_code;
19892
+ if (typeof code === "string" && code.trim().length > 0)
19893
+ return code.trim();
19894
+ } catch {}
19895
+ return null;
19896
+ }
19897
+ var ENTITLEMENT_BLOCKED_ERROR_CODES = new Set([
19898
+ "oauth_not_allowed_for_organization"
19899
+ ]);
19900
+ function isEntitlementBlockedErrorCode(code) {
19901
+ return code != null && ENTITLEMENT_BLOCKED_ERROR_CODES.has(code);
19902
+ }
19823
19903
  function isEntitlementDisabledMessage(message) {
19824
19904
  if (!message)
19825
19905
  return false;
@@ -19877,7 +19957,8 @@ async function fetchQuota(opts) {
19877
19957
  } catch {}
19878
19958
  clearTimeout(timeout);
19879
19959
  const apiErrorMessage = extractApiErrorMessage(bodyText);
19880
- const entitlement = resp.status === 403 && isEntitlementDisabledMessage(apiErrorMessage);
19960
+ const apiErrorCode = extractApiErrorCode(bodyText);
19961
+ const entitlement = resp.status === 403 && (isEntitlementBlockedErrorCode(apiErrorCode) || isEntitlementDisabledMessage(apiErrorMessage));
19881
19962
  return {
19882
19963
  ok: false,
19883
19964
  reason: `HTTP ${resp.status} from Anthropic (${parsed.reason})`,
@@ -22165,11 +22246,30 @@ class AuthBroker {
22165
22246
  return override;
22166
22247
  return auth.active ?? null;
22167
22248
  }
22249
+ exclusiveOwnerOf(account) {
22250
+ for (const [name, agent] of Object.entries(this.config.agents ?? {})) {
22251
+ if (agent.auth?.exclusive && agent.auth?.override === account)
22252
+ return name;
22253
+ }
22254
+ return null;
22255
+ }
22256
+ isServableBy(account, forAgent) {
22257
+ const owner = this.exclusiveOwnerOf(account);
22258
+ return owner === null || owner === forAgent;
22259
+ }
22168
22260
  servingAccount(identity) {
22169
- const account = this.callerAccount(identity);
22170
22261
  if (identity.kind === "operator")
22171
- return account;
22172
- return this.accountWithFailover(account);
22262
+ return this.callerAccount(identity);
22263
+ if (identity.kind === "agent")
22264
+ return this.servedAccountForAgent(identity.name);
22265
+ return this.accountWithFailover(this.callerAccount(identity));
22266
+ }
22267
+ servedAccountForAgent(name) {
22268
+ const agent = (this.config.agents ?? {})[name];
22269
+ const override = agent?.auth?.override;
22270
+ if (override && agent?.auth?.strict)
22271
+ return override;
22272
+ return this.accountWithFailover(override ?? this.config.auth?.active ?? null, name);
22173
22273
  }
22174
22274
  isOverageAllowed(account) {
22175
22275
  return (this.config.auth?.allow_overage_accounts ?? []).includes(account);
@@ -22287,6 +22387,8 @@ class AuthBroker {
22287
22387
  continue;
22288
22388
  if (this.isAccountPremiumWalled(cand))
22289
22389
  continue;
22390
+ if (this.exclusiveOwnerOf(cand))
22391
+ continue;
22290
22392
  if (!accountExists(cand, this.home))
22291
22393
  continue;
22292
22394
  if (!readAccountCredentials(cand, this.home))
@@ -22432,15 +22534,30 @@ class AuthBroker {
22432
22534
  if (reselected && this.accountEligibilityOf(reselected) === "eligible")
22433
22535
  return reselected;
22434
22536
  for (const cand of ring) {
22435
- if (cand && cand !== current && accountExists(cand, this.home) && this.accountEligibilityOf(cand) === "unknown") {
22537
+ if (cand && cand !== current && accountExists(cand, this.home) && !this.exclusiveOwnerOf(cand) && this.accountEligibilityOf(cand) === "unknown") {
22436
22538
  return cand;
22437
22539
  }
22438
22540
  }
22439
22541
  return null;
22440
22542
  }
22441
- accountWithFailover(account) {
22543
+ accountWithFailover(account, forAgent) {
22442
22544
  if (!account)
22443
22545
  return null;
22546
+ if (!this.isServableBy(account, forAgent)) {
22547
+ for (const cand of this.config.auth?.fallback_order ?? []) {
22548
+ if (cand === account || this.isAccountExhausted(cand))
22549
+ continue;
22550
+ if (!this.isServableBy(cand, forAgent))
22551
+ continue;
22552
+ if (readAccountCredentials(cand, this.home))
22553
+ return cand;
22554
+ }
22555
+ const active2 = this.config.auth?.active;
22556
+ if (active2 && active2 !== account && !this.isAccountExhausted(active2) && this.isServableBy(active2, forAgent) && readAccountCredentials(active2, this.home)) {
22557
+ return active2;
22558
+ }
22559
+ return null;
22560
+ }
22444
22561
  const exhausted = this.isAccountExhausted(account);
22445
22562
  if (!exhausted && !this.isAccountSoftAvoided(account))
22446
22563
  return account;
@@ -22448,6 +22565,8 @@ class AuthBroker {
22448
22565
  for (const cand of this.config.auth?.fallback_order ?? []) {
22449
22566
  if (cand === account || this.isAccountExhausted(cand))
22450
22567
  continue;
22568
+ if (!this.isServableBy(cand, forAgent))
22569
+ continue;
22451
22570
  if (this.isAccountSoftAvoided(cand)) {
22452
22571
  softAvoidExcludedAny = true;
22453
22572
  continue;
@@ -22461,6 +22580,8 @@ class AuthBroker {
22461
22580
  for (const cand of this.config.auth?.fallback_order ?? []) {
22462
22581
  if (cand === account || this.isAccountExhausted(cand))
22463
22582
  continue;
22583
+ if (!this.isServableBy(cand, forAgent))
22584
+ continue;
22464
22585
  if (!readAccountCredentials(cand, this.home))
22465
22586
  continue;
22466
22587
  const score = this.softAvoidUtilScore(cand);
@@ -22475,12 +22596,14 @@ class AuthBroker {
22475
22596
  for (const cand of this.config.auth?.fallback_order ?? []) {
22476
22597
  if (cand === account || this.isAccountExhausted(cand))
22477
22598
  continue;
22599
+ if (!this.isServableBy(cand, forAgent))
22600
+ continue;
22478
22601
  if (readAccountCredentials(cand, this.home))
22479
22602
  return cand;
22480
22603
  }
22481
22604
  }
22482
22605
  const active = this.config.auth?.active;
22483
- if (active && active !== account && !this.isAccountExhausted(active) && readAccountCredentials(active, this.home)) {
22606
+ if (active && active !== account && !this.isAccountExhausted(active) && this.isServableBy(active, forAgent) && readAccountCredentials(active, this.home)) {
22484
22607
  return active;
22485
22608
  }
22486
22609
  return account;
@@ -22532,7 +22655,9 @@ class AuthBroker {
22532
22655
  const agents = Object.entries(this.config.agents ?? {}).map(([name, agent]) => {
22533
22656
  const override = agent.auth?.override ?? null;
22534
22657
  const account = override ?? auth.active ?? "";
22535
- return { name, account, override };
22658
+ const strict = Boolean(override && agent.auth?.strict);
22659
+ const exclusive = Boolean(override && agent.auth?.exclusive);
22660
+ return { name, account, override, strict, exclusive };
22536
22661
  });
22537
22662
  const consumers = (auth.consumers ?? []).map((c) => ({
22538
22663
  name: c.name,
@@ -22859,6 +22984,12 @@ class AuthBroker {
22859
22984
  socket.write(encodeError(id, "ACCOUNT_NOT_FOUND", `account '${account}' not found`));
22860
22985
  return;
22861
22986
  }
22987
+ const exclusiveOwner = this.exclusiveOwnerOf(account);
22988
+ if (exclusiveOwner) {
22989
+ this.audit({ op: "set-active", identity, account, accountKind: "claude", ok: false, error: "exclusive-account" });
22990
+ socket.write(encodeError(id, "FORBIDDEN", `account '${account}' is exclusive to agent '${exclusiveOwner}' ` + `(agents.${exclusiveOwner}.auth.exclusive) — it cannot be the fleet active. ` + `Clear the exclusive flag in switchroom.yaml first if you really mean this.`));
22991
+ return;
22992
+ }
22862
22993
  const cfg = {
22863
22994
  ...this.config,
22864
22995
  auth: { ...this.config.auth ?? {}, active: account }
@@ -22870,6 +23001,15 @@ class AuthBroker {
22870
23001
  this.audit({ op: "set-active", identity, account, accountKind: "claude", ok: true });
22871
23002
  socket.write(encodeSuccess(id, { active: account, fanned }));
22872
23003
  }
23004
+ callerStrictPinned(identity) {
23005
+ if (identity.kind !== "agent")
23006
+ return false;
23007
+ const a = (this.config.agents ?? {})[identity.name]?.auth;
23008
+ return Boolean(a?.override && a?.strict);
23009
+ }
23010
+ callerRolledTo(identity, rolledTo) {
23011
+ return this.callerStrictPinned(identity) ? null : rolledTo;
23012
+ }
22873
23013
  async opMarkExhausted(socket, id, identity, until) {
22874
23014
  const account = this.callerAccount(identity);
22875
23015
  if (!account) {
@@ -22879,7 +23019,12 @@ class AuthBroker {
22879
23019
  }
22880
23020
  const { rolled, rolledTo } = await this.markExhaustedAndRoll(account, until, identity);
22881
23021
  this.audit({ op: "mark-exhausted", identity, account, accountKind: "claude", ok: true });
22882
- socket.write(encodeSuccess(id, { account, rolled, rolledTo }));
23022
+ socket.write(encodeSuccess(id, {
23023
+ account,
23024
+ rolled,
23025
+ rolledTo: this.callerRolledTo(identity, rolledTo),
23026
+ caller_pinned_strict: this.callerStrictPinned(identity)
23027
+ }));
22883
23028
  }
22884
23029
  async opMarkThrottled(socket, id, identity, until, probeOnly = false) {
22885
23030
  const account = this.callerAccount(identity);
@@ -22896,7 +23041,8 @@ class AuthBroker {
22896
23041
  account,
22897
23042
  throttled_until: this.quota[account]?.throttled_until ?? 0,
22898
23043
  escalated: escalated2,
22899
- rolledTo: escalated2 ? rolledTo2 : null
23044
+ rolledTo: escalated2 ? this.callerRolledTo(identity, rolledTo2) : null,
23045
+ caller_pinned_strict: this.callerStrictPinned(identity)
22900
23046
  }));
22901
23047
  return;
22902
23048
  }
@@ -22939,7 +23085,8 @@ class AuthBroker {
22939
23085
  account,
22940
23086
  throttled_until: throttledUntil,
22941
23087
  escalated,
22942
- rolledTo: escalated ? rolledTo : null
23088
+ rolledTo: escalated ? this.callerRolledTo(identity, rolledTo) : null,
23089
+ caller_pinned_strict: this.callerStrictPinned(identity)
22943
23090
  }));
22944
23091
  }
22945
23092
  async maybeEscalateThrottle(account, identity, now) {
@@ -23511,6 +23658,22 @@ class AuthBroker {
23511
23658
  socket.write(encodeError(id, "ACCOUNT_NOT_FOUND", `account '${account}' not found`));
23512
23659
  return;
23513
23660
  }
23661
+ if (account !== null) {
23662
+ const exclusiveOwner = this.exclusiveOwnerOf(account);
23663
+ if (exclusiveOwner && exclusiveOwner !== agentName) {
23664
+ this.audit({ op: "set-override", identity, account, accountKind: "claude", ok: false, error: "exclusive-account" });
23665
+ socket.write(encodeError(id, "FORBIDDEN", `account '${account}' is exclusive to agent '${exclusiveOwner}' ` + `(agents.${exclusiveOwner}.auth.exclusive) — agent '${agentName}' cannot pin it.`));
23666
+ return;
23667
+ }
23668
+ }
23669
+ {
23670
+ const curAuth = (this.config.agents ?? {})[agentName]?.auth;
23671
+ if ((curAuth?.strict || curAuth?.exclusive) && account !== (curAuth?.override ?? null)) {
23672
+ this.audit({ op: "set-override", identity, account: account ?? undefined, accountKind: "claude", ok: false, error: "yaml-pinned" });
23673
+ socket.write(encodeError(id, "FORBIDDEN", `agent '${agentName}' has a strict/exclusive pin declared in switchroom.yaml — ` + `edit the yaml (agents.${agentName}.auth) and restart the broker to change it.`));
23674
+ return;
23675
+ }
23676
+ }
23514
23677
  const agents = { ...this.config.agents ?? {} };
23515
23678
  const cur = agents[agentName];
23516
23679
  const auth = { ...cur.auth ?? {} };
@@ -23686,10 +23849,9 @@ class AuthBroker {
23686
23849
  return out;
23687
23850
  }
23688
23851
  fanoutToAffectedAgents(label) {
23689
- const auth = this.config.auth ?? {};
23690
23852
  const fanned = [];
23691
- for (const [name, agent] of Object.entries(this.config.agents ?? {})) {
23692
- const effective = this.accountWithFailover(agent.auth?.override ?? auth.active);
23853
+ for (const name of Object.keys(this.config.agents ?? {})) {
23854
+ const effective = this.servedAccountForAgent(name);
23693
23855
  if (effective === label) {
23694
23856
  if (this.fanoutForAgent(name))
23695
23857
  fanned.push(name);
@@ -23708,10 +23870,9 @@ class AuthBroker {
23708
23870
  const isEffective = effective === label;
23709
23871
  if (!isPinned && !isEffective)
23710
23872
  continue;
23711
- const toMirror = effective ?? bound;
23712
- if (toMirror == null)
23873
+ if (effective == null)
23713
23874
  continue;
23714
- if (this.mirrorAccountToConsumer(toMirror, consumer)) {
23875
+ if (this.mirrorAccountToConsumer(effective, consumer)) {
23715
23876
  fanned.push(consumer.name);
23716
23877
  }
23717
23878
  }
@@ -23812,6 +23973,10 @@ class AuthBroker {
23812
23973
  const effective = agent.auth?.override ?? auth.active;
23813
23974
  if (effective !== label)
23814
23975
  continue;
23976
+ if (agent.auth?.override && agent.auth?.strict)
23977
+ continue;
23978
+ if (!this.isServableBy(next, name))
23979
+ continue;
23815
23980
  if (this.mirrorAccountToAgent(next, name)) {
23816
23981
  rolled.push(name);
23817
23982
  }
@@ -23820,8 +23985,13 @@ class AuthBroker {
23820
23985
  }
23821
23986
  nextHealthyAccount(current, order) {
23822
23987
  const start = order.indexOf(current);
23823
- if (start === -1)
23824
- return order[0] ?? null;
23988
+ if (start === -1) {
23989
+ for (const cand of order) {
23990
+ if (cand && !this.exclusiveOwnerOf(cand))
23991
+ return cand;
23992
+ }
23993
+ return null;
23994
+ }
23825
23995
  let bestSoftAvoided = null;
23826
23996
  let bestScore = Number.POSITIVE_INFINITY;
23827
23997
  for (let i = 1;i <= order.length; i++) {
@@ -23830,6 +24000,8 @@ class AuthBroker {
23830
24000
  continue;
23831
24001
  if (this.isAccountExhausted(cand) || !accountExists(cand, this.home))
23832
24002
  continue;
24003
+ if (this.exclusiveOwnerOf(cand))
24004
+ continue;
23833
24005
  if (!this.isAccountSoftAvoided(cand))
23834
24006
  return cand;
23835
24007
  const score = this.softAvoidUtilScore(cand);
@@ -23841,11 +24013,10 @@ class AuthBroker {
23841
24013
  return bestSoftAvoided;
23842
24014
  }
23843
24015
  fanoutForAgent(name) {
23844
- const auth = this.config.auth ?? {};
23845
24016
  const agent = (this.config.agents ?? {})[name];
23846
24017
  if (!agent)
23847
24018
  return false;
23848
- const effective = this.accountWithFailover(agent.auth?.override ?? auth.active);
24019
+ const effective = this.servedAccountForAgent(name);
23849
24020
  if (!effective)
23850
24021
  return false;
23851
24022
  return this.mirrorAccountToAgent(effective, name);
@@ -23974,6 +24145,15 @@ class AuthBroker {
23974
24145
  `);
23975
24146
  return;
23976
24147
  }
24148
+ const exclusiveOwner = this.exclusiveOwnerOf(ov.active);
24149
+ if (exclusiveOwner) {
24150
+ process.stdout.write(`auth-broker: active-override dropped — account '${ov.active}' is now exclusive ` + `to agent '${exclusiveOwner}' (agents.${exclusiveOwner}.auth.exclusive); yaml wins
24151
+ `);
24152
+ try {
24153
+ unlinkSync(join7(this.stateDir, "active-override.json"));
24154
+ } catch {}
24155
+ return;
24156
+ }
23977
24157
  if (ov.active === this.config.auth?.active)
23978
24158
  return;
23979
24159
  this.config = {
@@ -12427,8 +12427,10 @@ var AgentSchema = exports_external.object({
12427
12427
  bot_username: exports_external.string().optional().describe("Per-agent Telegram bot username (without leading @) when it doesn't " + "contain the agent slug. Replaces the default 'username includes slug' " + "preflight check with an exact (case-insensitive) match. Use when an " + "agent and its bot have intentionally divergent names (e.g. agent " + "'lawgpt' paired with bot '@meken_law_bot')."),
12428
12428
  timezone: exports_external.string().regex(TIMEZONE_REGEX, "timezone must be an IANA zone name like 'Australia/Melbourne' or 'UTC' " + "(three-letter aliases like EST/PST and bare offsets like UTC+10 are not accepted)").optional().describe("Per-agent IANA timezone override. Wins over any profile/defaults " + "value and over the top-level switchroom.timezone global. Controls " + "the UserPromptSubmit timezone hook's emitted local time and the " + "agent container's `environment.TZ` in compose."),
12429
12429
  auth: exports_external.object({
12430
- override: exports_external.string().min(1).optional().describe("Per-agent override of the fleet-wide `auth.active`. Edge-case use only \u2014 " + "this agent talks to the named account regardless of fleet active. See RFC H \u00a74.5.")
12431
- }).optional().describe("Account routing for switchroom-auth-broker. RFC H schema uses " + "fleet-wide `auth.active` plus per-agent `override:` for edge cases. " + "Pre-RFC-H `auth.accounts: [..]` and `auth_label:` are migrated in-place " + "on first apply (see src/auth/migrate-schema.ts)."),
12430
+ override: exports_external.string().min(1).optional().describe("Per-agent override of the fleet-wide `auth.active`. Edge-case use only \u2014 " + "this agent talks to the named account regardless of fleet active. See RFC H \u00a74.5."),
12431
+ strict: exports_external.boolean().optional().describe("Requires `override`. When true, the pin is a hard binding, not a " + "routing preference: the broker NEVER serves this agent from any " + "other account \u2014 no failover to `auth.fallback_order` while the " + "pinned account is quota-walled or exhausted. The agent rides out " + "the wall on its own account (surfacing the normal 429/quota " + "cards) instead of silently borrowing fleet quota. Use for " + "accounts that must never cross a billing/compliance boundary " + "(e.g. an employer-provided subscription)."),
12432
+ exclusive: exports_external.boolean().optional().describe("Requires `override`. When true, the pinned account belongs to " + "THIS agent alone: the broker refuses to serve it to any other " + "agent or consumer, refuses `auth use <label>` (set-active) onto " + "it, and refuses pinning another agent to it. Config that routes " + "others to the account (`auth.active`, `auth.fallback_order`, " + "another agent's `override`, a consumer pin) is rejected at load. " + "Usually paired with `strict: true` for full two-way isolation.")
12433
+ }).optional().describe("Account routing for switchroom-auth-broker. RFC H schema uses " + "fleet-wide `auth.active` plus per-agent `override:` for edge cases, " + "with optional `strict:` (never borrow another account) and " + "`exclusive:` (no one else may use the pinned account) hardening. " + "Pre-RFC-H `auth.accounts: [..]` and `auth_label:` are migrated in-place " + "on first apply (see src/auth/migrate-schema.ts)."),
12432
12434
  dm_only: exports_external.boolean().optional().describe("Mark this agent as a DM-only bot \u2014 has its own bot_token and lives " + "exclusively in a private chat with the operator. Suppresses " + "scaffolding's default behavior of inheriting the global " + "telegram.forum_chat_id into the agent's access.json `groups` entry " + "(the forum chat the bot isn't a member of, which would otherwise " + "trigger a 'boot-probe-failed: 400 chat not found' warning every " + "restart). topic_name is still schema-required but unused \u2014 set it " + "to a display label like 'DM' for /switchroom status output."),
12433
12435
  topic_name: exports_external.string().describe("Telegram forum topic display name"),
12434
12436
  topic_emoji: exports_external.string().optional().describe("Emoji for the topic (e.g., '\uD83C\uDFCB\ufe0f')"),
@@ -12503,6 +12505,17 @@ var AgentSchema = exports_external.object({
12503
12505
  tmp_size: exports_external.string().regex(/^\d+(\.\d+)?[kmgKMG]?$/, "tmp_size must be a Docker size string like '1g', '4g', '512m'").refine((v) => parseFloat(v) > 0, "tmp_size must be greater than zero").optional()
12504
12506
  }).optional()
12505
12507
  }).superRefine((agent, ctx) => {
12508
+ if (agent.auth && !agent.auth.override) {
12509
+ for (const flag of ["strict", "exclusive"]) {
12510
+ if (agent.auth[flag]) {
12511
+ ctx.addIssue({
12512
+ code: exports_external.ZodIssueCode.custom,
12513
+ message: `\`auth.${flag}: true\` requires \`auth.override: <account>\` \u2014 there is no pinned account for it to apply to.`,
12514
+ path: ["auth", flag]
12515
+ });
12516
+ }
12517
+ }
12518
+ }
12506
12519
  if (agent.dm_only !== true)
12507
12520
  return;
12508
12521
  const tg = agent.channels?.telegram;
@@ -12615,6 +12628,13 @@ var UserSchema = exports_external.object({
12615
12628
  profile_bank: exports_external.string().describe("Hindsight bank holding this user's memory profile (author via " + "`switchroom memory profile add <bank> ...`)."),
12616
12629
  person_id: exports_external.string().optional().describe("Free-text display name projected into the inbound `<channel>` " + 'tag\'s `user` attribute (e.g. "Lisa") so an agent can greet by ' + "name instead of only seeing the raw Telegram id/username. NOT a " + "stable identity system \u2014 just a label. Resolution is boot-time-only " + "(no hot-reload; a config change needs an agent restart) and " + "chat-scoped (a resolved name is only shown in a chat/group the " + "person is actually a member of, per that chat's access.json " + "membership \u2014 never broadcast into every chat the agent operates " + "in). Keep this broadly safe to show, the same discipline as " + "picking a `profile_bank` name: there is no automated enforcement " + "that a `person_id` stays safe if a group's membership changes " + "later \u2014 see docs/configuration.md.")
12617
12630
  });
12631
+ var ConfigRepoConfigSchema = exports_external.object({
12632
+ enabled: exports_external.boolean().default(false).describe("Master switch for config-repo change control. Default false (opt-in). " + "When false the `config-repo sync` verb still runs on demand but the " + "doctor check and (later) the scheduled tick treat the feature as off."),
12633
+ path: exports_external.string().regex(/^[a-zA-Z0-9~._\-/]+$/, "config_repo.path must not contain shell-special characters ($, `, \", ', \\, etc.)").default("~/.switchroom-config").describe("Filesystem path to the operator's private config repo. Must be a git " + "repo (else `switchroom doctor` FAILs). Tilde-expanded at read time."),
12634
+ push: exports_external.boolean().default(true).describe("When true, `config-repo sync` pushes after committing (subject to the " + "`require_private` gate). When false, it commits locally only \u2014 useful " + "for offline hosts or a review-before-push workflow."),
12635
+ remote: exports_external.string().min(1).default("origin").describe("Git remote name to push to. Default `origin`."),
12636
+ require_private: exports_external.boolean().default(true).describe("Refuse to PUSH unless the GitHub API confirms the remote repo is " + "private. On a public remote \u2014 or when the API is unreachable \u2014 the " + "push is skipped (commits still land locally) and a WARN is emitted. " + "Fail-safe against exfiltrating memory files / workspace state to a " + "repo that has been flipped public. Default true.")
12637
+ });
12618
12638
  var SwitchroomConfigSchema = exports_external.object({
12619
12639
  switchroom: exports_external.object({
12620
12640
  version: exports_external.literal(1).describe("Config schema version"),
@@ -12627,6 +12647,7 @@ var SwitchroomConfigSchema = exports_external.object({
12627
12647
  memory: MemoryBackendConfigSchema.optional(),
12628
12648
  hindsight: HindsightConfigSchema.optional().describe("Fleet-singleton Hindsight (memory backend) configuration. Currently " + "just the LLM knob (provider + model) used for retain/reflect/" + "consolidation. Both fields optional; when unset the container falls " + "back to provider=claude-code + the hard-coded HINDSIGHT_DEFAULT_MODEL " + "so nothing changes for operators who don't set it. Read at container " + "launch by startHindsight() (src/setup/hindsight.ts) \u2014 takes effect on " + "the next `switchroom apply` / `memory setup --recreate`."),
12629
12649
  vault: VaultConfigSchema.optional(),
12650
+ config_repo: ConfigRepoConfigSchema.optional().describe("Change control for the operator's private ~/.switchroom-config git repo " + "(backup + audit trail of live host config, agent workspace state, and " + "mirrored personal skills). Consumed by `switchroom config-repo sync` and " + "the `config repo` doctor check. Optional and OFF by default."),
12630
12651
  auth: exports_external.object({
12631
12652
  active: exports_external.string().min(1).optional().describe("Fleet-wide active Anthropic account label. Every agent without " + "an explicit `agent.auth.override` uses this account. See " + "docs/auth.md for the full model. Set by `switchroom auth use <label>`."),
12632
12653
  fallback_order: exports_external.array(exports_external.string().min(1)).optional().describe("Ordered list of account labels for `switchroom auth rotate` to cycle " + "through when the active account hits a quota event. First entry is " + "normally the same as `auth.active`. When unset, `rotate` is a no-op."),
@@ -12694,6 +12715,48 @@ var SwitchroomConfigSchema = exports_external.object({
12694
12715
  for (const [name, a] of Object.entries(cfg.agents ?? {})) {
12695
12716
  checkServes(a.serves, ["agents", name, "serves"]);
12696
12717
  }
12718
+ for (const [name, a] of Object.entries(cfg.agents ?? {})) {
12719
+ const agentAuth = a.auth;
12720
+ if (!agentAuth?.exclusive || !agentAuth.override)
12721
+ continue;
12722
+ const acct = agentAuth.override;
12723
+ if (cfg.auth?.active === acct) {
12724
+ ctx.addIssue({
12725
+ code: exports_external.ZodIssueCode.custom,
12726
+ message: `account '${acct}' is exclusive to agent '${name}' ` + `(agents.${name}.auth.exclusive) \u2014 it cannot be the fleet \`auth.active\``,
12727
+ path: ["auth", "active"]
12728
+ });
12729
+ }
12730
+ const fbIdx = (cfg.auth?.fallback_order ?? []).indexOf(acct);
12731
+ if (fbIdx !== -1) {
12732
+ ctx.addIssue({
12733
+ code: exports_external.ZodIssueCode.custom,
12734
+ message: `account '${acct}' is exclusive to agent '${name}' ` + `(agents.${name}.auth.exclusive) \u2014 it cannot appear in \`auth.fallback_order\``,
12735
+ path: ["auth", "fallback_order", fbIdx]
12736
+ });
12737
+ }
12738
+ for (const [other, oa] of Object.entries(cfg.agents ?? {})) {
12739
+ if (other === name)
12740
+ continue;
12741
+ const otherOverride = oa.auth?.override;
12742
+ if (otherOverride === acct) {
12743
+ ctx.addIssue({
12744
+ code: exports_external.ZodIssueCode.custom,
12745
+ message: `account '${acct}' is exclusive to agent '${name}' ` + `(agents.${name}.auth.exclusive) \u2014 agent '${other}' cannot pin it`,
12746
+ path: ["agents", other, "auth", "override"]
12747
+ });
12748
+ }
12749
+ }
12750
+ (cfg.auth?.consumers ?? []).forEach((c, i) => {
12751
+ if (c.account === acct) {
12752
+ ctx.addIssue({
12753
+ code: exports_external.ZodIssueCode.custom,
12754
+ message: `account '${acct}' is exclusive to agent '${name}' ` + `(agents.${name}.auth.exclusive) \u2014 consumer '${c.name}' cannot pin it`,
12755
+ path: ["auth", "consumers", i, "account"]
12756
+ });
12757
+ }
12758
+ });
12759
+ }
12697
12760
  const microsoftAccounts = cfg.microsoft_accounts;
12698
12761
  for (const [name, a] of Object.entries(cfg.agents ?? {})) {
12699
12762
  const mw = a.microsoft_workspace;