switchroom 0.20.9 → 0.20.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/bin/handoff-briefing.sh +57 -5
  2. package/bin/working-state-reload-hook.sh +262 -0
  3. package/dist/agent-scheduler/index.js +65 -2
  4. package/dist/auth-broker/index.js +204 -24
  5. package/dist/cli/notion-write-pretool.mjs +65 -2
  6. package/dist/cli/self-improve-apply-guard-pretool.mjs +357 -92
  7. package/dist/cli/self-improve-stop.mjs +889 -7
  8. package/dist/cli/skill-validate-pretool.mjs +82 -3
  9. package/dist/cli/switchroom.js +3699 -2110
  10. package/dist/host-control/main.js +67 -4
  11. package/dist/vault/approvals/kernel-server.js +66 -3
  12. package/dist/vault/broker/server.js +66 -3
  13. package/package.json +1 -1
  14. package/profiles/_base/start.sh.hbs +49 -0
  15. package/profiles/_shared/agent-self-service.md.hbs +15 -22
  16. package/profiles/_shared/delegation-golden-rule.md.hbs +1 -1
  17. package/profiles/_shared/dev-protocol.md.hbs +1 -1
  18. package/profiles/_shared/execution-discipline.md.hbs +4 -4
  19. package/profiles/_shared/vault-protocol.md.hbs +2 -18
  20. package/profiles/default/CLAUDE.md.hbs +3 -5
  21. package/telegram-plugin/auto-fallback-fleet.ts +37 -2
  22. package/telegram-plugin/dist/gateway/gateway.js +1414 -918
  23. package/telegram-plugin/fallback-card-collapse.ts +1 -0
  24. package/telegram-plugin/gateway/auth-command.ts +11 -1
  25. package/telegram-plugin/gateway/callback-query-handlers.ts +100 -0
  26. package/telegram-plugin/gateway/eval-case-proposal-card.ts +86 -0
  27. package/telegram-plugin/gateway/fleet-fallback-notice-cooldown.test.ts +74 -0
  28. package/telegram-plugin/gateway/fleet-fallback-notice-cooldown.ts +71 -0
  29. package/telegram-plugin/gateway/gateway.ts +85 -90
  30. package/telegram-plugin/gateway/ipc-protocol.ts +43 -0
  31. package/telegram-plugin/gateway/ipc-server.ts +28 -0
  32. package/telegram-plugin/gateway/narrative-lane.ts +33 -2
  33. package/telegram-plugin/gateway/privacy-reset.test.ts +216 -0
  34. package/telegram-plugin/gateway/privacy-reset.ts +87 -0
  35. package/telegram-plugin/gateway/privacy-state.test.ts +165 -0
  36. package/telegram-plugin/gateway/privacy-state.ts +206 -0
  37. package/telegram-plugin/gateway/self-improve-proposal-wiring.ts +176 -0
  38. package/telegram-plugin/gateway/stale-pin-sweep-wiring.ts +24 -14
  39. package/telegram-plugin/gateway/stale-pin-sweep.test.ts +123 -26
  40. package/telegram-plugin/gateway/stale-pin-sweep.ts +48 -32
  41. package/telegram-plugin/gateway/throttle-tier-wiring.ts +15 -4
  42. package/telegram-plugin/slot-banner-driver.ts +42 -5
  43. package/telegram-plugin/tests/auto-fallback-fleet.test.ts +24 -0
  44. package/telegram-plugin/tests/gateway-handler-registration-wiring.test.ts +2 -0
  45. package/telegram-plugin/tests/narrative-lane-golden.test.ts +97 -0
  46. package/telegram-plugin/tests/privacy-reset-call-sites.test.ts +120 -0
  47. package/telegram-plugin/tests/status-pin-store.test.ts +25 -0
  48. package/telegram-plugin/tests/throttle-tier.test.ts +16 -0
  49. package/telegram-plugin/tests/turn-flush-safety.test.ts +67 -0
  50. package/telegram-plugin/throttle-tier.ts +12 -3
  51. package/telegram-plugin/turn-flush-safety.ts +97 -0
  52. package/vendor/hindsight-memory/CHANGELOG.md +31 -0
  53. package/vendor/hindsight-memory/hooks/hooks.json +2 -1
  54. package/vendor/hindsight-memory/scripts/retain.py +306 -0
  55. package/vendor/hindsight-memory/scripts/session_start.py +35 -8
  56. package/vendor/hindsight-memory/scripts/subagent_retain.py +29 -1
  57. package/vendor/hindsight-memory/scripts/tests/test_private_mode.py +415 -0
  58. package/vendor/hindsight-memory/scripts/tests/test_self_improve_correction_tag.py +167 -0
  59. package/vendor/hindsight-memory/scripts/tests/test_session_start_durability.py +107 -0
@@ -10994,7 +10994,7 @@ var init_observation_scopes = __esm(() => {
10994
10994
  });
10995
10995
 
10996
10996
  // src/config/schema.ts
10997
- var CodeRepoEntrySchema, AgentBindMountSchema, HttpDiffPollSchema, PollSpecSchema, TelegramMessageActionSchema, WebhookActionSchema, ActionSpecSchema, ScheduleEntrySchema, AgentSoulSchema, AgentToolsSchema, ObservationScopesSchema, ObservationScopeStrategySchema, AntiConfabulationDirectiveSchema, AgentMemorySchema, HookEntrySchema, AgentHooksSchema, SubagentSchema, SessionSchema, SessionContinuitySchema, webhookDispatchRule, TelegramChannelSchema, BuzzChannelSchema, ChannelsSchema, TIMEZONE_REGEX, ApproverIdSchema, GoogleWorkspaceTierSchema, GoogleServiceTokenSchema, GoogleWorkspaceConfigSchema, LiteLLMConfigSchema, HindsightPerOpLlmSchema, HindsightConfigSchema, MicrosoftWorkspaceConfigSchema, NotionWorkspaceConfigSchema, AgentGoogleWorkspaceConfigSchema, MicrosoftAccountEmailSchema, MicrosoftToolTokenSchema, MicrosoftAccountBindingSchema, AgentMicrosoftWorkspaceConfigSchema, AgentNotionWorkspaceConfigSchema, ReactionsSchema, ReactionDispatchSchema, releaseBlockFields, ReleaseBlock, RootReleaseBlock, NetworkIsolationSchema, servesField, knowsField, profileFields, ProfileSchema, _omitExtends, defaultsFields, AgentDefaultsSchema, AgentSchema, TelegramConfigSchema, MemoryBackendConfigSchema, VaultConfigSchema, QuotaConfigSchema, AutoReleaseCheckSchema, HostControlConfigSchema, WebServiceConfigSchema, FleetHealthConfigSchema, HostdConfigSchema, CronEgressSchema, CronConfigSchema, UserSchema, SwitchroomConfigSchema;
10997
+ var CodeRepoEntrySchema, AgentBindMountSchema, HttpDiffPollSchema, PollSpecSchema, TelegramMessageActionSchema, WebhookActionSchema, ActionSpecSchema, ScheduleEntrySchema, AgentSoulSchema, AgentToolsSchema, ObservationScopesSchema, ObservationScopeStrategySchema, AntiConfabulationDirectiveSchema, AgentMemorySchema, HookEntrySchema, AgentHooksSchema, SubagentSchema, SessionSchema, SessionContinuitySchema, webhookDispatchRule, TelegramChannelSchema, BuzzChannelSchema, ChannelsSchema, TIMEZONE_REGEX, ApproverIdSchema, GoogleWorkspaceTierSchema, GoogleServiceTokenSchema, GoogleWorkspaceConfigSchema, LiteLLMConfigSchema, HindsightPerOpLlmSchema, HindsightConfigSchema, MicrosoftWorkspaceConfigSchema, NotionWorkspaceConfigSchema, AgentGoogleWorkspaceConfigSchema, MicrosoftAccountEmailSchema, MicrosoftToolTokenSchema, MicrosoftAccountBindingSchema, AgentMicrosoftWorkspaceConfigSchema, AgentNotionWorkspaceConfigSchema, ReactionsSchema, ReactionDispatchSchema, releaseBlockFields, ReleaseBlock, RootReleaseBlock, NetworkIsolationSchema, servesField, knowsField, profileFields, ProfileSchema, _omitExtends, defaultsFields, AgentDefaultsSchema, AgentSchema, TelegramConfigSchema, MemoryBackendConfigSchema, VaultConfigSchema, QuotaConfigSchema, AutoReleaseCheckSchema, HostControlConfigSchema, WebServiceConfigSchema, FleetHealthConfigSchema, HostdConfigSchema, CronEgressSchema, CronConfigSchema, UserSchema, ConfigRepoConfigSchema, SwitchroomConfigSchema;
10998
10998
  var init_schema = __esm(() => {
10999
10999
  init_zod();
11000
11000
  init_observation_scopes();
@@ -11691,8 +11691,10 @@ var init_schema = __esm(() => {
11691
11691
  bot_username: exports_external.string().optional().describe("Per-agent Telegram bot username (without leading @) when it doesn't " + "contain the agent slug. Replaces the default 'username includes slug' " + "preflight check with an exact (case-insensitive) match. Use when an " + "agent and its bot have intentionally divergent names (e.g. agent " + "'lawgpt' paired with bot '@meken_law_bot')."),
11692
11692
  timezone: exports_external.string().regex(TIMEZONE_REGEX, "timezone must be an IANA zone name like 'Australia/Melbourne' or 'UTC' " + "(three-letter aliases like EST/PST and bare offsets like UTC+10 are not accepted)").optional().describe("Per-agent IANA timezone override. Wins over any profile/defaults " + "value and over the top-level switchroom.timezone global. Controls " + "the UserPromptSubmit timezone hook's emitted local time and the " + "agent container's `environment.TZ` in compose."),
11693
11693
  auth: exports_external.object({
11694
- override: exports_external.string().min(1).optional().describe("Per-agent override of the fleet-wide `auth.active`. Edge-case use only — " + "this agent talks to the named account regardless of fleet active. See RFC H §4.5.")
11695
- }).optional().describe("Account routing for switchroom-auth-broker. RFC H schema uses " + "fleet-wide `auth.active` plus per-agent `override:` for edge cases. " + "Pre-RFC-H `auth.accounts: [..]` and `auth_label:` are migrated in-place " + "on first apply (see src/auth/migrate-schema.ts)."),
11694
+ override: exports_external.string().min(1).optional().describe("Per-agent override of the fleet-wide `auth.active`. Edge-case use only — " + "this agent talks to the named account regardless of fleet active. See RFC H §4.5."),
11695
+ strict: exports_external.boolean().optional().describe("Requires `override`. When true, the pin is a hard binding, not a " + "routing preference: the broker NEVER serves this agent from any " + "other account — no failover to `auth.fallback_order` while the " + "pinned account is quota-walled or exhausted. The agent rides out " + "the wall on its own account (surfacing the normal 429/quota " + "cards) instead of silently borrowing fleet quota. Use for " + "accounts that must never cross a billing/compliance boundary " + "(e.g. an employer-provided subscription)."),
11696
+ exclusive: exports_external.boolean().optional().describe("Requires `override`. When true, the pinned account belongs to " + "THIS agent alone: the broker refuses to serve it to any other " + "agent or consumer, refuses `auth use <label>` (set-active) onto " + "it, and refuses pinning another agent to it. Config that routes " + "others to the account (`auth.active`, `auth.fallback_order`, " + "another agent's `override`, a consumer pin) is rejected at load. " + "Usually paired with `strict: true` for full two-way isolation.")
11697
+ }).optional().describe("Account routing for switchroom-auth-broker. RFC H schema uses " + "fleet-wide `auth.active` plus per-agent `override:` for edge cases, " + "with optional `strict:` (never borrow another account) and " + "`exclusive:` (no one else may use the pinned account) hardening. " + "Pre-RFC-H `auth.accounts: [..]` and `auth_label:` are migrated in-place " + "on first apply (see src/auth/migrate-schema.ts)."),
11696
11698
  dm_only: exports_external.boolean().optional().describe("Mark this agent as a DM-only bot — has its own bot_token and lives " + "exclusively in a private chat with the operator. Suppresses " + "scaffolding's default behavior of inheriting the global " + "telegram.forum_chat_id into the agent's access.json `groups` entry " + "(the forum chat the bot isn't a member of, which would otherwise " + "trigger a 'boot-probe-failed: 400 chat not found' warning every " + "restart). topic_name is still schema-required but unused — set it " + "to a display label like 'DM' for /switchroom status output."),
11697
11699
  topic_name: exports_external.string().describe("Telegram forum topic display name"),
11698
11700
  topic_emoji: exports_external.string().optional().describe("Emoji for the topic (e.g., '\uD83C\uDFCB️')"),
@@ -11767,6 +11769,17 @@ var init_schema = __esm(() => {
11767
11769
  tmp_size: exports_external.string().regex(/^\d+(\.\d+)?[kmgKMG]?$/, "tmp_size must be a Docker size string like '1g', '4g', '512m'").refine((v) => parseFloat(v) > 0, "tmp_size must be greater than zero").optional()
11768
11770
  }).optional()
11769
11771
  }).superRefine((agent, ctx) => {
11772
+ if (agent.auth && !agent.auth.override) {
11773
+ for (const flag of ["strict", "exclusive"]) {
11774
+ if (agent.auth[flag]) {
11775
+ ctx.addIssue({
11776
+ code: exports_external.ZodIssueCode.custom,
11777
+ message: `\`auth.${flag}: true\` requires \`auth.override: <account>\` — there is no pinned account for it to apply to.`,
11778
+ path: ["auth", flag]
11779
+ });
11780
+ }
11781
+ }
11782
+ }
11770
11783
  if (agent.dm_only !== true)
11771
11784
  return;
11772
11785
  const tg = agent.channels?.telegram;
@@ -11879,6 +11892,13 @@ var init_schema = __esm(() => {
11879
11892
  profile_bank: exports_external.string().describe("Hindsight bank holding this user's memory profile (author via " + "`switchroom memory profile add <bank> ...`)."),
11880
11893
  person_id: exports_external.string().optional().describe("Free-text display name projected into the inbound `<channel>` " + 'tag\'s `user` attribute (e.g. "Lisa") so an agent can greet by ' + "name instead of only seeing the raw Telegram id/username. NOT a " + "stable identity system — just a label. Resolution is boot-time-only " + "(no hot-reload; a config change needs an agent restart) and " + "chat-scoped (a resolved name is only shown in a chat/group the " + "person is actually a member of, per that chat's access.json " + "membership — never broadcast into every chat the agent operates " + "in). Keep this broadly safe to show, the same discipline as " + "picking a `profile_bank` name: there is no automated enforcement " + "that a `person_id` stays safe if a group's membership changes " + "later — see docs/configuration.md.")
11881
11894
  });
11895
+ ConfigRepoConfigSchema = exports_external.object({
11896
+ enabled: exports_external.boolean().default(false).describe("Master switch for config-repo change control. Default false (opt-in). " + "When false the `config-repo sync` verb still runs on demand but the " + "doctor check and (later) the scheduled tick treat the feature as off."),
11897
+ path: exports_external.string().regex(/^[a-zA-Z0-9~._\-/]+$/, "config_repo.path must not contain shell-special characters ($, `, \", ', \\, etc.)").default("~/.switchroom-config").describe("Filesystem path to the operator's private config repo. Must be a git " + "repo (else `switchroom doctor` FAILs). Tilde-expanded at read time."),
11898
+ push: exports_external.boolean().default(true).describe("When true, `config-repo sync` pushes after committing (subject to the " + "`require_private` gate). When false, it commits locally only — useful " + "for offline hosts or a review-before-push workflow."),
11899
+ remote: exports_external.string().min(1).default("origin").describe("Git remote name to push to. Default `origin`."),
11900
+ require_private: exports_external.boolean().default(true).describe("Refuse to PUSH unless the GitHub API confirms the remote repo is " + "private. On a public remote — or when the API is unreachable — the " + "push is skipped (commits still land locally) and a WARN is emitted. " + "Fail-safe against exfiltrating memory files / workspace state to a " + "repo that has been flipped public. Default true.")
11901
+ });
11882
11902
  SwitchroomConfigSchema = exports_external.object({
11883
11903
  switchroom: exports_external.object({
11884
11904
  version: exports_external.literal(1).describe("Config schema version"),
@@ -11891,6 +11911,7 @@ var init_schema = __esm(() => {
11891
11911
  memory: MemoryBackendConfigSchema.optional(),
11892
11912
  hindsight: HindsightConfigSchema.optional().describe("Fleet-singleton Hindsight (memory backend) configuration. Currently " + "just the LLM knob (provider + model) used for retain/reflect/" + "consolidation. Both fields optional; when unset the container falls " + "back to provider=claude-code + the hard-coded HINDSIGHT_DEFAULT_MODEL " + "so nothing changes for operators who don't set it. Read at container " + "launch by startHindsight() (src/setup/hindsight.ts) — takes effect on " + "the next `switchroom apply` / `memory setup --recreate`."),
11893
11913
  vault: VaultConfigSchema.optional(),
11914
+ config_repo: ConfigRepoConfigSchema.optional().describe("Change control for the operator's private ~/.switchroom-config git repo " + "(backup + audit trail of live host config, agent workspace state, and " + "mirrored personal skills). Consumed by `switchroom config-repo sync` and " + "the `config repo` doctor check. Optional and OFF by default."),
11894
11915
  auth: exports_external.object({
11895
11916
  active: exports_external.string().min(1).optional().describe("Fleet-wide active Anthropic account label. Every agent without " + "an explicit `agent.auth.override` uses this account. See " + "docs/auth.md for the full model. Set by `switchroom auth use <label>`."),
11896
11917
  fallback_order: exports_external.array(exports_external.string().min(1)).optional().describe("Ordered list of account labels for `switchroom auth rotate` to cycle " + "through when the active account hits a quota event. First entry is " + "normally the same as `auth.active`. When unset, `rotate` is a no-op."),
@@ -11958,6 +11979,48 @@ var init_schema = __esm(() => {
11958
11979
  for (const [name, a] of Object.entries(cfg.agents ?? {})) {
11959
11980
  checkServes(a.serves, ["agents", name, "serves"]);
11960
11981
  }
11982
+ for (const [name, a] of Object.entries(cfg.agents ?? {})) {
11983
+ const agentAuth = a.auth;
11984
+ if (!agentAuth?.exclusive || !agentAuth.override)
11985
+ continue;
11986
+ const acct = agentAuth.override;
11987
+ if (cfg.auth?.active === acct) {
11988
+ ctx.addIssue({
11989
+ code: exports_external.ZodIssueCode.custom,
11990
+ message: `account '${acct}' is exclusive to agent '${name}' ` + `(agents.${name}.auth.exclusive) — it cannot be the fleet \`auth.active\``,
11991
+ path: ["auth", "active"]
11992
+ });
11993
+ }
11994
+ const fbIdx = (cfg.auth?.fallback_order ?? []).indexOf(acct);
11995
+ if (fbIdx !== -1) {
11996
+ ctx.addIssue({
11997
+ code: exports_external.ZodIssueCode.custom,
11998
+ message: `account '${acct}' is exclusive to agent '${name}' ` + `(agents.${name}.auth.exclusive) — it cannot appear in \`auth.fallback_order\``,
11999
+ path: ["auth", "fallback_order", fbIdx]
12000
+ });
12001
+ }
12002
+ for (const [other, oa] of Object.entries(cfg.agents ?? {})) {
12003
+ if (other === name)
12004
+ continue;
12005
+ const otherOverride = oa.auth?.override;
12006
+ if (otherOverride === acct) {
12007
+ ctx.addIssue({
12008
+ code: exports_external.ZodIssueCode.custom,
12009
+ message: `account '${acct}' is exclusive to agent '${name}' ` + `(agents.${name}.auth.exclusive) — agent '${other}' cannot pin it`,
12010
+ path: ["agents", other, "auth", "override"]
12011
+ });
12012
+ }
12013
+ }
12014
+ (cfg.auth?.consumers ?? []).forEach((c, i) => {
12015
+ if (c.account === acct) {
12016
+ ctx.addIssue({
12017
+ code: exports_external.ZodIssueCode.custom,
12018
+ message: `account '${acct}' is exclusive to agent '${name}' ` + `(agents.${name}.auth.exclusive) — consumer '${c.name}' cannot pin it`,
12019
+ path: ["auth", "consumers", i, "account"]
12020
+ });
12021
+ }
12022
+ });
12023
+ }
11961
12024
  const microsoftAccounts = cfg.microsoft_accounts;
11962
12025
  for (const [name, a] of Object.entries(cfg.agents ?? {})) {
11963
12026
  const mw = a.microsoft_workspace;
@@ -21493,7 +21556,7 @@ function allocateAgentUid(name) {
21493
21556
  }
21494
21557
 
21495
21558
  // src/build-info.ts
21496
- var VERSION = "0.20.9";
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.9",
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": {
@@ -1692,6 +1692,55 @@ if ! ls -d "$sr_cb_dir"/chromium-*/ >/dev/null 2>&1; then
1692
1692
  fi
1693
1693
  unset sr_cb_dir
1694
1694
 
1695
+ # Playwright browsers cache seeding. The agent image bakes the fleet-default
1696
+ # Playwright version's chromium at /opt/playwright/browsers (immutable:
1697
+ # root-owned inside a `read_only: true` root fs), but PLAYWRIGHT_BROWSERS_PATH
1698
+ # points at the persistent per-agent HOME cache (Dockerfile.agent ENV) so a
1699
+ # project pinning a DIFFERENT Playwright version can `playwright install` its
1700
+ # matching browser revision — revision-keyed dirs coexist side by side, and
1701
+ # the install survives container recreate via the HOME bind mount. Symlink
1702
+ # each baked revision into the cache so the fleet default (@playwright/mcp,
1703
+ # webapp-testing skill) still resolves with ZERO download; a real
1704
+ # agent-installed dir with the same name always wins over the link. Dangling
1705
+ # links into the bake (stale after an image bump changed the default
1706
+ # revision) are swept first. Dot-entries (.links) are untouched by the glob —
1707
+ # they must stay real + writable so project installs can register there.
1708
+ #
1709
+ # Zero-download depends on TWO things, not just this seeding. (1) Playwright
1710
+ # decides "already installed / skip download" purely by the INSTALLATION_COMPLETE
1711
+ # marker inside each revision dir (playwright-core browserFetcher.ts) — which
1712
+ # resolves fine through these symlinks, so the seed alone is enough at launch.
1713
+ # (2) BUT `playwright install` first GC-sweeps the browsers dir, deleting any
1714
+ # revision not referenced by a `.links/<sha1>` entry. The baked packages
1715
+ # registered their reference in /opt/.../.links at build time, not here, so a
1716
+ # skewed-version project install would otherwise GC these seed links and force a
1717
+ # re-download. Dockerfile.agent sets PLAYWRIGHT_SKIP_BROWSER_GC=1 in the runtime
1718
+ # env to disable that sweep — keep the two in lockstep. (`.links` is still
1719
+ # written by every install; we just never run the removal pass.)
1720
+ if [ -d /opt/playwright/browsers ]; then
1721
+ sr_pw_cache="${PLAYWRIGHT_BROWSERS_PATH:-$HOME/.cache/ms-playwright}"
1722
+ mkdir -p "$sr_pw_cache" 2>/dev/null || true
1723
+ if [ -d "$sr_pw_cache" ] && [ -w "$sr_pw_cache" ]; then
1724
+ for sr_pw_link in "$sr_pw_cache"/*; do
1725
+ if [ -L "$sr_pw_link" ] && [ ! -e "$sr_pw_link" ]; then
1726
+ case "$(readlink "$sr_pw_link")" in
1727
+ /opt/playwright/browsers/*) rm -f "$sr_pw_link" ;;
1728
+ esac
1729
+ fi
1730
+ done
1731
+ for sr_pw_rev in /opt/playwright/browsers/*/; do
1732
+ [ -d "$sr_pw_rev" ] || continue
1733
+ sr_pw_name="$(basename "$sr_pw_rev")"
1734
+ if [ ! -e "$sr_pw_cache/$sr_pw_name" ]; then
1735
+ ln -s "${sr_pw_rev%/}" "$sr_pw_cache/$sr_pw_name" 2>/dev/null || true
1736
+ fi
1737
+ done
1738
+ else
1739
+ echo "WARNING (playwright cache): $sr_pw_cache missing or not writable — baked browsers not linked; playwright-based skills may attempt a fresh ~150MB download" >&2
1740
+ fi
1741
+ unset sr_pw_cache sr_pw_rev sr_pw_name sr_pw_link
1742
+ fi
1743
+
1695
1744
  # LiteLLM routing (opt-in, #litellm). When SWITCHROOM_LITELLM is set (compose
1696
1745
  # env, gated on litellm.enabled && keyConfirmed), route the unmodified `claude`
1697
1746
  # CLI through the operator's LiteLLM proxy at ANTHROPIC_BASE_URL: fetch the
@@ -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 GFMset `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 reachablethe 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