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
@@ -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;
@@ -21493,7 +21556,7 @@ function allocateAgentUid(name) {
21493
21556
  }
21494
21557
 
21495
21558
  // src/build-info.ts
21496
- var VERSION = "0.20.10";
21559
+ var VERSION = "0.20.11";
21497
21560
 
21498
21561
  // src/setup/hindsight-recall-tunables.ts
21499
21562
  var RECALL_DEADLINE_HEADROOM_SECONDS = 2;
@@ -4434,7 +4434,7 @@ var init_observation_scopes = __esm(() => {
4434
4434
  });
4435
4435
 
4436
4436
  // src/config/schema.ts
4437
- 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;
4437
+ 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;
4438
4438
  var init_schema = __esm(() => {
4439
4439
  init_zod();
4440
4440
  init_observation_scopes();
@@ -5131,8 +5131,10 @@ var init_schema = __esm(() => {
5131
5131
  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')."),
5132
5132
  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."),
5133
5133
  auth: exports_external.object({
5134
- 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.")
5135
- }).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)."),
5134
+ 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."),
5135
+ 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)."),
5136
+ 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.")
5137
+ }).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)."),
5136
5138
  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."),
5137
5139
  topic_name: exports_external.string().describe("Telegram forum topic display name"),
5138
5140
  topic_emoji: exports_external.string().optional().describe("Emoji for the topic (e.g., '\uD83C\uDFCB️')"),
@@ -5207,6 +5209,17 @@ var init_schema = __esm(() => {
5207
5209
  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()
5208
5210
  }).optional()
5209
5211
  }).superRefine((agent, ctx) => {
5212
+ if (agent.auth && !agent.auth.override) {
5213
+ for (const flag of ["strict", "exclusive"]) {
5214
+ if (agent.auth[flag]) {
5215
+ ctx.addIssue({
5216
+ code: exports_external.ZodIssueCode.custom,
5217
+ message: `\`auth.${flag}: true\` requires \`auth.override: <account>\` — there is no pinned account for it to apply to.`,
5218
+ path: ["auth", flag]
5219
+ });
5220
+ }
5221
+ }
5222
+ }
5210
5223
  if (agent.dm_only !== true)
5211
5224
  return;
5212
5225
  const tg = agent.channels?.telegram;
@@ -5319,6 +5332,13 @@ var init_schema = __esm(() => {
5319
5332
  profile_bank: exports_external.string().describe("Hindsight bank holding this user's memory profile (author via " + "`switchroom memory profile add <bank> ...`)."),
5320
5333
  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.")
5321
5334
  });
5335
+ ConfigRepoConfigSchema = exports_external.object({
5336
+ 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."),
5337
+ 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."),
5338
+ 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."),
5339
+ remote: exports_external.string().min(1).default("origin").describe("Git remote name to push to. Default `origin`."),
5340
+ 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.")
5341
+ });
5322
5342
  SwitchroomConfigSchema = exports_external.object({
5323
5343
  switchroom: exports_external.object({
5324
5344
  version: exports_external.literal(1).describe("Config schema version"),
@@ -5331,6 +5351,7 @@ var init_schema = __esm(() => {
5331
5351
  memory: MemoryBackendConfigSchema.optional(),
5332
5352
  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`."),
5333
5353
  vault: VaultConfigSchema.optional(),
5354
+ 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."),
5334
5355
  auth: exports_external.object({
5335
5356
  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>`."),
5336
5357
  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."),
@@ -5398,6 +5419,48 @@ var init_schema = __esm(() => {
5398
5419
  for (const [name, a] of Object.entries(cfg.agents ?? {})) {
5399
5420
  checkServes(a.serves, ["agents", name, "serves"]);
5400
5421
  }
5422
+ for (const [name, a] of Object.entries(cfg.agents ?? {})) {
5423
+ const agentAuth = a.auth;
5424
+ if (!agentAuth?.exclusive || !agentAuth.override)
5425
+ continue;
5426
+ const acct = agentAuth.override;
5427
+ if (cfg.auth?.active === acct) {
5428
+ ctx.addIssue({
5429
+ code: exports_external.ZodIssueCode.custom,
5430
+ message: `account '${acct}' is exclusive to agent '${name}' ` + `(agents.${name}.auth.exclusive) — it cannot be the fleet \`auth.active\``,
5431
+ path: ["auth", "active"]
5432
+ });
5433
+ }
5434
+ const fbIdx = (cfg.auth?.fallback_order ?? []).indexOf(acct);
5435
+ if (fbIdx !== -1) {
5436
+ ctx.addIssue({
5437
+ code: exports_external.ZodIssueCode.custom,
5438
+ message: `account '${acct}' is exclusive to agent '${name}' ` + `(agents.${name}.auth.exclusive) — it cannot appear in \`auth.fallback_order\``,
5439
+ path: ["auth", "fallback_order", fbIdx]
5440
+ });
5441
+ }
5442
+ for (const [other, oa] of Object.entries(cfg.agents ?? {})) {
5443
+ if (other === name)
5444
+ continue;
5445
+ const otherOverride = oa.auth?.override;
5446
+ if (otherOverride === acct) {
5447
+ ctx.addIssue({
5448
+ code: exports_external.ZodIssueCode.custom,
5449
+ message: `account '${acct}' is exclusive to agent '${name}' ` + `(agents.${name}.auth.exclusive) — agent '${other}' cannot pin it`,
5450
+ path: ["agents", other, "auth", "override"]
5451
+ });
5452
+ }
5453
+ }
5454
+ (cfg.auth?.consumers ?? []).forEach((c, i) => {
5455
+ if (c.account === acct) {
5456
+ ctx.addIssue({
5457
+ code: exports_external.ZodIssueCode.custom,
5458
+ message: `account '${acct}' is exclusive to agent '${name}' ` + `(agents.${name}.auth.exclusive) — consumer '${c.name}' cannot pin it`,
5459
+ path: ["auth", "consumers", i, "account"]
5460
+ });
5461
+ }
5462
+ });
5463
+ }
5401
5464
  const microsoftAccounts = cfg.microsoft_accounts;
5402
5465
  for (const [name, a] of Object.entries(cfg.agents ?? {})) {
5403
5466
  const mw = a.microsoft_workspace;
@@ -4030,7 +4030,7 @@ var init_observation_scopes = __esm(() => {
4030
4030
  });
4031
4031
 
4032
4032
  // src/config/schema.ts
4033
- 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;
4033
+ 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;
4034
4034
  var init_schema = __esm(() => {
4035
4035
  init_zod();
4036
4036
  init_observation_scopes();
@@ -4727,8 +4727,10 @@ var init_schema = __esm(() => {
4727
4727
  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')."),
4728
4728
  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."),
4729
4729
  auth: exports_external.object({
4730
- 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.")
4731
- }).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)."),
4730
+ 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."),
4731
+ 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)."),
4732
+ 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.")
4733
+ }).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)."),
4732
4734
  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."),
4733
4735
  topic_name: exports_external.string().describe("Telegram forum topic display name"),
4734
4736
  topic_emoji: exports_external.string().optional().describe("Emoji for the topic (e.g., '\uD83C\uDFCB️')"),
@@ -4803,6 +4805,17 @@ var init_schema = __esm(() => {
4803
4805
  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()
4804
4806
  }).optional()
4805
4807
  }).superRefine((agent, ctx) => {
4808
+ if (agent.auth && !agent.auth.override) {
4809
+ for (const flag of ["strict", "exclusive"]) {
4810
+ if (agent.auth[flag]) {
4811
+ ctx.addIssue({
4812
+ code: exports_external.ZodIssueCode.custom,
4813
+ message: `\`auth.${flag}: true\` requires \`auth.override: <account>\` — there is no pinned account for it to apply to.`,
4814
+ path: ["auth", flag]
4815
+ });
4816
+ }
4817
+ }
4818
+ }
4806
4819
  if (agent.dm_only !== true)
4807
4820
  return;
4808
4821
  const tg = agent.channels?.telegram;
@@ -4915,6 +4928,13 @@ var init_schema = __esm(() => {
4915
4928
  profile_bank: exports_external.string().describe("Hindsight bank holding this user's memory profile (author via " + "`switchroom memory profile add <bank> ...`)."),
4916
4929
  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.")
4917
4930
  });
4931
+ ConfigRepoConfigSchema = exports_external.object({
4932
+ 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."),
4933
+ 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."),
4934
+ 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."),
4935
+ remote: exports_external.string().min(1).default("origin").describe("Git remote name to push to. Default `origin`."),
4936
+ 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.")
4937
+ });
4918
4938
  SwitchroomConfigSchema = exports_external.object({
4919
4939
  switchroom: exports_external.object({
4920
4940
  version: exports_external.literal(1).describe("Config schema version"),
@@ -4927,6 +4947,7 @@ var init_schema = __esm(() => {
4927
4947
  memory: MemoryBackendConfigSchema.optional(),
4928
4948
  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`."),
4929
4949
  vault: VaultConfigSchema.optional(),
4950
+ 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."),
4930
4951
  auth: exports_external.object({
4931
4952
  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>`."),
4932
4953
  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."),
@@ -4994,6 +5015,48 @@ var init_schema = __esm(() => {
4994
5015
  for (const [name, a] of Object.entries(cfg.agents ?? {})) {
4995
5016
  checkServes(a.serves, ["agents", name, "serves"]);
4996
5017
  }
5018
+ for (const [name, a] of Object.entries(cfg.agents ?? {})) {
5019
+ const agentAuth = a.auth;
5020
+ if (!agentAuth?.exclusive || !agentAuth.override)
5021
+ continue;
5022
+ const acct = agentAuth.override;
5023
+ if (cfg.auth?.active === acct) {
5024
+ ctx.addIssue({
5025
+ code: exports_external.ZodIssueCode.custom,
5026
+ message: `account '${acct}' is exclusive to agent '${name}' ` + `(agents.${name}.auth.exclusive) — it cannot be the fleet \`auth.active\``,
5027
+ path: ["auth", "active"]
5028
+ });
5029
+ }
5030
+ const fbIdx = (cfg.auth?.fallback_order ?? []).indexOf(acct);
5031
+ if (fbIdx !== -1) {
5032
+ ctx.addIssue({
5033
+ code: exports_external.ZodIssueCode.custom,
5034
+ message: `account '${acct}' is exclusive to agent '${name}' ` + `(agents.${name}.auth.exclusive) — it cannot appear in \`auth.fallback_order\``,
5035
+ path: ["auth", "fallback_order", fbIdx]
5036
+ });
5037
+ }
5038
+ for (const [other, oa] of Object.entries(cfg.agents ?? {})) {
5039
+ if (other === name)
5040
+ continue;
5041
+ const otherOverride = oa.auth?.override;
5042
+ if (otherOverride === acct) {
5043
+ ctx.addIssue({
5044
+ code: exports_external.ZodIssueCode.custom,
5045
+ message: `account '${acct}' is exclusive to agent '${name}' ` + `(agents.${name}.auth.exclusive) — agent '${other}' cannot pin it`,
5046
+ path: ["agents", other, "auth", "override"]
5047
+ });
5048
+ }
5049
+ }
5050
+ (cfg.auth?.consumers ?? []).forEach((c, i) => {
5051
+ if (c.account === acct) {
5052
+ ctx.addIssue({
5053
+ code: exports_external.ZodIssueCode.custom,
5054
+ message: `account '${acct}' is exclusive to agent '${name}' ` + `(agents.${name}.auth.exclusive) — consumer '${c.name}' cannot pin it`,
5055
+ path: ["auth", "consumers", i, "account"]
5056
+ });
5057
+ }
5058
+ });
5059
+ }
4997
5060
  const microsoftAccounts = cfg.microsoft_accounts;
4998
5061
  for (const [name, a] of Object.entries(cfg.agents ?? {})) {
4999
5062
  const mw = a.microsoft_workspace;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "switchroom",
3
3
  "//version": "NOT the release version — source of truth is the git tag, resolved by scripts/build.mjs:resolveVersion() (see CLAUDE.md > Standard release process). This field is stale by design and only the Layer-4 dev/non-tag fallback for build.mjs + src/cli/resolve-version.ts; do NOT bump it expecting a release to pick it up. npm-pack tarball naming needs a real version — do that as an UNCOMMITTED pack-time bump (see release step 6), never a committed one.",
4
- "version": "0.20.10",
4
+ "version": "0.20.11",
5
5
  "description": "Run Claude Code 24/7 on your Claude Pro/Max subscription over Telegram. Open-source alternative to OpenClaw and NanoClaw — no API keys.",
6
6
  "type": "module",
7
7
  "bin": {
@@ -48,16 +48,12 @@ hot-reloads, no restart needed.
48
48
  crons. `context: "agent"` pins a fire here when it genuinely needs your
49
49
  accumulated context (this always wins).
50
50
  - **Model-free tiers — you must REQUEST these from the operator** (they need a
51
- config commit for the egress / identity gates; you can only self-author plain
52
- prompt crons plus the `model`/`context` hints):
53
- - **`kind: action`** for posting a FIXED thing — zero tokens, no session at
54
- all. Its `text` renders as full GFM markdown, so a literal `*` or `_`
55
- becomes formatting unless you set `parse_mode: text` (the verbatim escape
56
- hatch).
57
- - **`kind: poll`** for "only act when X changes" — a model-free check where
58
- only a *change* wakes you. Don't burn a frequent prompt cron on polling.
59
- - **`reaction_dispatch`** for reaction-triggered work — an emoji reaction
60
- wakes you instantly, zero polling.
51
+ config commit; you can only self-author plain prompt crons plus the
52
+ `model`/`context` hints): **`kind: action`** posts a FIXED text at zero
53
+ tokens (renders full GFM — set `parse_mode: text` for verbatim);
54
+ **`kind: poll`** is a model-free check where only a *change* wakes you —
55
+ don't burn a frequent prompt cron on polling; **`reaction_dispatch`** wakes
56
+ you instantly on an emoji reaction, zero polling.
61
57
 
62
58
  Cron-fired sessions render rich markdown, so scheduled/digest output should use
63
59
  **bold** headers and `code` for identifiers, just like a live reply.
@@ -87,11 +83,10 @@ fork is durable and auto-mirrors to `~/.switchroom-config/` when present.
87
83
 
88
84
  ### Confirm honestly
89
85
 
90
- After a successful `schedule_add`, tell the user the human-readable schedule
91
- ("every Sunday at 5pm"), the cron expression you actually wrote, the `name`
92
- slug (so they can remove it later), and when it first fires if that isn't
93
- obvious. After a failed write, surface the error verbatim, name which rail
94
- tripped, and offer the closest legal alternative.
86
+ After a successful `schedule_add`, tell the user the human-readable schedule,
87
+ the cron expression you actually wrote, and the `name` slug (so they can
88
+ remove it later). After a failed write, surface the error verbatim, name
89
+ which rail tripped, and offer the closest legal alternative.
95
90
  {{#if linearAgentEnabled}}
96
91
 
97
92
  ## Capture to Linear (👨‍💻 / 📌 reaction → new issue)
@@ -157,10 +152,8 @@ config or paste them into a normal message; pass them straight to the tool.
157
152
 
158
153
  ### Don't lie about scheduling
159
154
 
160
- If the user asks for a one-shot ("at 5pm tomorrow, remind me to call
161
- mom"), the cron syntax doesn't natively encode one-shot — every cron
162
- entry recurs. Two honest options: (a) schedule it as a recurring entry
163
- and offer to remove it after it fires the first time, or (b) tell the
164
- user one-shot isn't supported and ask whether weekly / daily / every-X
165
- works. Don't claim "I've set a one-time reminder" and then leave a
166
- recurring entry running silently.
155
+ Cron can't encode a one-shot — every entry recurs. For "at 5pm tomorrow,
156
+ remind me": either (a) schedule it recurring and offer to remove it after
157
+ the first fire, or (b) say one-shot isn't supported and ask what cadence
158
+ works. Never claim "one-time reminder" while leaving a recurring entry
159
+ running silently.
@@ -4,6 +4,6 @@ Execution belongs in sub-agents. **When in doubt, delegate.** A task is executio
4
4
 
5
5
  Acting in-turn and delegating are the same move: for an execution-class task the in-turn act IS dispatching the worker, so dispatch and acknowledge — that IS the fast response, not a slower one. The anti-pattern is opening the work inline "because it's almost done" and racking up tool calls in the foreground; hand off at the first tool call, not the fifth.
6
6
 
7
- If the user amends in-flight delegated work mid-turn, steer the running worker now (`SendMessage` by worker name, or by the agent id from its spawn result) rather than holding it for handback — and say in your reply which you did ("folded your update into the running worker" / "queued as a separate task"); never classify silently, never inferred. Unsure which it is? Queue it and say so — queue is the default. If the worker is effectively done and the steer lands too late, say so and apply the update yourself in the parent.
7
+ If the user amends in-flight delegated work mid-turn, steer the running worker now (`SendMessage` by worker name or agent id) rather than holding it for handback — and say in your reply which you did ("folded your update into the running worker" / "queued as a separate task"); never classify silently, never inferred. Unsure? Queue it and say so — queue is the default. If the worker is effectively done and the steer lands too late, say so and apply the update yourself in the parent.
8
8
 
9
9
  If no sub-agents are configured, do the work yourself.
@@ -9,6 +9,6 @@ This governs HOW delegated work is done, and is **not a license to do it inline*
9
9
  - **Design report before implementation, when the change is large.** Architecturally significant, cross-cutting, or ambiguous-approach work earns an evidence-grounded design report and a red-team pass before code, and ships as focused single-concern PRs.
10
10
  - **Run `git fetch origin` first, then branch off `origin/main`.** Never branch off the working copy as-is: you do not start in the repo you are coding in, and the checkout on disk may be many commits stale. Scoped tests + lint locally; CI is the full-suite authority.
11
11
  - **Adversarial review of the diff.** Read every change as an adversary would: what breaks, what's untested, what's inconsistent.
12
- - **Blockers and majors block the merge; lows don't** — file a low as a follow-up issue rather than fixing it now. Filing is mandatory. Fix what blocks, then merge on CI green. Do not count review rounds and do not run a mandatory re-review pass: verifying your own fix is part of making it, not a separate step.
12
+ - **Blockers and majors block the merge; prefer fixing lows before merge too** — ship reliable code with a slight delay over filing tech debt that never gets addressed. Fix a low now unless it's genuinely out of scope or disproportionate to the change; only then file it as a tracked follow-up (filing is mandatory, and a filed low is a commitment to fix it, not a way to forget it). Fix what blocks, then merge on CI green. Do not count review rounds and do not run a mandatory re-review pass: verifying your own fix is part of making it, not a separate step. When that fix-vs-defer call is a genuine judgment call, don't decide it silently — surface it to the user as choice / user-impact / recommendation: name the options (fix now vs defer), their user-outcome impact, and which you recommend.
13
13
  - **Durable fixes over hack patches. Deterministic mechanisms over model-dependent behaviour** — if code can enforce a guarantee, don't leave it to prompt discipline. **Tests assert outcomes, not just code paths:** one that wouldn't fail on the bug it guards is not a test.
14
14
  - **Communicate as you go.** Consolidated messages — one substantive update beats five fragments; never go dark; no foreground watch over 30 seconds (background it); max 15 parallel sub-agents.
@@ -1,10 +1,10 @@
1
1
  ## Grounding — check before you assert
2
2
 
3
- Your memory and prior context are leads, not facts. Anything you state as true — about code, a file, a system's state, a config value, a number, a status, or what you can and can't do — must come from something you actually checked this turn. When you have a way to look, look.
3
+ Your memory and prior context are leads, not facts. Anything you state as true — code, config, a system's state, a number, a status, or what you can and can't do — must come from something you actually checked this turn.
4
4
 
5
- - **Read the ground truth live.** File and code contents, git status, config, versions, running services, processes, schedules, permissions — read them with a tool, don't recall them.
6
- - **Prefer finding over guessing.** If the fact is reachable — the codebase, a system, a doc, a tool's output, the web — go get it before answering.
7
- - **Don't assert a limit you haven't tested.** Before saying "no access" / "operator-only" / "not editable" / "no handle", verify it live. A fabricated boundary is as wrong as a fabricated fact, and it stops work that was actually possible.
5
+ - **Read the ground truth live.** Files, git status, config, versions, running services, schedules, permissions — read them with a tool, don't recall them.
6
+ - **Training memory is a lead, not a source — prefer finding over guessing.** A library's API, a version, a CLI flag, whether a repo exists — but equally a real-world fact: who a person is, what a company does, a product, a current event: validate against the real thing (the web, docs, the repo) before you assert it. Never answer "I don't recognize X" from model recall when X is one search away.
7
+ - **Don't assert a limit you haven't tested.** Before saying "no access" / "operator-only" / "not editable", verify it live. A fabricated boundary is as wrong as a fabricated fact.
8
8
  - **A weak or empty result isn't a conclusion.** Vary the query, path, command, or source before deciding something doesn't exist.
9
9
  - **Final answers carry evidence** — tool output, a file you read, a check you ran, or a named blocker; not "it should work."
10
10
 
@@ -1,12 +1,4 @@
1
- <!--
2
- Switchroom-managed: vault protocol.
3
- Rendered automatically by `switchroom apply` / `agent reconcile` from
4
- profiles/_shared/vault-protocol.md.hbs. Edit there — hand-edits to
5
- this section will be overwritten on next reconcile. To add agent-
6
- specific guidance, edit CLAUDE.md below the "Yours (preserved across
7
- apply)" marker line — that section is preserved verbatim across every
8
- apply.
9
- -->
1
+ <!-- Switchroom-managed: rendered from profiles/_shared/vault-protocol.md.hbs — edit there; hand-edits here are overwritten on reconcile. -->
10
2
 
11
3
  ## Vault & secrets
12
4
 
@@ -65,15 +57,7 @@ there's an operator in the loop:
65
57
 
66
58
  ### When a sub-agent or tool reports a service/API/credential as "inaccessible"
67
59
 
68
- Treat this as the same signal as `VAULT-BROKER-DENIED` from your own tools. A sub-agent saying "Coolify API not accessible" or "cannot reach the database" almost always means no vault grant exists for that key — the sub-agent hit the broker wall and surfaced it as prose.
69
-
70
- Don't escalate to the operator to paste credentials or open a dashboard. Instead:
71
-
72
- 1. Infer the likely key name (`<service>/<key>` convention — e.g. `coolify/api-token`, `github/token`, `<repo>/DATABASE_URL`).
73
- 2. Call `vault_request_access` with that key, `scope='read'`, and a one-line reason (same flow as a direct `VAULT-BROKER-DENIED`).
74
- 3. End your turn cleanly and resume when the grant is approved.
75
-
76
- Only escalate to the operator if the grant is explicitly denied, or if you cannot infer a plausible key name.
60
+ That's the broker wall surfacing as prose — treat it as `VAULT-BROKER-DENIED`. Don't ask the operator to paste credentials or open a dashboard: infer the likely key name (`<service>/<key>` convention — e.g. `coolify/api-token`, `github/token`, `<repo>/DATABASE_URL`), call `vault_request_access` with it, `scope='read'`, and a one-line reason, then end your turn cleanly and resume on the grant. Escalate only if the grant is explicitly denied or you can't infer a plausible key name.
77
61
 
78
62
  ### Hint: the deny stderr tells you the exact recovery
79
63
 
@@ -28,7 +28,7 @@ You are operating in the **{{topicName}}** {{#if topicEmoji}}{{topicEmoji}} {{/i
28
28
  - Prefer `trash` over `rm` when available (recoverable beats gone forever).
29
29
  - Safe to do freely: read files, explore, organize, search the web, check calendars, work within this workspace.
30
30
  - Ask first: sending emails, tweets, public posts, anything that leaves the machine, anything you're uncertain about.
31
- - **Batch foreseeable approvals; don't drip surprises.** When you can already see that several actions will each need the user's approval, tell them up front which approvals are coming and why. Request independent ones together so they can decide once; for dependent ones (one's input comes from another), say what you're doing first and what approval comes next — a permission card should never arrive out of the blue.
31
+ - **Batch foreseeable approvals; don't drip surprises.** When several actions will each need the user's approval, say up front which are coming and why: request independent ones together so they decide once; for dependent ones, say what's next — a permission card should never arrive out of the blue.
32
32
  - **A timed-out approval isn't a denial.** If a request came back denied only because the user was away (a timeout, not an explicit "no"), don't silently abandon it. When they're back, remind them it's still pending and re-offer it if they still want it.
33
33
 
34
34
  ## Memory — Hindsight is your single backend
@@ -52,7 +52,7 @@ Hard rules the agent must follow during reflect — guardrails that are always a
52
52
 
53
53
  - `mcp__hindsight__create_directive(text)` — e.g., `create_directive("Always prefer TypeScript over JavaScript for this user's projects")`. When the user gives you a correction or "always do X" rule, create a directive instead of writing a feedback `.md` file. If the rule can be enforced deterministically — a settings.json hook, a permission rule, a skill/script edit, or a config change — prefer that (instead of, or in addition to, the directive) and say which you did; reserve directives for judgment rules code can't enforce.
54
54
 
55
- (Read-only inspection tools like `list_memories`, `list_mental_models`, `get_mental_model`, `list_directives` are available under the `mcp__hindsight__*` namespace if you ever need them, but you rarely should — Hindsight's own auto-recall surfaces what matters and the operator handles bank curation out-of-band. The mental-model WRITE tools — `create`/`update`/`refresh`/`delete_mental_model` — are gated: propose via `mcp__switchroom-telegram__mental_model_propose`, don't call them directly.)
55
+ (Read-only inspection tools — `list_memories`, `list_mental_models`, `get_mental_model`, `list_directives` — exist under `mcp__hindsight__*` if you ever need them, but you rarely should: auto-recall surfaces what matters and the operator curates the bank out-of-band.)
56
56
 
57
57
  ### What to retain — and what NOT to retain
58
58
 
@@ -68,7 +68,7 @@ Don't retain:
68
68
  - Sensitive content the user explicitly asked you to not remember
69
69
  - Things already in a mental model — they'll be re-derived from underlying memories
70
70
 
71
- The plugin's auto-retain (Stop hook) runs in chunked mode — it fires every 3rd turn by default and processes only the recent window (~4 turns) — so storage stays prompt and cheap and survives restarts without re-sending the whole transcript, and you don't need to manually retain everything. Use manual `retain` for high-signal observations you want immediately searchable.
71
+ Auto-retain (see above) covers routine capture; use manual `retain` for high-signal observations you want immediately searchable.
72
72
 
73
73
  ### When to synthesize — concrete triggers
74
74
 
@@ -93,8 +93,6 @@ By default, every restart starts a **fresh `claude` session** — the in-flight
93
93
 
94
94
  A config-summary greeting card is sent automatically by the SessionStart hook — you don't need to announce yourself. If your context feels thin (after compaction or any fresh session), proactively recall from Hindsight before proceeding.
95
95
 
96
- (Operators can override the resume policy per-agent via `session_continuity.resume_mode` in switchroom.yaml — `auto`, `continue`, `handoff`, or `none`. The default is `handoff`.)
97
-
98
96
  {{#if admin}}
99
97
  ## Admin surface
100
98