switchroom 0.8.1 → 0.11.0

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 (137) hide show
  1. package/README.md +54 -61
  2. package/bin/timezone-hook.sh +9 -7
  3. package/dist/agent-scheduler/index.js +285 -45
  4. package/dist/auth-broker/index.js +13932 -0
  5. package/dist/cli/drive-write-pretool.mjs +5418 -0
  6. package/dist/cli/switchroom.js +8890 -5560
  7. package/dist/host-control/main.js +582 -43
  8. package/dist/vault/approvals/kernel-server.js +276 -47
  9. package/dist/vault/broker/server.js +333 -69
  10. package/examples/minimal.yaml +63 -0
  11. package/examples/personal-google-workspace-mcp/.env.example +34 -0
  12. package/examples/personal-google-workspace-mcp/README.md +194 -0
  13. package/examples/personal-google-workspace-mcp/compose.yaml +66 -0
  14. package/examples/switchroom.yaml +220 -0
  15. package/package.json +6 -4
  16. package/profiles/_base/start.sh.hbs +3 -3
  17. package/profiles/_shared/agent-self-service.md.hbs +126 -0
  18. package/profiles/default/CLAUDE.md +10 -0
  19. package/profiles/default/CLAUDE.md.hbs +16 -0
  20. package/skills/buildkite-agent-infrastructure/SKILL.md +30 -11
  21. package/skills/buildkite-agent-runtime/SKILL.md +44 -11
  22. package/skills/buildkite-api/SKILL.md +31 -8
  23. package/skills/buildkite-cli/SKILL.md +27 -9
  24. package/skills/buildkite-migration/SKILL.md +22 -9
  25. package/skills/buildkite-pipelines/SKILL.md +26 -9
  26. package/skills/buildkite-secure-delivery/SKILL.md +23 -9
  27. package/skills/buildkite-test-engine/SKILL.md +25 -8
  28. package/skills/docx/SKILL.md +1 -1
  29. package/skills/file-bug/SKILL.md +34 -6
  30. package/skills/humanizer/SKILL.md +15 -0
  31. package/skills/humanizer-calibrate/SKILL.md +7 -1
  32. package/skills/mcp-builder/SKILL.md +1 -1
  33. package/skills/pdf/SKILL.md +1 -1
  34. package/skills/pptx/SKILL.md +1 -1
  35. package/skills/skill-creator/SKILL.md +21 -1
  36. package/skills/skill-creator/scripts/__pycache__/__init__.cpython-313.pyc +0 -0
  37. package/skills/skill-creator/scripts/__pycache__/generate_report.cpython-313.pyc +0 -0
  38. package/skills/skill-creator/scripts/__pycache__/improve_description.cpython-313.pyc +0 -0
  39. package/skills/skill-creator/scripts/__pycache__/run_eval.cpython-313.pyc +0 -0
  40. package/skills/skill-creator/scripts/__pycache__/run_loop.cpython-313.pyc +0 -0
  41. package/skills/skill-creator/scripts/__pycache__/utils.cpython-313.pyc +0 -0
  42. package/skills/switchroom-cli/SKILL.md +63 -64
  43. package/skills/switchroom-health/SKILL.md +23 -10
  44. package/skills/switchroom-install/SKILL.md +3 -3
  45. package/skills/switchroom-manage/SKILL.md +26 -19
  46. package/skills/switchroom-runtime/SKILL.md +67 -15
  47. package/skills/switchroom-status/SKILL.md +26 -1
  48. package/skills/telegram-test-harness/SKILL.md +3 -0
  49. package/skills/webapp-testing/SKILL.md +31 -1
  50. package/skills/xlsx/SKILL.md +1 -1
  51. package/telegram-plugin/admin-commands/dispatch.test.ts +1 -1
  52. package/telegram-plugin/admin-commands/index.ts +9 -5
  53. package/telegram-plugin/auth-snapshot-format.ts +612 -0
  54. package/telegram-plugin/auto-fallback-fleet.ts +215 -0
  55. package/telegram-plugin/auto-fallback.ts +28 -301
  56. package/telegram-plugin/dist/gateway/gateway.js +17453 -15100
  57. package/telegram-plugin/fleet-fallback-gate.ts +105 -0
  58. package/telegram-plugin/gateway/approval-callback.test.ts +104 -0
  59. package/telegram-plugin/gateway/approval-callback.ts +31 -3
  60. package/telegram-plugin/gateway/auth-add-flow.ts +326 -0
  61. package/telegram-plugin/gateway/auth-broker-client.ts +75 -0
  62. package/telegram-plugin/gateway/auth-command.ts +905 -0
  63. package/telegram-plugin/gateway/auth-line.ts +123 -0
  64. package/telegram-plugin/gateway/auth-status-adapter.ts +101 -0
  65. package/telegram-plugin/gateway/boot-card.ts +23 -37
  66. package/telegram-plugin/gateway/boot-probes.ts +9 -12
  67. package/telegram-plugin/gateway/diff-preview-card.test.ts +192 -0
  68. package/telegram-plugin/gateway/diff-preview-card.ts +170 -0
  69. package/telegram-plugin/gateway/drive-write-approval.test.ts +312 -0
  70. package/telegram-plugin/gateway/drive-write-approval.ts +243 -0
  71. package/telegram-plugin/gateway/folder-picker-handler.test.ts +314 -0
  72. package/telegram-plugin/gateway/folder-picker-handler.ts +348 -0
  73. package/telegram-plugin/gateway/gateway.ts +1156 -938
  74. package/telegram-plugin/gateway/hostd-dispatch.ts +244 -0
  75. package/telegram-plugin/gateway/ipc-protocol.ts +83 -2
  76. package/telegram-plugin/gateway/ipc-server.ts +69 -0
  77. package/telegram-plugin/hooks/sandbox-hint-posttool.mjs +103 -12
  78. package/telegram-plugin/hooks/tool-label-pretool.mjs +11 -0
  79. package/telegram-plugin/hooks/wedge-detect-posttool.mjs +303 -0
  80. package/telegram-plugin/model-unavailable.ts +28 -12
  81. package/telegram-plugin/permission-title.ts +56 -0
  82. package/telegram-plugin/quota-check.ts +19 -41
  83. package/telegram-plugin/scripts/build.mjs +0 -1
  84. package/telegram-plugin/shared/bot-runtime.ts +5 -4
  85. package/telegram-plugin/silence-poke.ts +153 -1
  86. package/telegram-plugin/tests/auth-add-flow.test.ts +559 -0
  87. package/telegram-plugin/tests/auth-code-redact.test.ts +8 -4
  88. package/telegram-plugin/tests/auth-command-format2.test.ts +156 -0
  89. package/telegram-plugin/tests/auth-command-vernacular.test.ts +531 -0
  90. package/telegram-plugin/tests/auth-snapshot-format.test.ts +429 -0
  91. package/telegram-plugin/tests/auth-status-adapter.test.ts +129 -0
  92. package/telegram-plugin/tests/auto-fallback-fleet.test.ts +211 -0
  93. package/telegram-plugin/tests/auto-fallback.test.ts +60 -358
  94. package/telegram-plugin/tests/boot-probes.test.ts +27 -22
  95. package/telegram-plugin/tests/fleet-fallback-gate.test.ts +197 -0
  96. package/telegram-plugin/tests/model-unavailable.test.ts +30 -5
  97. package/telegram-plugin/tests/permission-title.test.ts +31 -0
  98. package/telegram-plugin/tests/quota-check.test.ts +5 -35
  99. package/telegram-plugin/tests/sandbox-hint-posttool.test.ts +212 -2
  100. package/telegram-plugin/tests/silence-poke.test.ts +237 -0
  101. package/telegram-plugin/tests/turn-flush-safety.test.ts +112 -0
  102. package/telegram-plugin/turn-flush-safety.ts +55 -1
  103. package/telegram-plugin/uat/SETUP.md +35 -1
  104. package/telegram-plugin/uat/runners/agent-self-sufficiency.ts +457 -0
  105. package/telegram-plugin/uat/runners/paraphrases.ts +231 -0
  106. package/telegram-plugin/uat/runners/report.ts +150 -0
  107. package/telegram-plugin/uat/runners/run-agent-self-sufficiency.sh +50 -0
  108. package/telegram-plugin/uat/runners/scorer.test.ts +196 -0
  109. package/telegram-plugin/uat/runners/scorer.ts +106 -0
  110. package/telegram-plugin/uat/runners/skill-coverage.test.ts +100 -0
  111. package/telegram-plugin/uat/runners/skill-coverage.ts +620 -0
  112. package/telegram-plugin/uat/scenarios/jtbd-interrupt-marker-dm.test.ts +7 -1
  113. package/telegram-plugin/uat/scenarios/jtbd-rapid-followup-dm.test.ts +7 -1
  114. package/telegram-plugin/auth-dashboard.ts +0 -1104
  115. package/telegram-plugin/auth-slot-parser.ts +0 -497
  116. package/telegram-plugin/auto-fallback-dispatcher.ts +0 -68
  117. package/telegram-plugin/dist/foreman/foreman.js +0 -31358
  118. package/telegram-plugin/foreman/foreman-create-flow.ts +0 -202
  119. package/telegram-plugin/foreman/foreman-handlers.ts +0 -493
  120. package/telegram-plugin/foreman/foreman.ts +0 -1165
  121. package/telegram-plugin/foreman/setup-flow.ts +0 -345
  122. package/telegram-plugin/foreman/setup-state.ts +0 -239
  123. package/telegram-plugin/foreman/state.ts +0 -203
  124. package/telegram-plugin/tests/auth-account-identity-surface.test.ts +0 -118
  125. package/telegram-plugin/tests/auth-dashboard-edge-cases.test.ts +0 -260
  126. package/telegram-plugin/tests/auth-dashboard-restart-flow.test.ts +0 -140
  127. package/telegram-plugin/tests/auth-dashboard-v3b.test.ts +0 -559
  128. package/telegram-plugin/tests/auth-dashboard.test.ts +0 -1045
  129. package/telegram-plugin/tests/auth-slot-commands.test.ts +0 -640
  130. package/telegram-plugin/tests/auto-fallback-dispatcher.e2e.test.ts +0 -183
  131. package/telegram-plugin/tests/boot-card-account-quota.test.ts +0 -137
  132. package/telegram-plugin/tests/foreman-create-flow.test.ts +0 -359
  133. package/telegram-plugin/tests/foreman-handlers.test.ts +0 -347
  134. package/telegram-plugin/tests/foreman-state.test.ts +0 -164
  135. package/telegram-plugin/tests/foreman-write-ops.test.ts +0 -214
  136. package/telegram-plugin/tests/setup-flow.test.ts +0 -510
  137. package/telegram-plugin/tests/setup-state.test.ts +0 -146
@@ -10948,7 +10948,7 @@ var init_dist = __esm(() => {
10948
10948
  });
10949
10949
 
10950
10950
  // src/config/schema.ts
10951
- var CodeRepoEntrySchema, AgentBindMountSchema, ScheduleEntrySchema, AgentSoulSchema, AgentToolsSchema, AgentMemorySchema, HookEntrySchema, AgentHooksSchema, SubagentSchema, SessionSchema, SessionContinuitySchema, TelegramChannelSchema, ChannelsSchema, TIMEZONE_REGEX, ApproverIdSchema, DriveConfigSchema, AgentDriveConfigSchema, ReactionsSchema, profileFields, ProfileSchema, _omitExtends, defaultsFields, AgentDefaultsSchema, AgentSchema, TelegramConfigSchema, MemoryBackendConfigSchema, VaultConfigSchema, QuotaConfigSchema, HostControlConfigSchema, SwitchroomConfigSchema;
10951
+ var CodeRepoEntrySchema, AgentBindMountSchema, ScheduleEntrySchema, AgentSoulSchema, AgentToolsSchema, AgentMemorySchema, HookEntrySchema, AgentHooksSchema, SubagentSchema, SessionSchema, SessionContinuitySchema, TelegramChannelSchema, ChannelsSchema, TIMEZONE_REGEX, ApproverIdSchema, GoogleWorkspaceTierSchema, GoogleWorkspaceConfigSchema, AgentGoogleWorkspaceConfigSchema, ReactionsSchema, profileFields, ProfileSchema, _omitExtends, defaultsFields, AgentDefaultsSchema, AgentSchema, TelegramConfigSchema, MemoryBackendConfigSchema, VaultConfigSchema, QuotaConfigSchema, HostControlConfigSchema, SwitchroomConfigSchema;
10952
10952
  var init_schema = __esm(() => {
10953
10953
  init_zod();
10954
10954
  CodeRepoEntrySchema = exports_external.object({
@@ -11004,7 +11004,7 @@ var init_schema = __esm(() => {
11004
11004
  SessionEnd: exports_external.array(HookEntrySchema).optional()
11005
11005
  }).catchall(exports_external.array(HookEntrySchema)).optional();
11006
11006
  SubagentSchema = exports_external.object({
11007
- description: exports_external.string().describe("When the main agent should delegate to this sub-agent"),
11007
+ description: exports_external.string().optional().describe("When the main agent should delegate to this sub-agent"),
11008
11008
  model: exports_external.string().optional().describe("Model: 'sonnet', 'opus', 'haiku', full ID, or 'inherit' (default)"),
11009
11009
  background: exports_external.boolean().optional().describe("Run in background by default (non-blocking). Default false"),
11010
11010
  isolation: exports_external.enum(["worktree"]).optional().describe("'worktree' gives the sub-agent its own git branch"),
@@ -11083,13 +11083,20 @@ var init_schema = __esm(() => {
11083
11083
  }).optional();
11084
11084
  TIMEZONE_REGEX = /^UTC$|^[A-Z][A-Za-z0-9_+-]+(\/[A-Z][A-Za-z0-9_+-]+){1,2}$/;
11085
11085
  ApproverIdSchema = exports_external.union([exports_external.number(), exports_external.string().regex(/^\d+$/)]);
11086
- DriveConfigSchema = exports_external.object({
11086
+ GoogleWorkspaceTierSchema = exports_external.enum([
11087
+ "core",
11088
+ "extended",
11089
+ "complete"
11090
+ ]);
11091
+ GoogleWorkspaceConfigSchema = exports_external.object({
11087
11092
  google_client_id: exports_external.string().min(1).describe("Google OAuth client ID (literal string or vault reference e.g. 'vault:google-oauth-client-id')"),
11088
11093
  google_client_secret: exports_external.string().min(1).describe("Google OAuth client secret (literal string or vault reference e.g. 'vault:google-oauth-client-secret')"),
11089
- approvers: exports_external.array(ApproverIdSchema).min(1).describe("Array of numeric Telegram user IDs authorized to approve drive onboarding. " + "At least one must be specified.")
11094
+ approvers: exports_external.array(ApproverIdSchema).min(1).describe("Array of numeric Telegram user IDs authorized to approve drive onboarding. " + "At least one must be specified."),
11095
+ tier: GoogleWorkspaceTierSchema.optional().describe("RFC G Phase 1: which upstream MCP tier to expose. " + "core (default) = ~16 tools (Drive+Docs+Sheets+Calendar). " + "extended = ~40 tools (+Slides, Forms, Tasks, Chat). " + "complete = ~60+ tools (+Gmail; not recommended yet — see RFC G §5).")
11090
11096
  }).optional();
11091
- AgentDriveConfigSchema = exports_external.object({
11092
- approvers: exports_external.array(ApproverIdSchema).min(1).optional().describe("Per-agent approver override. When set, replaces (does not extend) " + "the top-level drive.approvers list for this agent's onboarding card.")
11097
+ AgentGoogleWorkspaceConfigSchema = exports_external.object({
11098
+ approvers: exports_external.array(ApproverIdSchema).min(1).optional().describe("Per-agent approver override. When set, replaces (does not extend) " + "the top-level drive.approvers list for this agent's onboarding card."),
11099
+ tier: GoogleWorkspaceTierSchema.optional().describe("Per-agent tier override (RFC G Phase 1). When set, replaces the " + "top-level google_workspace.tier for this agent. Common case: most " + "agents on `core`, one specialist on `extended` for Slides access.")
11093
11100
  }).optional();
11094
11101
  ReactionsSchema = exports_external.object({
11095
11102
  enabled: exports_external.boolean().optional().describe("Master switch for the reaction-trigger path. When false, " + "reactions are still persisted via recordReaction but never " + "dispatched to the agent as synthetic inbound turns. Default true."),
@@ -11143,6 +11150,12 @@ var init_schema = __esm(() => {
11143
11150
  claude_md_raw: exports_external.string().optional(),
11144
11151
  cli_args: exports_external.array(exports_external.string()).optional(),
11145
11152
  extra_stable_files: exports_external.array(exports_external.string()).optional().describe("Extra filenames (relative to the agent's workspace directory) to append " + "to the stable bootstrap render. Loaded once at session start via " + "`--append-system-prompt`. Missing files are silently skipped. " + "Example: ['BRIEF.md', 'CONTEXT.md']."),
11153
+ resources: exports_external.object({
11154
+ memory: exports_external.string().regex(/^\d+(\.\d+)?[kmgKMG]?$/, "memory must be a Docker size string like '6g', '512m', '1.5g'").optional().describe("Hard memory cap (Docker `mem_limit` → cgroup memory.max). When the " + "container exceeds this, the kernel OOM-kills processes in the cgroup. " + "Format: '6g', '1.5g', '512m'. When unset at every cascade layer the " + "compose generator falls back to the hard-coded per-profile defaults " + "in src/agents/compose.ts (klanker 6g, coding 2g, conversational 1.5g, " + "lightweight 1g, default 1.5g)."),
11155
+ memory_reservation: exports_external.string().regex(/^\d+(\.\d+)?[kmgKMG]?$/, "memory_reservation must be a Docker size string like '4g', '256m'").optional().describe("Soft memory floor (Docker `mem_reservation` → cgroup memory.low). " + "Under host-wide memory pressure, the kernel protects at least this " + "much from being reclaimed from the cgroup. Must be ≤ memory. Use to " + "keep an agent RAM-resident when the host has other tenants that " + "might push the box (Coolify apps, build jobs). Default: unset."),
11156
+ pids_limit: exports_external.number().int().positive().optional().describe("Max processes the cgroup can spawn (cgroup pids.max). Prevents " + "fork bombs and runaway test runners. Counts every process in the " + "cgroup including bash subprocesses, claude itself, sidecars, and " + "any test/build worker. A typical agent at idle uses ~30 PIDs; " + "`npm test`-style workloads can spike to 200+. Set generously " + "(2000 is a comfortable cap for test-running agents). Default: " + "unset (no cgroup pid cap)."),
11157
+ cpus: exports_external.number().positive().optional().describe("CPU quota (Docker `cpus`). Fractional values OK (e.g. 0.5, 2.0). " + "When unset at every cascade layer the compose generator falls " + "back to the per-profile default (klanker/coding 2.0, default 1.0, " + "lightweight 0.5).")
11158
+ }).optional().describe("Per-agent resource limits. Cascades through defaults → profile → " + "per-agent with per-field merge (agent wins on each field independently). " + "Any field left unset at every layer falls back to the hard-coded " + "per-profile defaults in src/agents/compose.ts."),
11146
11159
  experimental: exports_external.object({
11147
11160
  legacy_pty: exports_external.boolean().optional().describe("Opt out of the default tmux supervisor (#725) and run the agent under " + "the legacy PTY supervisor instead. Default: false (tmux is the default)."),
11148
11161
  legacy_autoaccept_expect: exports_external.boolean().optional().describe("Opt the autoaccept gateway back into the legacy expect-script behaviour " + "instead of the tmux send-keys path. Default: false.")
@@ -11156,13 +11169,13 @@ var init_schema = __esm(() => {
11156
11169
  bot_token: exports_external.string().optional().describe("Per-agent Telegram bot token or vault reference (overrides global telegram.bot_token)"),
11157
11170
  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')."),
11158
11171
  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 " + "systemd unit's TZ= env."),
11159
- auth_label: exports_external.string().optional().describe("Human-readable identity for the session-start greeting (e.g. 'user@example.com'). " + "Anthropic does not expose a public user-profile endpoint for OAuth tokens, so the " + "email/account cannot be read locally; the user declares it here. Appears in the Auth " + "row as '✓ max · <label> · expires ...'."),
11160
11172
  auth: exports_external.object({
11161
- accounts: exports_external.array(exports_external.string()).optional().describe("Ordered list of Anthropic account labels (from `~/.switchroom/accounts/`) " + "this agent can use. The first non-quota-exhausted account is the active one; " + "subsequent entries are auto-fallback targets. switchroom-auth-broker keeps " + "`<agentDir>/.claude/credentials.json` in sync with the active account on " + "every refresh and on every quota event. When unset, the agent falls back to " + "a single 'default' account; if no `default` account exists, the boot self-test " + "surfaces a one-line nudge to run `switchroom auth account add`.")
11162
- }).optional().describe("Account routing for switchroom-auth-broker. See " + "reference/share-auth-across-the-fleet.md for the unit-of-authentication model."),
11173
+ 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.")
11174
+ }).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)."),
11163
11175
  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."),
11164
11176
  topic_name: exports_external.string().describe("Telegram forum topic display name"),
11165
11177
  topic_emoji: exports_external.string().optional().describe("Emoji for the topic (e.g., '\uD83C\uDFCB️')"),
11178
+ purpose: exports_external.string().max(140).optional().describe("One-line description of what this agent does (≤140 chars). Shown to " + "peer agents when they call the agent-config MCP `peers_list` tool, so " + "every agent on the instance can answer 'is there an agent that does X' " + "without baking the fleet into prompts. Sourced live from " + "switchroom.yaml — never memorized into Hindsight. Falls back to " + "`topic_name` when absent."),
11166
11179
  role: exports_external.enum(["assistant", "foreman"]).optional().describe("Agent role. Default (omitted) is `assistant` — a fleet agent doing " + "user-facing tasks. `foreman` opts the agent in to switchroom's bundled " + "operator skills (switchroom-architecture / cli / health / install / manage " + "/ status), auto-symlinked into the agent's .claude/skills/ on scaffold and " + "reconcile. Fleet agents (assistant role) get no operator skills; reconcile " + "actively retracts them if the role flips back. See docs/skills.md for the model."),
11167
11180
  topic_id: exports_external.number().optional().describe("Telegram topic thread ID (auto-populated by switchroom topics sync)"),
11168
11181
  webhook_sources: exports_external.array(exports_external.enum(["github", "generic"])).optional().describe("[DEPRECATED — moved to channels.telegram.webhook_sources in #596] " + "Old per-agent location. Still read but logs a deprecation warning. " + "See channels.telegram.webhook_sources for the canonical spot."),
@@ -11209,7 +11222,8 @@ var init_schema = __esm(() => {
11209
11222
  disallowed_tools: exports_external.array(exports_external.string()).optional().describe("Granular tool denylist passed verbatim to Claude Code's --disallowedTools " + "flag. Same pattern syntax as allowed_tools (e.g. 'Bash(rm *)'). See #199."),
11210
11223
  extra_stable_files: exports_external.array(exports_external.string()).optional().describe("Extra filenames (relative to the agent's workspace directory) to append " + "to the stable bootstrap render. Loaded once at session start via " + "`--append-system-prompt`. Missing files are silently skipped. " + "Example: ['BRIEF.md', 'CONTEXT.md']."),
11211
11224
  code_repos: exports_external.array(CodeRepoEntrySchema).optional().describe("Git repositories this agent is allowed to claim worktrees from. " + "Each entry provides a short name alias, a source path, and an " + "optional concurrency cap (default 5). When code_repos is set, " + "claim_worktree accepts the alias as the repo argument. " + "Absolute paths may always be passed regardless of this list."),
11212
- drive: AgentDriveConfigSchema.describe("Per-agent drive onboarding overrides (currently just approvers). " + "When set, replaces the top-level drive.approvers list for this agent. " + "google_client_id/secret are not per-agent — they live at the top level."),
11225
+ drive: AgentGoogleWorkspaceConfigSchema.describe("RFC D legacy key — use `google_workspace:` instead. Per-agent " + "google_workspace overrides (currently approvers + tier). When set, " + "replaces the top-level approvers list for this agent. " + "google_client_id/secret are not per-agent — they live at the top level."),
11226
+ google_workspace: AgentGoogleWorkspaceConfigSchema.describe("RFC G canonical key. Per-agent Google Workspace overrides — currently " + "approvers (replaces, does not extend the top-level list) and tier " + "(`core` | `extended` | `complete`, replaces top-level default). " + "google_client_id/secret are not per-agent — they live at the top level. " + "Mutually exclusive with `drive:` on the same agent (loader fails fast " + "if both are set)."),
11213
11227
  repos: exports_external.record(exports_external.string().regex(/^[a-z0-9][a-z0-9-]*$/, "Repo slug must be kebab-case ASCII: start with a lowercase letter or digit, contain only lowercase letters, digits, and hyphens"), exports_external.object({
11214
11228
  url: exports_external.string().min(1).describe("Git remote URL for the repo (e.g. 'git@github.com:org/repo.git' or " + "'https://github.com/org/repo.git'). Used verbatim for git clone."),
11215
11229
  branch_default: exports_external.string().optional().describe("Default branch to track (defaults to the remote's HEAD, typically 'main'). " + "The per-agent branch 'agent/<agentName>/main' fast-forwards to this branch " + "when the worktree is clean on session start.")
@@ -11217,7 +11231,13 @@ var init_schema = __esm(() => {
11217
11231
  experimental: exports_external.object({
11218
11232
  legacy_pty: exports_external.boolean().optional().describe("Opt out of the default tmux supervisor (#725) and run the agent " + "under the legacy PTY supervisor instead. Default: false."),
11219
11233
  legacy_autoaccept_expect: exports_external.boolean().optional().describe("Opt the autoaccept gateway back into the legacy expect-script " + "behaviour instead of the tmux send-keys path. Default: false.")
11220
- }).optional().describe("Opt-in flags for experimental / legacy behaviours. Cascades through " + "defaults → profile → per-agent.")
11234
+ }).optional().describe("Opt-in flags for experimental / legacy behaviours. Cascades through " + "defaults → profile → per-agent."),
11235
+ resources: exports_external.object({
11236
+ memory: exports_external.string().regex(/^\d+(\.\d+)?[kmgKMG]?$/).optional(),
11237
+ memory_reservation: exports_external.string().regex(/^\d+(\.\d+)?[kmgKMG]?$/).optional(),
11238
+ pids_limit: exports_external.number().int().positive().optional(),
11239
+ cpus: exports_external.number().positive().optional()
11240
+ }).optional()
11221
11241
  });
11222
11242
  TelegramConfigSchema = exports_external.object({
11223
11243
  bot_token: exports_external.string().describe("Telegram bot token or vault reference (e.g., 'vault:telegram-bot-token')"),
@@ -11258,7 +11278,7 @@ var init_schema = __esm(() => {
11258
11278
  monthly_budget_usd: exports_external.number().positive().optional().describe("Monthly USD spend budget. If unset, the greeting shows raw usage only.")
11259
11279
  });
11260
11280
  HostControlConfigSchema = exports_external.object({
11261
- enabled: exports_external.boolean().optional().describe("Opt-in to the host-control daemon. Default: false (Phase 1). " + "When true, the compose generator emits per-agent bind mounts " + "at `~/.switchroom/hostd/<name>/sock` for every admin-flagged " + "agent. The daemon itself is installed by `switchroom setup` " + "as a systemd user unit. systemd hosts only compose-mode " + "support is deferred to a v2 RFC (see RFC C §5.1 for why). " + "Gateway integration (swap of spawnSwitchroomDetached callsites) " + "lands in a Phase 2 follow-up PR; setting enabled: true in " + "Phase 1 ships the daemon but does not change gateway behavior.")
11281
+ enabled: exports_external.boolean().optional().describe("Opt-in to the host-control daemon. Default: false. " + "When true, the compose generator emits per-agent bind mounts " + "at `~/.switchroom/hostd/<name>/sock` for every admin-flagged " + "agent. Install the daemon with `switchroom hostd install` " + "it runs as a docker container in its own compose project " + "(`switchroom-hostd`), separate from the agent fleet's compose " + "project so `up -d --remove-orphans` cycles of the fleet " + "can't recreate the daemon mid-RPC. See RFC C §5.1. " + "Since Phase 2 (#1175 PR γ) the gateway's /restart, /new, /reset, " + "and /update apply slash-commands automatically dispatch through " + "hostd when enabled replacing the in-container " + "`spawnSwitchroomDetached` shellout that requires docker access. " + "Set enabled: true on docker-mode installs to make those verbs work " + "(they otherwise fail because the agent container has no docker " + "binary/socket).")
11262
11282
  });
11263
11283
  SwitchroomConfigSchema = exports_external.object({
11264
11284
  switchroom: exports_external.object({
@@ -11270,9 +11290,28 @@ var init_schema = __esm(() => {
11270
11290
  telegram: TelegramConfigSchema,
11271
11291
  memory: MemoryBackendConfigSchema.optional(),
11272
11292
  vault: VaultConfigSchema.optional(),
11273
- drive: DriveConfigSchema.describe("Optional drive onboarding configuration. When set, supplies Google " + "OAuth client credentials and the approver allowlist for `switchroom " + "drive connect`. Env vars (SWITCHROOM_GOOGLE_CLIENT_ID, " + "SWITCHROOM_GOOGLE_CLIENT_SECRET, SWITCHROOM_APPROVER_USER_ID) take " + "precedence over this block when set, preserving back-compat with " + "the env-only flow shipped in #766."),
11293
+ auth: exports_external.object({
11294
+ 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>`."),
11295
+ 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."),
11296
+ consumers: exports_external.array(exports_external.object({
11297
+ name: exports_external.string().regex(/^[a-zA-Z0-9][a-zA-Z0-9_-]*$/, {
11298
+ message: "Consumer name must be a path-safe slug (letters, digits, underscore, hyphen)"
11299
+ }).describe("Socket-path identity; binds at /run/switchroom/auth-broker/<name>/sock"),
11300
+ account: exports_external.string().min(1).describe("Pinned account label for this consumer. `get-credentials` returns " + "this account's credentials; `mark-exhausted` from this consumer " + "only affects this account."),
11301
+ uid: exports_external.number().int().nonnegative().optional().describe("Optional UID to chown the consumer socket to (defaults to 0 = root, " + "suitable for sibling containers running as root).")
11302
+ })).optional().describe("Non-agent peers that hold a broker socket (RFC H §4.8). Each gets " + "its own `/run/switchroom/auth-broker/<name>/sock` chowned to its UID. " + "Consumers cannot be admins; a consumer name that collides with an " + "agent (whether that agent has `admin: true` or not) is a config " + "error caught at schema validation.")
11303
+ }).optional().describe("Switchroom-auth-broker configuration (RFC H). Fleet-wide active account, " + "fallback order, admin-agent ACL, and ephemeral-consumer surface. " + "Required from the v0.8+ schema onwards; pre-v0.8 fleets are migrated " + "in-place by `switchroom apply` (see src/auth/migrate-schema.ts)."),
11304
+ drive: GoogleWorkspaceConfigSchema.describe("RFC D legacy key — use `google_workspace:` instead. Optional Google " + "Workspace onboarding configuration. When set, supplies Google OAuth " + "client credentials, the approver allowlist for `switchroom drive " + "connect`, and the optional tier knob. Env vars " + "(SWITCHROOM_GOOGLE_CLIENT_ID, SWITCHROOM_GOOGLE_CLIENT_SECRET, " + "SWITCHROOM_APPROVER_USER_ID) take precedence over this block when " + "set, preserving back-compat with the env-only flow shipped in #766."),
11305
+ google_workspace: GoogleWorkspaceConfigSchema.describe("RFC G canonical key. Top-level Google Workspace configuration — " + "OAuth client credentials, approver allowlist, and tier knob (`core` " + "| `extended` | `complete`, default `core`). Mutually exclusive with " + "`drive:` at the top level (loader fails fast if both are set)."),
11274
11306
  quota: QuotaConfigSchema.optional().describe("Optional weekly/monthly USD spend budgets rendered in the session " + "greeting. Usage is read from ccusage at runtime; no network calls."),
11275
11307
  host_control: HostControlConfigSchema.optional().describe("Optional host-control daemon configuration. See RFC C " + "(docs/rfcs/host-control-daemon.md) and the field-level help on " + "`enabled` for the Phase 1 scope."),
11308
+ google_accounts: exports_external.record(exports_external.string().regex(/^[^@\s:]+@[^@\s:]+\.[^@\s:]+$/, {
11309
+ message: "Account key must be a Google account email like 'alice@example.com' (colons not allowed)"
11310
+ }).transform((v) => v.trim().toLowerCase()), exports_external.object({
11311
+ enabled_for: exports_external.array(exports_external.string().regex(/^[a-z0-9][a-z0-9_-]{0,50}$/, {
11312
+ message: "Agent name must match the standard agent-name pattern"
11313
+ })).describe("Agent slugs that may read this account's vault slots " + "(`google:<account>:refresh_token` etc). Per-agent ACL is " + "enforced at the broker, not at the agent identity layer — " + "the agent still authenticates via socket-path-as-identity " + "per RFC D §4.1, broker just gates the cross-agent token share.")
11314
+ })).optional().describe("RFC G Phase 2: per-Google-account ACL for vault slots holding " + "OAuth refresh tokens. Maps account email → list of agents " + "permitted to read that account's slots. Written by `switchroom " + "auth google enable|disable` (Phase 3); read by the broker on " + "every Google slot access. Replaces RFC D's per-agent vault slot " + "scope (which can't express 'two agents share one Google account')."),
11276
11315
  defaults: AgentDefaultsSchema.describe("Implicit bottom-of-cascade profile applied to every agent before " + "per-agent config and `extends:` resolution. Tools, mcp_servers, and " + "schedule are unioned/concatenated; scalars and nested objects are " + "shallow-merged with per-agent values winning."),
11277
11316
  profiles: exports_external.record(exports_external.string(), ProfileSchema).optional().describe("Named profile definitions. Agents reference via `extends: <name>`. " + "Inline profiles declared here take priority over filesystem " + "profiles/<name>/ directories when both exist."),
11278
11317
  agents: exports_external.record(exports_external.string().regex(/^[a-z0-9][a-z0-9_-]{0,50}$/, {
@@ -11307,6 +11346,142 @@ function resolveDualPath(pathStr) {
11307
11346
  var DEFAULT_STATE_DIR = ".switchroom", LEGACY_STATE_DIR = ".clerk";
11308
11347
  var init_paths = () => {};
11309
11348
 
11349
+ // src/config/overlay-schema.ts
11350
+ var OverlayDocSchema;
11351
+ var init_overlay_schema = __esm(() => {
11352
+ init_zod();
11353
+ init_schema();
11354
+ OverlayDocSchema = exports_external.object({
11355
+ schedule: exports_external.array(ScheduleEntrySchema).optional(),
11356
+ skills: exports_external.array(exports_external.string()).optional()
11357
+ }).strict();
11358
+ });
11359
+
11360
+ // src/config/overlay-loader.ts
11361
+ import { existsSync as existsSync2, readFileSync, readdirSync, statSync } from "node:fs";
11362
+ import { resolve as resolve2 } from "node:path";
11363
+ function overlayDirFor(agentName, subdir) {
11364
+ const base = resolveDualPath(`~/.switchroom/agents/${agentName}/${subdir}`);
11365
+ return resolve2(base);
11366
+ }
11367
+ function listYamlFiles(dir) {
11368
+ if (!existsSync2(dir))
11369
+ return [];
11370
+ let entries;
11371
+ try {
11372
+ entries = readdirSync(dir);
11373
+ } catch {
11374
+ return [];
11375
+ }
11376
+ const out = [];
11377
+ for (const name of entries) {
11378
+ if (!/\.ya?ml$/i.test(name))
11379
+ continue;
11380
+ const full = resolve2(dir, name);
11381
+ try {
11382
+ if (statSync(full).isFile())
11383
+ out.push(full);
11384
+ } catch {}
11385
+ }
11386
+ return out.sort();
11387
+ }
11388
+ function stampOverlay(entry) {
11389
+ Object.defineProperty(entry, OVERLAY_SOURCE, {
11390
+ value: true,
11391
+ enumerable: false,
11392
+ configurable: false,
11393
+ writable: false
11394
+ });
11395
+ return entry;
11396
+ }
11397
+ function applyAgentOverlays(config) {
11398
+ const warnings = [];
11399
+ const agents = config.agents ?? {};
11400
+ for (const [agentName, agentCfg] of Object.entries(agents)) {
11401
+ try {
11402
+ const scheduleDir = overlayDirFor(agentName, "schedule.d");
11403
+ const files = listYamlFiles(scheduleDir);
11404
+ if (files.length > 0) {
11405
+ const merged = [...agentCfg.schedule ?? []];
11406
+ for (const file of files) {
11407
+ try {
11408
+ const raw = readFileSync(file, "utf-8");
11409
+ const parsed = $parse(raw);
11410
+ const doc = OverlayDocSchema.parse(parsed);
11411
+ for (const entry of doc.schedule ?? []) {
11412
+ if (entry.secrets && entry.secrets.length > 0) {
11413
+ const w = {
11414
+ agent: agentName,
11415
+ file,
11416
+ reason: "Overlay schedule entry declares secrets — dropped pending Phase E operator approval"
11417
+ };
11418
+ warnings.push(w);
11419
+ console.warn(`[switchroom] overlay-loader: agent='${agentName}' file='${file}': ${w.reason}`);
11420
+ continue;
11421
+ }
11422
+ merged.push(stampOverlay(entry));
11423
+ }
11424
+ } catch (err) {
11425
+ const reason = err instanceof ZodError ? `schema rejection: ${err.errors.map((e) => `${e.path.join(".")}: ${e.message}`).join("; ")}` : `parse error: ${err.message}`;
11426
+ warnings.push({ agent: agentName, file, reason });
11427
+ console.warn(`[switchroom] overlay-loader: agent='${agentName}' file='${file}': ${reason}`);
11428
+ }
11429
+ }
11430
+ agentCfg.schedule = merged;
11431
+ }
11432
+ } catch (err) {
11433
+ warnings.push({
11434
+ agent: agentName,
11435
+ file: "(agent schedule overlay scan)",
11436
+ reason: `unexpected error: ${err.message}`
11437
+ });
11438
+ console.warn(`[switchroom] overlay-loader: agent='${agentName}' schedule.d: unexpected error: ${err.message}`);
11439
+ }
11440
+ try {
11441
+ const skillsDir = overlayDirFor(agentName, "skills.d");
11442
+ const skillFiles = listYamlFiles(skillsDir);
11443
+ if (skillFiles.length === 0) {} else {
11444
+ const merged = [...agentCfg.skills ?? []];
11445
+ const seen = new Set(merged);
11446
+ for (const file of skillFiles) {
11447
+ try {
11448
+ const raw = readFileSync(file, "utf-8");
11449
+ const parsed = $parse(raw);
11450
+ const doc = OverlayDocSchema.parse(parsed);
11451
+ for (const skillName of doc.skills ?? []) {
11452
+ if (seen.has(skillName))
11453
+ continue;
11454
+ seen.add(skillName);
11455
+ merged.push(skillName);
11456
+ }
11457
+ } catch (err) {
11458
+ const reason = err instanceof ZodError ? `schema rejection: ${err.errors.map((e) => `${e.path.join(".")}: ${e.message}`).join("; ")}` : `parse error: ${err.message}`;
11459
+ warnings.push({ agent: agentName, file, reason });
11460
+ console.warn(`[switchroom] overlay-loader: agent='${agentName}' file='${file}': ${reason}`);
11461
+ }
11462
+ }
11463
+ agentCfg.skills = merged;
11464
+ }
11465
+ } catch (err) {
11466
+ warnings.push({
11467
+ agent: agentName,
11468
+ file: "(agent skills overlay scan)",
11469
+ reason: `unexpected error: ${err.message}`
11470
+ });
11471
+ console.warn(`[switchroom] overlay-loader: agent='${agentName}' skills.d: unexpected error: ${err.message}`);
11472
+ }
11473
+ }
11474
+ return { config, warnings };
11475
+ }
11476
+ var OVERLAY_SOURCE;
11477
+ var init_overlay_loader = __esm(() => {
11478
+ init_dist();
11479
+ init_zod();
11480
+ init_overlay_schema();
11481
+ init_paths();
11482
+ OVERLAY_SOURCE = Symbol.for("switchroom.config.overlay-source");
11483
+ });
11484
+
11310
11485
  // src/config/loader.ts
11311
11486
  var exports_loader = {};
11312
11487
  __export(exports_loader, {
@@ -11316,36 +11491,74 @@ __export(exports_loader, {
11316
11491
  findConfigFile: () => findConfigFile,
11317
11492
  ConfigError: () => ConfigError
11318
11493
  });
11319
- import { readFileSync, existsSync as existsSync2 } from "node:fs";
11494
+ import { readFileSync as readFileSync2, existsSync as existsSync3 } from "node:fs";
11320
11495
  import { homedir } from "node:os";
11321
- import { resolve as resolve2 } from "node:path";
11496
+ import { resolve as resolve3 } from "node:path";
11322
11497
  function formatZodErrors(error) {
11323
11498
  return error.errors.map((e) => {
11324
11499
  const path = e.path.join(".");
11325
11500
  return ` ${path}: ${e.message}`;
11326
11501
  });
11327
11502
  }
11503
+ function coerceLegacyGoogleWorkspaceKeys(parsed, filePath) {
11504
+ const stableStringify = (v) => {
11505
+ if (v === null || typeof v !== "object")
11506
+ return JSON.stringify(v);
11507
+ if (Array.isArray(v))
11508
+ return `[${v.map(stableStringify).join(",")}]`;
11509
+ const obj = v;
11510
+ const keys = Object.keys(obj).sort();
11511
+ return `{${keys.map((k) => `${JSON.stringify(k)}:${stableStringify(obj[k])}`).join(",")}}`;
11512
+ };
11513
+ const aliasInPlace = (obj, where) => {
11514
+ const a = obj.drive;
11515
+ const b = obj.google_workspace;
11516
+ if (a !== undefined && b !== undefined) {
11517
+ if (stableStringify(a) !== stableStringify(b)) {
11518
+ throw new ConfigError(`Both \`drive:\` and \`google_workspace:\` are set on ${where} in ${filePath} with different values.`, [
11519
+ " These are aliases — pick one and remove the other.",
11520
+ " `google_workspace:` is the RFC G canonical key; `drive:` is the legacy alias.",
11521
+ " Allowed during transition: setting both with identical values."
11522
+ ]);
11523
+ }
11524
+ return;
11525
+ }
11526
+ if (a !== undefined && b === undefined)
11527
+ obj.google_workspace = a;
11528
+ if (b !== undefined && a === undefined)
11529
+ obj.drive = b;
11530
+ };
11531
+ aliasInPlace(parsed, "the top level");
11532
+ const agents = parsed.agents;
11533
+ if (agents && typeof agents === "object" && !Array.isArray(agents)) {
11534
+ for (const [name, agent] of Object.entries(agents)) {
11535
+ if (agent && typeof agent === "object" && !Array.isArray(agent)) {
11536
+ aliasInPlace(agent, `agent \`${name}\``);
11537
+ }
11538
+ }
11539
+ }
11540
+ }
11328
11541
  function findConfigFile(startDir) {
11329
11542
  const envPath = process.env.SWITCHROOM_CONFIG;
11330
11543
  const home2 = homedir();
11331
- const userDir = resolve2(home2, ".switchroom");
11544
+ const userDir = resolve3(home2, ".switchroom");
11332
11545
  const searchPaths = [
11333
- envPath ? resolve2(envPath) : null,
11334
- startDir ? resolve2(startDir, "switchroom.yaml") : null,
11335
- startDir ? resolve2(startDir, "switchroom.yml") : null,
11336
- startDir ? resolve2(startDir, "clerk.yaml") : null,
11337
- startDir ? resolve2(startDir, "clerk.yml") : null,
11338
- resolve2(process.cwd(), "switchroom.yaml"),
11339
- resolve2(process.cwd(), "switchroom.yml"),
11340
- resolve2(process.cwd(), "clerk.yaml"),
11341
- resolve2(process.cwd(), "clerk.yml"),
11342
- resolve2(userDir, "switchroom.yaml"),
11343
- resolve2(userDir, "switchroom.yml"),
11344
- resolve2(userDir, "clerk.yaml"),
11345
- resolve2(userDir, "clerk.yml")
11546
+ envPath ? resolve3(envPath) : null,
11547
+ startDir ? resolve3(startDir, "switchroom.yaml") : null,
11548
+ startDir ? resolve3(startDir, "switchroom.yml") : null,
11549
+ startDir ? resolve3(startDir, "clerk.yaml") : null,
11550
+ startDir ? resolve3(startDir, "clerk.yml") : null,
11551
+ resolve3(process.cwd(), "switchroom.yaml"),
11552
+ resolve3(process.cwd(), "switchroom.yml"),
11553
+ resolve3(process.cwd(), "clerk.yaml"),
11554
+ resolve3(process.cwd(), "clerk.yml"),
11555
+ resolve3(userDir, "switchroom.yaml"),
11556
+ resolve3(userDir, "switchroom.yml"),
11557
+ resolve3(userDir, "clerk.yaml"),
11558
+ resolve3(userDir, "clerk.yml")
11346
11559
  ].filter(Boolean);
11347
11560
  for (const path of searchPaths) {
11348
- if (existsSync2(path)) {
11561
+ if (existsSync3(path)) {
11349
11562
  return path;
11350
11563
  }
11351
11564
  }
@@ -11353,12 +11566,12 @@ function findConfigFile(startDir) {
11353
11566
  }
11354
11567
  function loadConfig(configPath) {
11355
11568
  const filePath = configPath ?? findConfigFile();
11356
- if (!existsSync2(filePath)) {
11569
+ if (!existsSync3(filePath)) {
11357
11570
  throw new ConfigError(`Config file not found: ${filePath}`);
11358
11571
  }
11359
11572
  let raw;
11360
11573
  try {
11361
- raw = readFileSync(filePath, "utf-8");
11574
+ raw = readFileSync2(filePath, "utf-8");
11362
11575
  } catch (err) {
11363
11576
  throw new ConfigError(`Failed to read config file: ${filePath}`, [
11364
11577
  ` ${err.message}`
@@ -11377,16 +11590,26 @@ function loadConfig(configPath) {
11377
11590
  obj.switchroom = obj.clerk;
11378
11591
  delete obj.clerk;
11379
11592
  }
11593
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
11594
+ coerceLegacyGoogleWorkspaceKeys(parsed, filePath);
11595
+ }
11596
+ let config;
11380
11597
  try {
11381
- return SwitchroomConfigSchema.parse(parsed);
11598
+ config = SwitchroomConfigSchema.parse(parsed);
11382
11599
  } catch (err) {
11383
11600
  if (err instanceof ZodError) {
11384
11601
  throw new ConfigError("Invalid switchroom.yaml configuration", formatZodErrors(err));
11385
11602
  }
11386
11603
  throw err;
11387
11604
  }
11605
+ applyAgentOverlays(config);
11606
+ return config;
11388
11607
  }
11389
11608
  function resolveAgentsDir(config) {
11609
+ const override = process.env.SWITCHROOM_AGENTS_DIR;
11610
+ if (override && override.length > 0 && override.startsWith("/")) {
11611
+ return override;
11612
+ }
11390
11613
  return resolveDualPath(config.switchroom.agents_dir);
11391
11614
  }
11392
11615
  function resolvePath(pathStr) {
@@ -11398,6 +11621,7 @@ var init_loader = __esm(() => {
11398
11621
  init_zod();
11399
11622
  init_schema();
11400
11623
  init_paths();
11624
+ init_overlay_loader();
11401
11625
  ConfigError = class ConfigError extends Error {
11402
11626
  details;
11403
11627
  constructor(message, details) {
@@ -11410,8 +11634,8 @@ var init_loader = __esm(() => {
11410
11634
 
11411
11635
  // src/vault/approvals/kernel-server.ts
11412
11636
  import * as net from "node:net";
11413
- import { mkdirSync, chmodSync, chownSync, existsSync as existsSync3, unlinkSync, readdirSync, statSync } from "node:fs";
11414
- import { dirname, resolve as resolve3, basename } from "node:path";
11637
+ import { mkdirSync, chmodSync, chownSync, existsSync as existsSync4, unlinkSync, readdirSync as readdirSync2, statSync as statSync2 } from "node:fs";
11638
+ import { dirname, resolve as resolve4, basename } from "node:path";
11415
11639
  import { Database } from "bun:sqlite";
11416
11640
 
11417
11641
  // src/vault/broker/protocol.ts
@@ -12316,6 +12540,11 @@ function mergeAgentConfig(defaultsIn, agentIn) {
12316
12540
  }
12317
12541
  merged.reactions = combined;
12318
12542
  }
12543
+ if (defaults.resources || merged.resources) {
12544
+ const d = defaults.resources ?? {};
12545
+ const a = merged.resources ?? {};
12546
+ merged.resources = { ...d, ...a };
12547
+ }
12319
12548
  if (defaults.experimental || merged.experimental) {
12320
12549
  const d = defaults.experimental ?? {};
12321
12550
  const a = merged.experimental ?? {};
@@ -12445,7 +12674,7 @@ function openKernelDb(dbPath) {
12445
12674
  return db;
12446
12675
  }
12447
12676
  async function bindAgentSocket(parentDir, agent, db) {
12448
- const dir = resolve3(parentDir, agent);
12677
+ const dir = resolve4(parentDir, agent);
12449
12678
  mkdirSync(dir, { recursive: true, mode: 448 });
12450
12679
  try {
12451
12680
  chownSync(dir, 0, 0);
@@ -12454,8 +12683,8 @@ async function bindAgentSocket(parentDir, agent, db) {
12454
12683
  chmodSync(dir, 448);
12455
12684
  } catch {}
12456
12685
  const uid = allocateAgentUid(agent);
12457
- const socketPath = resolve3(dir, "sock");
12458
- if (existsSync3(socketPath)) {
12686
+ const socketPath = resolve4(dir, "sock");
12687
+ if (existsSync4(socketPath)) {
12459
12688
  try {
12460
12689
  unlinkSync(socketPath);
12461
12690
  } catch (err) {
@@ -12668,7 +12897,7 @@ async function bootstrap(opts) {
12668
12897
  l.server.close();
12669
12898
  } catch {}
12670
12899
  try {
12671
- if (existsSync3(l.socketPath))
12900
+ if (existsSync4(l.socketPath))
12672
12901
  unlinkSync(l.socketPath);
12673
12902
  } catch {}
12674
12903
  }
@@ -12680,16 +12909,16 @@ async function bootstrap(opts) {
12680
12909
  }
12681
12910
  async function main() {
12682
12911
  const socketEnv = process.env.SWITCHROOM_KERNEL_SOCKET ?? `${DEFAULT_SOCKET_PARENT}/approval-kernel.sock`;
12683
- const socketParent = dirname(resolve3(socketEnv));
12912
+ const socketParent = dirname(resolve4(socketEnv));
12684
12913
  const dbPath = process.env.SWITCHROOM_KERNEL_DB_PATH ?? DEFAULT_DB_PATH;
12685
12914
  const configPath = process.env.SWITCHROOM_CONFIG;
12686
12915
  let agents = [];
12687
12916
  try {
12688
- if (existsSync3(socketParent)) {
12689
- agents = readdirSync(socketParent).filter((name) => {
12917
+ if (existsSync4(socketParent)) {
12918
+ agents = readdirSync2(socketParent).filter((name) => {
12690
12919
  try {
12691
- const p = resolve3(socketParent, name);
12692
- return statSync(p).isDirectory();
12920
+ const p = resolve4(socketParent, name);
12921
+ return statSync2(p).isDirectory();
12693
12922
  } catch {
12694
12923
  return false;
12695
12924
  }
@@ -12717,8 +12946,8 @@ async function main() {
12717
12946
  chmodSync(socketParent, 493);
12718
12947
  } catch {}
12719
12948
  const db = openKernelDb(dbPath);
12720
- const socketPath = resolve3(socketEnv);
12721
- if (existsSync3(socketPath)) {
12949
+ const socketPath = resolve4(socketEnv);
12950
+ if (existsSync4(socketPath)) {
12722
12951
  try {
12723
12952
  unlinkSync(socketPath);
12724
12953
  } catch {}
@@ -12740,7 +12969,7 @@ async function main() {
12740
12969
  server.close();
12741
12970
  } catch {}
12742
12971
  try {
12743
- if (existsSync3(socketPath))
12972
+ if (existsSync4(socketPath))
12744
12973
  unlinkSync(socketPath);
12745
12974
  } catch {}
12746
12975
  try {