switchroom 0.8.1 → 0.10.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.
- package/README.md +49 -57
- package/bin/timezone-hook.sh +9 -7
- package/dist/agent-scheduler/index.js +285 -45
- package/dist/auth-broker/index.js +13932 -0
- package/dist/cli/switchroom.js +15931 -12778
- package/dist/host-control/main.js +582 -43
- package/dist/vault/approvals/kernel-server.js +276 -47
- package/dist/vault/broker/server.js +333 -69
- package/examples/minimal.yaml +63 -0
- package/examples/personal-google-workspace-mcp/.env.example +34 -0
- package/examples/personal-google-workspace-mcp/README.md +194 -0
- package/examples/personal-google-workspace-mcp/compose.yaml +66 -0
- package/examples/switchroom.yaml +220 -0
- package/package.json +6 -4
- package/profiles/_base/start.sh.hbs +3 -3
- package/profiles/_shared/agent-self-service.md.hbs +126 -0
- package/profiles/default/CLAUDE.md +10 -0
- package/profiles/default/CLAUDE.md.hbs +16 -0
- package/skills/buildkite-agent-infrastructure/SKILL.md +30 -11
- package/skills/buildkite-agent-runtime/SKILL.md +44 -11
- package/skills/buildkite-api/SKILL.md +31 -8
- package/skills/buildkite-cli/SKILL.md +27 -9
- package/skills/buildkite-migration/SKILL.md +22 -9
- package/skills/buildkite-pipelines/SKILL.md +26 -9
- package/skills/buildkite-secure-delivery/SKILL.md +23 -9
- package/skills/buildkite-test-engine/SKILL.md +25 -8
- package/skills/docx/SKILL.md +1 -1
- package/skills/file-bug/SKILL.md +34 -6
- package/skills/humanizer/SKILL.md +15 -0
- package/skills/humanizer-calibrate/SKILL.md +7 -1
- package/skills/mcp-builder/SKILL.md +1 -1
- package/skills/pdf/SKILL.md +1 -1
- package/skills/pptx/SKILL.md +1 -1
- package/skills/skill-creator/SKILL.md +21 -1
- package/skills/skill-creator/scripts/__pycache__/__init__.cpython-313.pyc +0 -0
- package/skills/skill-creator/scripts/__pycache__/generate_report.cpython-313.pyc +0 -0
- package/skills/skill-creator/scripts/__pycache__/improve_description.cpython-313.pyc +0 -0
- package/skills/skill-creator/scripts/__pycache__/run_eval.cpython-313.pyc +0 -0
- package/skills/skill-creator/scripts/__pycache__/run_loop.cpython-313.pyc +0 -0
- package/skills/skill-creator/scripts/__pycache__/utils.cpython-313.pyc +0 -0
- package/skills/switchroom-cli/SKILL.md +63 -64
- package/skills/switchroom-health/SKILL.md +23 -10
- package/skills/switchroom-install/SKILL.md +3 -3
- package/skills/switchroom-manage/SKILL.md +26 -19
- package/skills/switchroom-runtime/SKILL.md +67 -15
- package/skills/switchroom-status/SKILL.md +26 -1
- package/skills/telegram-test-harness/SKILL.md +3 -0
- package/skills/webapp-testing/SKILL.md +31 -1
- package/skills/xlsx/SKILL.md +1 -1
- package/telegram-plugin/admin-commands/index.ts +7 -5
- package/telegram-plugin/dist/gateway/gateway.js +13042 -12844
- package/telegram-plugin/gateway/auth-add-flow.ts +326 -0
- package/telegram-plugin/gateway/auth-broker-client.ts +75 -0
- package/telegram-plugin/gateway/auth-command.ts +794 -0
- package/telegram-plugin/gateway/auth-line.ts +123 -0
- package/telegram-plugin/gateway/boot-card.ts +22 -36
- package/telegram-plugin/gateway/boot-probes.ts +3 -3
- package/telegram-plugin/gateway/gateway.ts +313 -798
- package/telegram-plugin/gateway/hostd-dispatch.ts +117 -0
- package/telegram-plugin/hooks/tool-label-pretool.mjs +11 -0
- package/telegram-plugin/hooks/wedge-detect-posttool.mjs +303 -0
- package/telegram-plugin/permission-title.ts +56 -0
- package/telegram-plugin/quota-check.ts +19 -41
- package/telegram-plugin/scripts/build.mjs +0 -1
- package/telegram-plugin/shared/bot-runtime.ts +5 -4
- package/telegram-plugin/tests/auth-add-flow.test.ts +559 -0
- package/telegram-plugin/tests/auth-code-redact.test.ts +8 -4
- package/telegram-plugin/tests/auth-command-vernacular.test.ts +531 -0
- package/telegram-plugin/tests/boot-probes.test.ts +11 -4
- package/telegram-plugin/tests/hostd-dispatch.test.ts +129 -0
- package/telegram-plugin/tests/permission-title.test.ts +31 -0
- package/telegram-plugin/tests/quota-check.test.ts +5 -35
- package/telegram-plugin/uat/SETUP.md +31 -1
- package/telegram-plugin/uat/runners/agent-self-sufficiency.ts +457 -0
- package/telegram-plugin/uat/runners/paraphrases.ts +231 -0
- package/telegram-plugin/uat/runners/report.ts +150 -0
- package/telegram-plugin/uat/runners/run-agent-self-sufficiency.sh +50 -0
- package/telegram-plugin/uat/runners/scorer.test.ts +196 -0
- package/telegram-plugin/uat/runners/scorer.ts +106 -0
- package/telegram-plugin/uat/runners/skill-coverage.test.ts +100 -0
- package/telegram-plugin/uat/runners/skill-coverage.ts +620 -0
- package/telegram-plugin/uat/scenarios/jtbd-interrupt-marker-dm.test.ts +7 -1
- package/telegram-plugin/uat/scenarios/jtbd-rapid-followup-dm.test.ts +7 -1
- package/telegram-plugin/auth-dashboard.ts +0 -1104
- package/telegram-plugin/auth-slot-parser.ts +0 -497
- package/telegram-plugin/dist/foreman/foreman.js +0 -31358
- package/telegram-plugin/foreman/foreman-create-flow.ts +0 -202
- package/telegram-plugin/foreman/foreman-handlers.ts +0 -493
- package/telegram-plugin/foreman/foreman.ts +0 -1165
- package/telegram-plugin/foreman/setup-flow.ts +0 -345
- package/telegram-plugin/foreman/setup-state.ts +0 -239
- package/telegram-plugin/foreman/state.ts +0 -203
- package/telegram-plugin/tests/auth-account-identity-surface.test.ts +0 -118
- package/telegram-plugin/tests/auth-dashboard-edge-cases.test.ts +0 -260
- package/telegram-plugin/tests/auth-dashboard-restart-flow.test.ts +0 -140
- package/telegram-plugin/tests/auth-dashboard-v3b.test.ts +0 -559
- package/telegram-plugin/tests/auth-dashboard.test.ts +0 -1045
- package/telegram-plugin/tests/auth-slot-commands.test.ts +0 -640
- package/telegram-plugin/tests/boot-card-account-quota.test.ts +0 -137
- package/telegram-plugin/tests/foreman-create-flow.test.ts +0 -359
- package/telegram-plugin/tests/foreman-handlers.test.ts +0 -347
- package/telegram-plugin/tests/foreman-state.test.ts +0 -164
- package/telegram-plugin/tests/foreman-write-ops.test.ts +0 -214
- package/telegram-plugin/tests/setup-flow.test.ts +0 -510
- package/telegram-plugin/tests/setup-state.test.ts +0 -146
|
@@ -10948,7 +10948,7 @@ var init_zod = __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,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
11162
|
-
}).optional().describe("Account routing for switchroom-auth-broker.
|
|
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:
|
|
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
|
|
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
|
-
|
|
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 existsSync5, readFileSync as readFileSync5, readdirSync as readdirSync2, statSync as statSync3 } from "node:fs";
|
|
11362
|
+
import { resolve as resolve3 } from "node:path";
|
|
11363
|
+
function overlayDirFor(agentName, subdir) {
|
|
11364
|
+
const base = resolveDualPath(`~/.switchroom/agents/${agentName}/${subdir}`);
|
|
11365
|
+
return resolve3(base);
|
|
11366
|
+
}
|
|
11367
|
+
function listYamlFiles(dir) {
|
|
11368
|
+
if (!existsSync5(dir))
|
|
11369
|
+
return [];
|
|
11370
|
+
let entries;
|
|
11371
|
+
try {
|
|
11372
|
+
entries = readdirSync2(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 = resolve3(dir, name);
|
|
11381
|
+
try {
|
|
11382
|
+
if (statSync3(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 = readFileSync5(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 = readFileSync5(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 as
|
|
11494
|
+
import { readFileSync as readFileSync6, existsSync as existsSync6 } from "node:fs";
|
|
11320
11495
|
import { homedir } from "node:os";
|
|
11321
|
-
import { resolve as
|
|
11496
|
+
import { resolve as resolve4 } 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 =
|
|
11544
|
+
const userDir = resolve4(home2, ".switchroom");
|
|
11332
11545
|
const searchPaths = [
|
|
11333
|
-
envPath ?
|
|
11334
|
-
startDir ?
|
|
11335
|
-
startDir ?
|
|
11336
|
-
startDir ?
|
|
11337
|
-
startDir ?
|
|
11338
|
-
|
|
11339
|
-
|
|
11340
|
-
|
|
11341
|
-
|
|
11342
|
-
|
|
11343
|
-
|
|
11344
|
-
|
|
11345
|
-
|
|
11546
|
+
envPath ? resolve4(envPath) : null,
|
|
11547
|
+
startDir ? resolve4(startDir, "switchroom.yaml") : null,
|
|
11548
|
+
startDir ? resolve4(startDir, "switchroom.yml") : null,
|
|
11549
|
+
startDir ? resolve4(startDir, "clerk.yaml") : null,
|
|
11550
|
+
startDir ? resolve4(startDir, "clerk.yml") : null,
|
|
11551
|
+
resolve4(process.cwd(), "switchroom.yaml"),
|
|
11552
|
+
resolve4(process.cwd(), "switchroom.yml"),
|
|
11553
|
+
resolve4(process.cwd(), "clerk.yaml"),
|
|
11554
|
+
resolve4(process.cwd(), "clerk.yml"),
|
|
11555
|
+
resolve4(userDir, "switchroom.yaml"),
|
|
11556
|
+
resolve4(userDir, "switchroom.yml"),
|
|
11557
|
+
resolve4(userDir, "clerk.yaml"),
|
|
11558
|
+
resolve4(userDir, "clerk.yml")
|
|
11346
11559
|
].filter(Boolean);
|
|
11347
11560
|
for (const path of searchPaths) {
|
|
11348
|
-
if (
|
|
11561
|
+
if (existsSync6(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 (!
|
|
11569
|
+
if (!existsSync6(filePath)) {
|
|
11357
11570
|
throw new ConfigError(`Config file not found: ${filePath}`);
|
|
11358
11571
|
}
|
|
11359
11572
|
let raw;
|
|
11360
11573
|
try {
|
|
11361
|
-
raw =
|
|
11574
|
+
raw = readFileSync6(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
|
-
|
|
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,7 +11634,7 @@ var init_loader = __esm(() => {
|
|
|
11410
11634
|
|
|
11411
11635
|
// src/vault/broker/server.ts
|
|
11412
11636
|
import * as net from "node:net";
|
|
11413
|
-
import { mkdirSync as mkdirSync5, chmodSync as chmodSync4, chownSync, existsSync as
|
|
11637
|
+
import { mkdirSync as mkdirSync5, chmodSync as chmodSync4, chownSync, existsSync as existsSync8, readFileSync as readFileSync8, readdirSync as readdirSync3, unlinkSync as unlinkSync4, writeFileSync as writeFileSync3, renameSync as renameSync3 } from "node:fs";
|
|
11414
11638
|
|
|
11415
11639
|
// src/agents/compose.ts
|
|
11416
11640
|
import { createHash } from "node:crypto";
|
|
@@ -11690,6 +11914,11 @@ function mergeAgentConfig(defaultsIn, agentIn) {
|
|
|
11690
11914
|
}
|
|
11691
11915
|
merged.reactions = combined;
|
|
11692
11916
|
}
|
|
11917
|
+
if (defaults.resources || merged.resources) {
|
|
11918
|
+
const d = defaults.resources ?? {};
|
|
11919
|
+
const a = merged.resources ?? {};
|
|
11920
|
+
merged.resources = { ...d, ...a };
|
|
11921
|
+
}
|
|
11693
11922
|
if (defaults.experimental || merged.experimental) {
|
|
11694
11923
|
const d = defaults.experimental ?? {};
|
|
11695
11924
|
const a = merged.experimental ?? {};
|
|
@@ -12031,7 +12260,7 @@ function allocateAgentUid(name) {
|
|
|
12031
12260
|
var BIND_MOUNT_EXACT_SOURCE_DENY = new Set(["/var/run/docker.sock"]);
|
|
12032
12261
|
|
|
12033
12262
|
// src/vault/broker/server.ts
|
|
12034
|
-
import { dirname as dirname4, resolve as
|
|
12263
|
+
import { dirname as dirname4, resolve as resolve5, basename as basename3 } from "node:path";
|
|
12035
12264
|
import * as os3 from "node:os";
|
|
12036
12265
|
import * as path3 from "node:path";
|
|
12037
12266
|
|
|
@@ -12559,7 +12788,7 @@ init_loader();
|
|
|
12559
12788
|
|
|
12560
12789
|
// src/vault/auto-unlock.ts
|
|
12561
12790
|
import { createHmac, randomBytes as randomBytes2, createCipheriv as createCipheriv2, createDecipheriv as createDecipheriv2 } from "node:crypto";
|
|
12562
|
-
import { chmodSync as chmodSync2, existsSync as
|
|
12791
|
+
import { chmodSync as chmodSync2, existsSync as existsSync7, mkdirSync as mkdirSync3, readFileSync as readFileSync7, writeFileSync as writeFileSync2 } from "node:fs";
|
|
12563
12792
|
var FORMAT_VERSION = 1;
|
|
12564
12793
|
var SALT_LEN = 16;
|
|
12565
12794
|
var NONCE_LEN = 12;
|
|
@@ -12587,7 +12816,7 @@ class AutoUnlockDecryptError extends Error {
|
|
|
12587
12816
|
function readMachineId() {
|
|
12588
12817
|
for (const path of [MACHINE_ID_PRIMARY, MACHINE_ID_FALLBACK]) {
|
|
12589
12818
|
try {
|
|
12590
|
-
const id =
|
|
12819
|
+
const id = readFileSync7(path, "utf8").trim();
|
|
12591
12820
|
if (id.length > 0)
|
|
12592
12821
|
return id;
|
|
12593
12822
|
} catch {}
|
|
@@ -12624,12 +12853,12 @@ function decryptAutoUnlock(blob, machineId) {
|
|
|
12624
12853
|
}
|
|
12625
12854
|
}
|
|
12626
12855
|
function readAutoUnlockFile(filePath) {
|
|
12627
|
-
if (!
|
|
12856
|
+
if (!existsSync7(filePath)) {
|
|
12628
12857
|
throw new AutoUnlockDecryptError("io");
|
|
12629
12858
|
}
|
|
12630
12859
|
let blob;
|
|
12631
12860
|
try {
|
|
12632
|
-
blob =
|
|
12861
|
+
blob = readFileSync7(filePath);
|
|
12633
12862
|
} catch {
|
|
12634
12863
|
throw new AutoUnlockDecryptError("io");
|
|
12635
12864
|
}
|
|
@@ -12720,6 +12949,10 @@ function checkAclByAgent(config, agentName, key) {
|
|
|
12720
12949
|
if (!agentConfig) {
|
|
12721
12950
|
return { allow: false, reason: `agent '${agentName}' not found in config` };
|
|
12722
12951
|
}
|
|
12952
|
+
const googleSlot = parseGoogleAccountSlotKey(key);
|
|
12953
|
+
if (googleSlot !== null) {
|
|
12954
|
+
return checkGoogleAccountAcl(config, agentName, googleSlot.account, key);
|
|
12955
|
+
}
|
|
12723
12956
|
const schedule = agentConfig.schedule ?? [];
|
|
12724
12957
|
if (schedule.length === 0) {
|
|
12725
12958
|
return {
|
|
@@ -12738,6 +12971,37 @@ function checkAclByAgent(config, agentName, key) {
|
|
|
12738
12971
|
reason: `key '${key}' not in ACL for agent '${agentName}'`
|
|
12739
12972
|
};
|
|
12740
12973
|
}
|
|
12974
|
+
function parseGoogleAccountSlotKey(key) {
|
|
12975
|
+
const match = key.match(/^google:([^:]+):([a-z_]+)$/);
|
|
12976
|
+
if (!match)
|
|
12977
|
+
return null;
|
|
12978
|
+
return { account: match[1], field: match[2] };
|
|
12979
|
+
}
|
|
12980
|
+
function checkGoogleAccountAcl(config, agentName, account, key) {
|
|
12981
|
+
const accounts = config.google_accounts ?? {};
|
|
12982
|
+
const accountKey = account.toLowerCase();
|
|
12983
|
+
const accountEntry = accounts[accountKey] ?? accounts[account];
|
|
12984
|
+
if (!accountEntry) {
|
|
12985
|
+
return {
|
|
12986
|
+
allow: false,
|
|
12987
|
+
reason: `google_accounts['${account}'] not configured (key '${key}')`
|
|
12988
|
+
};
|
|
12989
|
+
}
|
|
12990
|
+
const enabled = accountEntry.enabled_for ?? [];
|
|
12991
|
+
if (enabled.length === 0) {
|
|
12992
|
+
return {
|
|
12993
|
+
allow: false,
|
|
12994
|
+
reason: `google_accounts['${account}'].enabled_for is empty (key '${key}')`
|
|
12995
|
+
};
|
|
12996
|
+
}
|
|
12997
|
+
if (!enabled.includes(agentName)) {
|
|
12998
|
+
return {
|
|
12999
|
+
allow: false,
|
|
13000
|
+
reason: `agent '${agentName}' not in google_accounts['${account}'].enabled_for (key '${key}')`
|
|
13001
|
+
};
|
|
13002
|
+
}
|
|
13003
|
+
return { allow: true };
|
|
13004
|
+
}
|
|
12741
13005
|
|
|
12742
13006
|
// src/vault/broker/protocol.ts
|
|
12743
13007
|
init_zod();
|
|
@@ -13144,13 +13408,13 @@ function genSalt(rounds, seed_length, callback) {
|
|
|
13144
13408
|
throw Error("Illegal callback: " + typeof callback);
|
|
13145
13409
|
_async(callback);
|
|
13146
13410
|
} else
|
|
13147
|
-
return new Promise(function(
|
|
13411
|
+
return new Promise(function(resolve5, reject) {
|
|
13148
13412
|
_async(function(err, res) {
|
|
13149
13413
|
if (err) {
|
|
13150
13414
|
reject(err);
|
|
13151
13415
|
return;
|
|
13152
13416
|
}
|
|
13153
|
-
|
|
13417
|
+
resolve5(res);
|
|
13154
13418
|
});
|
|
13155
13419
|
});
|
|
13156
13420
|
}
|
|
@@ -13170,13 +13434,13 @@ function hash(password, salt, callback, progressCallback) {
|
|
|
13170
13434
|
throw Error("Illegal callback: " + typeof callback);
|
|
13171
13435
|
_async(callback);
|
|
13172
13436
|
} else
|
|
13173
|
-
return new Promise(function(
|
|
13437
|
+
return new Promise(function(resolve5, reject) {
|
|
13174
13438
|
_async(function(err, res) {
|
|
13175
13439
|
if (err) {
|
|
13176
13440
|
reject(err);
|
|
13177
13441
|
return;
|
|
13178
13442
|
}
|
|
13179
|
-
|
|
13443
|
+
resolve5(res);
|
|
13180
13444
|
});
|
|
13181
13445
|
});
|
|
13182
13446
|
}
|
|
@@ -13209,13 +13473,13 @@ function compare(password, hashValue, callback, progressCallback) {
|
|
|
13209
13473
|
throw Error("Illegal callback: " + typeof callback);
|
|
13210
13474
|
_async(callback);
|
|
13211
13475
|
} else
|
|
13212
|
-
return new Promise(function(
|
|
13476
|
+
return new Promise(function(resolve5, reject) {
|
|
13213
13477
|
_async(function(err, res) {
|
|
13214
13478
|
if (err) {
|
|
13215
13479
|
reject(err);
|
|
13216
13480
|
return;
|
|
13217
13481
|
}
|
|
13218
|
-
|
|
13482
|
+
resolve5(res);
|
|
13219
13483
|
});
|
|
13220
13484
|
});
|
|
13221
13485
|
}
|
|
@@ -15330,7 +15594,7 @@ class VaultBroker {
|
|
|
15330
15594
|
if (process.platform !== "linux" && process.env.SWITCHROOM_BROKER_ALLOW_NON_LINUX !== "1") {
|
|
15331
15595
|
throw new Error(`vault-broker is Linux-only (running on ${process.platform}). ` + `The broker's ACL relies on cgroup-based systemd unit identification, ` + `which is not available on this platform. ` + `Use 'switchroom vault get --no-broker' for direct vault access. ` + `If you need to run the broker for development on this platform, ` + `set SWITCHROOM_BROKER_ALLOW_NON_LINUX=1 — but understand that the ` + `broker will accept any same-user caller without per-cron ACL enforcement.`);
|
|
15332
15596
|
}
|
|
15333
|
-
this.socketPath =
|
|
15597
|
+
this.socketPath = resolve5(socketPath);
|
|
15334
15598
|
this.unlockSocketPath = unlockSocketFor(this.socketPath);
|
|
15335
15599
|
this.startedAt = Date.now();
|
|
15336
15600
|
if (this.testOpts._testConfig) {
|
|
@@ -15340,7 +15604,7 @@ class VaultBroker {
|
|
|
15340
15604
|
this.config = loadConfig2(configPath);
|
|
15341
15605
|
}
|
|
15342
15606
|
if (vaultPath) {
|
|
15343
|
-
this.vaultPath =
|
|
15607
|
+
this.vaultPath = resolve5(vaultPath);
|
|
15344
15608
|
} else {
|
|
15345
15609
|
this.vaultPath = resolvePath(this.config.vault?.path ?? "~/.switchroom/vault.enc");
|
|
15346
15610
|
}
|
|
@@ -15357,7 +15621,7 @@ class VaultBroker {
|
|
|
15357
15621
|
chmodSync4(parentDir, 448);
|
|
15358
15622
|
} catch {}
|
|
15359
15623
|
for (const p of [this.socketPath, this.unlockSocketPath]) {
|
|
15360
|
-
if (
|
|
15624
|
+
if (existsSync8(p)) {
|
|
15361
15625
|
try {
|
|
15362
15626
|
unlinkSync4(p);
|
|
15363
15627
|
} catch {}
|
|
@@ -15407,7 +15671,7 @@ class VaultBroker {
|
|
|
15407
15671
|
try {
|
|
15408
15672
|
entry.server.close();
|
|
15409
15673
|
} catch {}
|
|
15410
|
-
if (
|
|
15674
|
+
if (existsSync8(sockPath)) {
|
|
15411
15675
|
try {
|
|
15412
15676
|
unlinkSync4(sockPath);
|
|
15413
15677
|
} catch {}
|
|
@@ -15415,7 +15679,7 @@ class VaultBroker {
|
|
|
15415
15679
|
}
|
|
15416
15680
|
this.agentServers.clear();
|
|
15417
15681
|
for (const p of [this.socketPath, this.unlockSocketPath]) {
|
|
15418
|
-
if (p &&
|
|
15682
|
+
if (p && existsSync8(p)) {
|
|
15419
15683
|
try {
|
|
15420
15684
|
unlinkSync4(p);
|
|
15421
15685
|
} catch {}
|
|
@@ -15423,7 +15687,7 @@ class VaultBroker {
|
|
|
15423
15687
|
}
|
|
15424
15688
|
try {
|
|
15425
15689
|
const pidPath = resolvePath(PID_FILE_DEFAULT);
|
|
15426
|
-
if (
|
|
15690
|
+
if (existsSync8(pidPath))
|
|
15427
15691
|
unlinkSync4(pidPath);
|
|
15428
15692
|
} catch {}
|
|
15429
15693
|
}
|
|
@@ -15438,7 +15702,7 @@ class VaultBroker {
|
|
|
15438
15702
|
return this.secrets;
|
|
15439
15703
|
}
|
|
15440
15704
|
bindAgentSocket(socketPath) {
|
|
15441
|
-
const abs =
|
|
15705
|
+
const abs = resolve5(socketPath);
|
|
15442
15706
|
const agentName = socketPathToAgent(abs);
|
|
15443
15707
|
if (agentName === null) {
|
|
15444
15708
|
return Promise.reject(new Error(`bindAgentSocket: socket path '${abs}' does not match the canonical ` + `/run/switchroom/broker/<agent>.sock shape — refusing to bind without ` + `a verifiable agent identity`));
|
|
@@ -15446,7 +15710,7 @@ class VaultBroker {
|
|
|
15446
15710
|
return new Promise((resolveP, rejectP) => {
|
|
15447
15711
|
if (abs.endsWith("/sock")) {
|
|
15448
15712
|
const dir = abs.slice(0, -"/sock".length);
|
|
15449
|
-
if (
|
|
15713
|
+
if (existsSync8(dir)) {
|
|
15450
15714
|
try {
|
|
15451
15715
|
chownSync(dir, 0, 0);
|
|
15452
15716
|
} catch {}
|
|
@@ -15455,7 +15719,7 @@ class VaultBroker {
|
|
|
15455
15719
|
} catch {}
|
|
15456
15720
|
}
|
|
15457
15721
|
}
|
|
15458
|
-
if (
|
|
15722
|
+
if (existsSync8(abs)) {
|
|
15459
15723
|
try {
|
|
15460
15724
|
unlinkSync4(abs);
|
|
15461
15725
|
} catch (err) {
|
|
@@ -15490,7 +15754,7 @@ class VaultBroker {
|
|
|
15490
15754
|
});
|
|
15491
15755
|
}
|
|
15492
15756
|
_bindDataSocket() {
|
|
15493
|
-
return new Promise((
|
|
15757
|
+
return new Promise((resolve6, reject) => {
|
|
15494
15758
|
const server = net.createServer((socket) => {
|
|
15495
15759
|
this._handleDataConnection(socket);
|
|
15496
15760
|
});
|
|
@@ -15502,12 +15766,12 @@ class VaultBroker {
|
|
|
15502
15766
|
chmodSync4(this.socketPath, 384);
|
|
15503
15767
|
} catch {}
|
|
15504
15768
|
this.server = server;
|
|
15505
|
-
|
|
15769
|
+
resolve6();
|
|
15506
15770
|
});
|
|
15507
15771
|
});
|
|
15508
15772
|
}
|
|
15509
15773
|
bindOperatorListener(socketPath, operatorUid) {
|
|
15510
|
-
const abs =
|
|
15774
|
+
const abs = resolve5(socketPath);
|
|
15511
15775
|
const identity2 = socketPathToIdentity(abs);
|
|
15512
15776
|
if (identity2?.kind !== "operator") {
|
|
15513
15777
|
return Promise.reject(new Error(`bindOperatorListener: socket path '${abs}' does not match the canonical ` + `/run/switchroom/broker/operator/sock shape — refusing to bind`));
|
|
@@ -15515,7 +15779,7 @@ class VaultBroker {
|
|
|
15515
15779
|
const unlockAbs = unlockSocketFor(abs);
|
|
15516
15780
|
if (abs.endsWith("/sock")) {
|
|
15517
15781
|
const dir = abs.slice(0, -"/sock".length);
|
|
15518
|
-
if (
|
|
15782
|
+
if (existsSync8(dir)) {
|
|
15519
15783
|
try {
|
|
15520
15784
|
chownSync(dir, 0, 0);
|
|
15521
15785
|
} catch {}
|
|
@@ -15525,7 +15789,7 @@ class VaultBroker {
|
|
|
15525
15789
|
}
|
|
15526
15790
|
}
|
|
15527
15791
|
for (const p of [abs, unlockAbs]) {
|
|
15528
|
-
if (
|
|
15792
|
+
if (existsSync8(p)) {
|
|
15529
15793
|
try {
|
|
15530
15794
|
unlinkSync4(p);
|
|
15531
15795
|
} catch {}
|
|
@@ -15571,7 +15835,7 @@ class VaultBroker {
|
|
|
15571
15835
|
});
|
|
15572
15836
|
}
|
|
15573
15837
|
_bindUnlockSocket() {
|
|
15574
|
-
return new Promise((
|
|
15838
|
+
return new Promise((resolve6, reject) => {
|
|
15575
15839
|
const server = net.createServer((socket) => {
|
|
15576
15840
|
this._handleUnlockConnection(socket);
|
|
15577
15841
|
});
|
|
@@ -15583,7 +15847,7 @@ class VaultBroker {
|
|
|
15583
15847
|
chmodSync4(this.unlockSocketPath, 384);
|
|
15584
15848
|
} catch {}
|
|
15585
15849
|
this.unlockServer = server;
|
|
15586
|
-
|
|
15850
|
+
resolve6();
|
|
15587
15851
|
});
|
|
15588
15852
|
});
|
|
15589
15853
|
}
|
|
@@ -16357,7 +16621,7 @@ class VaultBroker {
|
|
|
16357
16621
|
const row = this.grantsDb.query("SELECT agent_slug FROM vault_grants WHERE id = ?").get(id);
|
|
16358
16622
|
if (row) {
|
|
16359
16623
|
const tokenPath = path3.join(os3.homedir(), ".switchroom", "agents", row.agent_slug, ".vault-token");
|
|
16360
|
-
if (
|
|
16624
|
+
if (existsSync8(tokenPath)) {
|
|
16361
16625
|
try {
|
|
16362
16626
|
unlinkSync4(tokenPath);
|
|
16363
16627
|
} catch {}
|
|
@@ -16595,7 +16859,7 @@ class VaultBroker {
|
|
|
16595
16859
|
const envPath = process.env.SWITCHROOM_VAULT_BROKER_AUTO_UNLOCK_PATH;
|
|
16596
16860
|
const configuredPath = (envPath && envPath.length > 0 ? envPath : undefined) ?? this.config?.vault?.broker?.autoUnlockCredentialPath ?? DEFAULT_AUTO_UNLOCK_PATH;
|
|
16597
16861
|
const filePath = resolvePath(configuredPath);
|
|
16598
|
-
if (!
|
|
16862
|
+
if (!existsSync8(filePath))
|
|
16599
16863
|
return false;
|
|
16600
16864
|
let passphrase;
|
|
16601
16865
|
try {
|
|
@@ -16632,7 +16896,7 @@ class VaultBroker {
|
|
|
16632
16896
|
const credPath = `${dir}/vault-passphrase`;
|
|
16633
16897
|
let passphrase;
|
|
16634
16898
|
try {
|
|
16635
|
-
passphrase =
|
|
16899
|
+
passphrase = readFileSync8(credPath, "utf8").replace(/\n+$/, "");
|
|
16636
16900
|
} catch (err) {
|
|
16637
16901
|
const code = err.code;
|
|
16638
16902
|
if (code === "ENOENT") {
|
|
@@ -16704,19 +16968,19 @@ async function main() {
|
|
|
16704
16968
|
const vaultPath = process.env.SWITCHROOM_VAULT_PATH;
|
|
16705
16969
|
let perAgentTargets = [];
|
|
16706
16970
|
try {
|
|
16707
|
-
if (
|
|
16708
|
-
const entries =
|
|
16971
|
+
if (existsSync8(perAgentDir)) {
|
|
16972
|
+
const entries = readdirSync3(perAgentDir, { withFileTypes: true });
|
|
16709
16973
|
const flat = [];
|
|
16710
16974
|
const subdirs = [];
|
|
16711
16975
|
for (const e of entries) {
|
|
16712
16976
|
if (e.name.startsWith("."))
|
|
16713
16977
|
continue;
|
|
16714
16978
|
if ((e.isFile() || e.isSocket()) && e.name.endsWith(".sock")) {
|
|
16715
|
-
flat.push(
|
|
16979
|
+
flat.push(resolve5(perAgentDir, e.name));
|
|
16716
16980
|
continue;
|
|
16717
16981
|
}
|
|
16718
16982
|
if (e.isDirectory()) {
|
|
16719
|
-
const candidate =
|
|
16983
|
+
const candidate = resolve5(perAgentDir, e.name, "sock");
|
|
16720
16984
|
if (socketPathToAgent(candidate) !== null) {
|
|
16721
16985
|
subdirs.push(candidate);
|
|
16722
16986
|
}
|
|
@@ -16746,7 +17010,7 @@ async function main() {
|
|
|
16746
17010
|
}
|
|
16747
17011
|
const operatorUidStr = process.env.SWITCHROOM_BROKER_OPERATOR_UID;
|
|
16748
17012
|
const operatorDir = "/run/switchroom/broker/operator";
|
|
16749
|
-
if (operatorUidStr !== undefined &&
|
|
17013
|
+
if (operatorUidStr !== undefined && existsSync8(operatorDir)) {
|
|
16750
17014
|
const operatorUid = parseInt(operatorUidStr, 10);
|
|
16751
17015
|
if (!Number.isFinite(operatorUid) || operatorUid <= 0) {
|
|
16752
17016
|
process.stderr.write(`[vault-broker] SWITCHROOM_BROKER_OPERATOR_UID='${operatorUidStr}' is not a positive integer; skipping operator listener
|