switchroom 0.20.10 → 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 (52) hide show
  1. package/dist/agent-scheduler/index.js +65 -2
  2. package/dist/auth-broker/index.js +185 -23
  3. package/dist/cli/notion-write-pretool.mjs +65 -2
  4. package/dist/cli/self-improve-apply-guard-pretool.mjs +357 -92
  5. package/dist/cli/self-improve-stop.mjs +889 -7
  6. package/dist/cli/skill-validate-pretool.mjs +82 -3
  7. package/dist/cli/switchroom.js +3669 -2107
  8. package/dist/host-control/main.js +67 -4
  9. package/dist/vault/approvals/kernel-server.js +66 -3
  10. package/dist/vault/broker/server.js +66 -3
  11. package/package.json +1 -1
  12. package/profiles/_shared/agent-self-service.md.hbs +15 -22
  13. package/profiles/_shared/delegation-golden-rule.md.hbs +1 -1
  14. package/profiles/_shared/dev-protocol.md.hbs +1 -1
  15. package/profiles/_shared/execution-discipline.md.hbs +4 -4
  16. package/profiles/_shared/vault-protocol.md.hbs +2 -18
  17. package/profiles/default/CLAUDE.md.hbs +3 -5
  18. package/telegram-plugin/auto-fallback-fleet.ts +37 -2
  19. package/telegram-plugin/dist/gateway/gateway.js +1405 -918
  20. package/telegram-plugin/fallback-card-collapse.ts +1 -0
  21. package/telegram-plugin/gateway/auth-command.ts +11 -1
  22. package/telegram-plugin/gateway/callback-query-handlers.ts +100 -0
  23. package/telegram-plugin/gateway/eval-case-proposal-card.ts +86 -0
  24. package/telegram-plugin/gateway/fleet-fallback-notice-cooldown.test.ts +74 -0
  25. package/telegram-plugin/gateway/fleet-fallback-notice-cooldown.ts +71 -0
  26. package/telegram-plugin/gateway/gateway.ts +85 -90
  27. package/telegram-plugin/gateway/ipc-protocol.ts +43 -0
  28. package/telegram-plugin/gateway/ipc-server.ts +28 -0
  29. package/telegram-plugin/gateway/narrative-lane.ts +33 -2
  30. package/telegram-plugin/gateway/privacy-reset.test.ts +216 -0
  31. package/telegram-plugin/gateway/privacy-reset.ts +87 -0
  32. package/telegram-plugin/gateway/privacy-state.test.ts +165 -0
  33. package/telegram-plugin/gateway/privacy-state.ts +206 -0
  34. package/telegram-plugin/gateway/self-improve-proposal-wiring.ts +176 -0
  35. package/telegram-plugin/gateway/stale-pin-sweep-wiring.ts +24 -14
  36. package/telegram-plugin/gateway/stale-pin-sweep.test.ts +123 -26
  37. package/telegram-plugin/gateway/stale-pin-sweep.ts +48 -32
  38. package/telegram-plugin/gateway/throttle-tier-wiring.ts +15 -4
  39. package/telegram-plugin/slot-banner-driver.ts +42 -5
  40. package/telegram-plugin/tests/auto-fallback-fleet.test.ts +24 -0
  41. package/telegram-plugin/tests/gateway-handler-registration-wiring.test.ts +2 -0
  42. package/telegram-plugin/tests/narrative-lane-golden.test.ts +97 -0
  43. package/telegram-plugin/tests/privacy-reset-call-sites.test.ts +120 -0
  44. package/telegram-plugin/tests/status-pin-store.test.ts +25 -0
  45. package/telegram-plugin/tests/throttle-tier.test.ts +16 -0
  46. package/telegram-plugin/tests/turn-flush-safety.test.ts +67 -0
  47. package/telegram-plugin/throttle-tier.ts +12 -3
  48. package/telegram-plugin/turn-flush-safety.ts +97 -0
  49. package/vendor/hindsight-memory/scripts/retain.py +306 -0
  50. package/vendor/hindsight-memory/scripts/subagent_retain.py +29 -1
  51. package/vendor/hindsight-memory/scripts/tests/test_private_mode.py +415 -0
  52. package/vendor/hindsight-memory/scripts/tests/test_self_improve_correction_tag.py +167 -0
@@ -11666,8 +11666,10 @@ var AgentSchema = exports_external.object({
11666
11666
  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')."),
11667
11667
  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."),
11668
11668
  auth: exports_external.object({
11669
- 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.")
11670
- }).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)."),
11669
+ 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."),
11670
+ 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)."),
11671
+ 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.")
11672
+ }).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)."),
11671
11673
  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."),
11672
11674
  topic_name: exports_external.string().describe("Telegram forum topic display name"),
11673
11675
  topic_emoji: exports_external.string().optional().describe("Emoji for the topic (e.g., '\uD83C\uDFCB️')"),
@@ -11742,6 +11744,17 @@ var AgentSchema = exports_external.object({
11742
11744
  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()
11743
11745
  }).optional()
11744
11746
  }).superRefine((agent, ctx) => {
11747
+ if (agent.auth && !agent.auth.override) {
11748
+ for (const flag of ["strict", "exclusive"]) {
11749
+ if (agent.auth[flag]) {
11750
+ ctx.addIssue({
11751
+ code: exports_external.ZodIssueCode.custom,
11752
+ message: `\`auth.${flag}: true\` requires \`auth.override: <account>\` — there is no pinned account for it to apply to.`,
11753
+ path: ["auth", flag]
11754
+ });
11755
+ }
11756
+ }
11757
+ }
11745
11758
  if (agent.dm_only !== true)
11746
11759
  return;
11747
11760
  const tg = agent.channels?.telegram;
@@ -11854,6 +11867,13 @@ var UserSchema = exports_external.object({
11854
11867
  profile_bank: exports_external.string().describe("Hindsight bank holding this user's memory profile (author via " + "`switchroom memory profile add <bank> ...`)."),
11855
11868
  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.")
11856
11869
  });
11870
+ var ConfigRepoConfigSchema = exports_external.object({
11871
+ 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."),
11872
+ 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."),
11873
+ 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."),
11874
+ remote: exports_external.string().min(1).default("origin").describe("Git remote name to push to. Default `origin`."),
11875
+ 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.")
11876
+ });
11857
11877
  var SwitchroomConfigSchema = exports_external.object({
11858
11878
  switchroom: exports_external.object({
11859
11879
  version: exports_external.literal(1).describe("Config schema version"),
@@ -11866,6 +11886,7 @@ var SwitchroomConfigSchema = exports_external.object({
11866
11886
  memory: MemoryBackendConfigSchema.optional(),
11867
11887
  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`."),
11868
11888
  vault: VaultConfigSchema.optional(),
11889
+ 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."),
11869
11890
  auth: exports_external.object({
11870
11891
  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>`."),
11871
11892
  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."),
@@ -11933,6 +11954,48 @@ var SwitchroomConfigSchema = exports_external.object({
11933
11954
  for (const [name, a] of Object.entries(cfg.agents ?? {})) {
11934
11955
  checkServes(a.serves, ["agents", name, "serves"]);
11935
11956
  }
11957
+ for (const [name, a] of Object.entries(cfg.agents ?? {})) {
11958
+ const agentAuth = a.auth;
11959
+ if (!agentAuth?.exclusive || !agentAuth.override)
11960
+ continue;
11961
+ const acct = agentAuth.override;
11962
+ if (cfg.auth?.active === acct) {
11963
+ ctx.addIssue({
11964
+ code: exports_external.ZodIssueCode.custom,
11965
+ message: `account '${acct}' is exclusive to agent '${name}' ` + `(agents.${name}.auth.exclusive) — it cannot be the fleet \`auth.active\``,
11966
+ path: ["auth", "active"]
11967
+ });
11968
+ }
11969
+ const fbIdx = (cfg.auth?.fallback_order ?? []).indexOf(acct);
11970
+ if (fbIdx !== -1) {
11971
+ ctx.addIssue({
11972
+ code: exports_external.ZodIssueCode.custom,
11973
+ message: `account '${acct}' is exclusive to agent '${name}' ` + `(agents.${name}.auth.exclusive) — it cannot appear in \`auth.fallback_order\``,
11974
+ path: ["auth", "fallback_order", fbIdx]
11975
+ });
11976
+ }
11977
+ for (const [other, oa] of Object.entries(cfg.agents ?? {})) {
11978
+ if (other === name)
11979
+ continue;
11980
+ const otherOverride = oa.auth?.override;
11981
+ if (otherOverride === acct) {
11982
+ ctx.addIssue({
11983
+ code: exports_external.ZodIssueCode.custom,
11984
+ message: `account '${acct}' is exclusive to agent '${name}' ` + `(agents.${name}.auth.exclusive) — agent '${other}' cannot pin it`,
11985
+ path: ["agents", other, "auth", "override"]
11986
+ });
11987
+ }
11988
+ }
11989
+ (cfg.auth?.consumers ?? []).forEach((c, i) => {
11990
+ if (c.account === acct) {
11991
+ ctx.addIssue({
11992
+ code: exports_external.ZodIssueCode.custom,
11993
+ message: `account '${acct}' is exclusive to agent '${name}' ` + `(agents.${name}.auth.exclusive) — consumer '${c.name}' cannot pin it`,
11994
+ path: ["auth", "consumers", i, "account"]
11995
+ });
11996
+ }
11997
+ });
11998
+ }
11936
11999
  const microsoftAccounts = cfg.microsoft_accounts;
11937
12000
  for (const [name, a] of Object.entries(cfg.agents ?? {})) {
11938
12001
  const mw = a.microsoft_workspace;
@@ -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;
@@ -22183,11 +22246,30 @@ class AuthBroker {
22183
22246
  return override;
22184
22247
  return auth.active ?? null;
22185
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
+ }
22186
22260
  servingAccount(identity) {
22187
- const account = this.callerAccount(identity);
22188
22261
  if (identity.kind === "operator")
22189
- return account;
22190
- 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);
22191
22273
  }
22192
22274
  isOverageAllowed(account) {
22193
22275
  return (this.config.auth?.allow_overage_accounts ?? []).includes(account);
@@ -22305,6 +22387,8 @@ class AuthBroker {
22305
22387
  continue;
22306
22388
  if (this.isAccountPremiumWalled(cand))
22307
22389
  continue;
22390
+ if (this.exclusiveOwnerOf(cand))
22391
+ continue;
22308
22392
  if (!accountExists(cand, this.home))
22309
22393
  continue;
22310
22394
  if (!readAccountCredentials(cand, this.home))
@@ -22450,15 +22534,30 @@ class AuthBroker {
22450
22534
  if (reselected && this.accountEligibilityOf(reselected) === "eligible")
22451
22535
  return reselected;
22452
22536
  for (const cand of ring) {
22453
- 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") {
22454
22538
  return cand;
22455
22539
  }
22456
22540
  }
22457
22541
  return null;
22458
22542
  }
22459
- accountWithFailover(account) {
22543
+ accountWithFailover(account, forAgent) {
22460
22544
  if (!account)
22461
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
+ }
22462
22561
  const exhausted = this.isAccountExhausted(account);
22463
22562
  if (!exhausted && !this.isAccountSoftAvoided(account))
22464
22563
  return account;
@@ -22466,6 +22565,8 @@ class AuthBroker {
22466
22565
  for (const cand of this.config.auth?.fallback_order ?? []) {
22467
22566
  if (cand === account || this.isAccountExhausted(cand))
22468
22567
  continue;
22568
+ if (!this.isServableBy(cand, forAgent))
22569
+ continue;
22469
22570
  if (this.isAccountSoftAvoided(cand)) {
22470
22571
  softAvoidExcludedAny = true;
22471
22572
  continue;
@@ -22479,6 +22580,8 @@ class AuthBroker {
22479
22580
  for (const cand of this.config.auth?.fallback_order ?? []) {
22480
22581
  if (cand === account || this.isAccountExhausted(cand))
22481
22582
  continue;
22583
+ if (!this.isServableBy(cand, forAgent))
22584
+ continue;
22482
22585
  if (!readAccountCredentials(cand, this.home))
22483
22586
  continue;
22484
22587
  const score = this.softAvoidUtilScore(cand);
@@ -22493,12 +22596,14 @@ class AuthBroker {
22493
22596
  for (const cand of this.config.auth?.fallback_order ?? []) {
22494
22597
  if (cand === account || this.isAccountExhausted(cand))
22495
22598
  continue;
22599
+ if (!this.isServableBy(cand, forAgent))
22600
+ continue;
22496
22601
  if (readAccountCredentials(cand, this.home))
22497
22602
  return cand;
22498
22603
  }
22499
22604
  }
22500
22605
  const active = this.config.auth?.active;
22501
- 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)) {
22502
22607
  return active;
22503
22608
  }
22504
22609
  return account;
@@ -22550,7 +22655,9 @@ class AuthBroker {
22550
22655
  const agents = Object.entries(this.config.agents ?? {}).map(([name, agent]) => {
22551
22656
  const override = agent.auth?.override ?? null;
22552
22657
  const account = override ?? auth.active ?? "";
22553
- 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 };
22554
22661
  });
22555
22662
  const consumers = (auth.consumers ?? []).map((c) => ({
22556
22663
  name: c.name,
@@ -22877,6 +22984,12 @@ class AuthBroker {
22877
22984
  socket.write(encodeError(id, "ACCOUNT_NOT_FOUND", `account '${account}' not found`));
22878
22985
  return;
22879
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
+ }
22880
22993
  const cfg = {
22881
22994
  ...this.config,
22882
22995
  auth: { ...this.config.auth ?? {}, active: account }
@@ -22888,6 +23001,15 @@ class AuthBroker {
22888
23001
  this.audit({ op: "set-active", identity, account, accountKind: "claude", ok: true });
22889
23002
  socket.write(encodeSuccess(id, { active: account, fanned }));
22890
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
+ }
22891
23013
  async opMarkExhausted(socket, id, identity, until) {
22892
23014
  const account = this.callerAccount(identity);
22893
23015
  if (!account) {
@@ -22897,7 +23019,12 @@ class AuthBroker {
22897
23019
  }
22898
23020
  const { rolled, rolledTo } = await this.markExhaustedAndRoll(account, until, identity);
22899
23021
  this.audit({ op: "mark-exhausted", identity, account, accountKind: "claude", ok: true });
22900
- 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
+ }));
22901
23028
  }
22902
23029
  async opMarkThrottled(socket, id, identity, until, probeOnly = false) {
22903
23030
  const account = this.callerAccount(identity);
@@ -22914,7 +23041,8 @@ class AuthBroker {
22914
23041
  account,
22915
23042
  throttled_until: this.quota[account]?.throttled_until ?? 0,
22916
23043
  escalated: escalated2,
22917
- rolledTo: escalated2 ? rolledTo2 : null
23044
+ rolledTo: escalated2 ? this.callerRolledTo(identity, rolledTo2) : null,
23045
+ caller_pinned_strict: this.callerStrictPinned(identity)
22918
23046
  }));
22919
23047
  return;
22920
23048
  }
@@ -22957,7 +23085,8 @@ class AuthBroker {
22957
23085
  account,
22958
23086
  throttled_until: throttledUntil,
22959
23087
  escalated,
22960
- rolledTo: escalated ? rolledTo : null
23088
+ rolledTo: escalated ? this.callerRolledTo(identity, rolledTo) : null,
23089
+ caller_pinned_strict: this.callerStrictPinned(identity)
22961
23090
  }));
22962
23091
  }
22963
23092
  async maybeEscalateThrottle(account, identity, now) {
@@ -23529,6 +23658,22 @@ class AuthBroker {
23529
23658
  socket.write(encodeError(id, "ACCOUNT_NOT_FOUND", `account '${account}' not found`));
23530
23659
  return;
23531
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
+ }
23532
23677
  const agents = { ...this.config.agents ?? {} };
23533
23678
  const cur = agents[agentName];
23534
23679
  const auth = { ...cur.auth ?? {} };
@@ -23704,10 +23849,9 @@ class AuthBroker {
23704
23849
  return out;
23705
23850
  }
23706
23851
  fanoutToAffectedAgents(label) {
23707
- const auth = this.config.auth ?? {};
23708
23852
  const fanned = [];
23709
- for (const [name, agent] of Object.entries(this.config.agents ?? {})) {
23710
- 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);
23711
23855
  if (effective === label) {
23712
23856
  if (this.fanoutForAgent(name))
23713
23857
  fanned.push(name);
@@ -23726,10 +23870,9 @@ class AuthBroker {
23726
23870
  const isEffective = effective === label;
23727
23871
  if (!isPinned && !isEffective)
23728
23872
  continue;
23729
- const toMirror = effective ?? bound;
23730
- if (toMirror == null)
23873
+ if (effective == null)
23731
23874
  continue;
23732
- if (this.mirrorAccountToConsumer(toMirror, consumer)) {
23875
+ if (this.mirrorAccountToConsumer(effective, consumer)) {
23733
23876
  fanned.push(consumer.name);
23734
23877
  }
23735
23878
  }
@@ -23830,6 +23973,10 @@ class AuthBroker {
23830
23973
  const effective = agent.auth?.override ?? auth.active;
23831
23974
  if (effective !== label)
23832
23975
  continue;
23976
+ if (agent.auth?.override && agent.auth?.strict)
23977
+ continue;
23978
+ if (!this.isServableBy(next, name))
23979
+ continue;
23833
23980
  if (this.mirrorAccountToAgent(next, name)) {
23834
23981
  rolled.push(name);
23835
23982
  }
@@ -23838,8 +23985,13 @@ class AuthBroker {
23838
23985
  }
23839
23986
  nextHealthyAccount(current, order) {
23840
23987
  const start = order.indexOf(current);
23841
- if (start === -1)
23842
- 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
+ }
23843
23995
  let bestSoftAvoided = null;
23844
23996
  let bestScore = Number.POSITIVE_INFINITY;
23845
23997
  for (let i = 1;i <= order.length; i++) {
@@ -23848,6 +24000,8 @@ class AuthBroker {
23848
24000
  continue;
23849
24001
  if (this.isAccountExhausted(cand) || !accountExists(cand, this.home))
23850
24002
  continue;
24003
+ if (this.exclusiveOwnerOf(cand))
24004
+ continue;
23851
24005
  if (!this.isAccountSoftAvoided(cand))
23852
24006
  return cand;
23853
24007
  const score = this.softAvoidUtilScore(cand);
@@ -23859,11 +24013,10 @@ class AuthBroker {
23859
24013
  return bestSoftAvoided;
23860
24014
  }
23861
24015
  fanoutForAgent(name) {
23862
- const auth = this.config.auth ?? {};
23863
24016
  const agent = (this.config.agents ?? {})[name];
23864
24017
  if (!agent)
23865
24018
  return false;
23866
- const effective = this.accountWithFailover(agent.auth?.override ?? auth.active);
24019
+ const effective = this.servedAccountForAgent(name);
23867
24020
  if (!effective)
23868
24021
  return false;
23869
24022
  return this.mirrorAccountToAgent(effective, name);
@@ -23992,6 +24145,15 @@ class AuthBroker {
23992
24145
  `);
23993
24146
  return;
23994
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
+ }
23995
24157
  if (ov.active === this.config.auth?.active)
23996
24158
  return;
23997
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;