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.
- package/README.md +54 -61
- 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/drive-write-pretool.mjs +5418 -0
- package/dist/cli/switchroom.js +8890 -5560
- 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/dispatch.test.ts +1 -1
- package/telegram-plugin/admin-commands/index.ts +9 -5
- package/telegram-plugin/auth-snapshot-format.ts +612 -0
- package/telegram-plugin/auto-fallback-fleet.ts +215 -0
- package/telegram-plugin/auto-fallback.ts +28 -301
- package/telegram-plugin/dist/gateway/gateway.js +17453 -15100
- package/telegram-plugin/fleet-fallback-gate.ts +105 -0
- package/telegram-plugin/gateway/approval-callback.test.ts +104 -0
- package/telegram-plugin/gateway/approval-callback.ts +31 -3
- 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 +905 -0
- package/telegram-plugin/gateway/auth-line.ts +123 -0
- package/telegram-plugin/gateway/auth-status-adapter.ts +101 -0
- package/telegram-plugin/gateway/boot-card.ts +23 -37
- package/telegram-plugin/gateway/boot-probes.ts +9 -12
- package/telegram-plugin/gateway/diff-preview-card.test.ts +192 -0
- package/telegram-plugin/gateway/diff-preview-card.ts +170 -0
- package/telegram-plugin/gateway/drive-write-approval.test.ts +312 -0
- package/telegram-plugin/gateway/drive-write-approval.ts +243 -0
- package/telegram-plugin/gateway/folder-picker-handler.test.ts +314 -0
- package/telegram-plugin/gateway/folder-picker-handler.ts +348 -0
- package/telegram-plugin/gateway/gateway.ts +1156 -938
- package/telegram-plugin/gateway/hostd-dispatch.ts +244 -0
- package/telegram-plugin/gateway/ipc-protocol.ts +83 -2
- package/telegram-plugin/gateway/ipc-server.ts +69 -0
- package/telegram-plugin/hooks/sandbox-hint-posttool.mjs +103 -12
- package/telegram-plugin/hooks/tool-label-pretool.mjs +11 -0
- package/telegram-plugin/hooks/wedge-detect-posttool.mjs +303 -0
- package/telegram-plugin/model-unavailable.ts +28 -12
- 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/silence-poke.ts +153 -1
- 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-format2.test.ts +156 -0
- package/telegram-plugin/tests/auth-command-vernacular.test.ts +531 -0
- package/telegram-plugin/tests/auth-snapshot-format.test.ts +429 -0
- package/telegram-plugin/tests/auth-status-adapter.test.ts +129 -0
- package/telegram-plugin/tests/auto-fallback-fleet.test.ts +211 -0
- package/telegram-plugin/tests/auto-fallback.test.ts +60 -358
- package/telegram-plugin/tests/boot-probes.test.ts +27 -22
- package/telegram-plugin/tests/fleet-fallback-gate.test.ts +197 -0
- package/telegram-plugin/tests/model-unavailable.test.ts +30 -5
- package/telegram-plugin/tests/permission-title.test.ts +31 -0
- package/telegram-plugin/tests/quota-check.test.ts +5 -35
- package/telegram-plugin/tests/sandbox-hint-posttool.test.ts +212 -2
- package/telegram-plugin/tests/silence-poke.test.ts +237 -0
- package/telegram-plugin/tests/turn-flush-safety.test.ts +112 -0
- package/telegram-plugin/turn-flush-safety.ts +55 -1
- package/telegram-plugin/uat/SETUP.md +35 -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/auto-fallback-dispatcher.ts +0 -68
- 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/auto-fallback-dispatcher.e2e.test.ts +0 -183
- 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
|
@@ -6932,12 +6932,12 @@ var require_public_api = __commonJS((exports) => {
|
|
|
6932
6932
|
});
|
|
6933
6933
|
|
|
6934
6934
|
// src/agent-scheduler/index.ts
|
|
6935
|
-
import { resolve as
|
|
6935
|
+
import { resolve as resolve4, join } from "node:path";
|
|
6936
6936
|
|
|
6937
6937
|
// src/config/loader.ts
|
|
6938
|
-
import { readFileSync, existsSync } from "node:fs";
|
|
6938
|
+
import { readFileSync as readFileSync2, existsSync as existsSync3 } from "node:fs";
|
|
6939
6939
|
import { homedir } from "node:os";
|
|
6940
|
-
import { resolve } from "node:path";
|
|
6940
|
+
import { resolve as resolve3 } from "node:path";
|
|
6941
6941
|
|
|
6942
6942
|
// node_modules/.bun/yaml@2.8.3/node_modules/yaml/dist/index.js
|
|
6943
6943
|
var composer = require_composer();
|
|
@@ -11012,7 +11012,7 @@ var AgentHooksSchema = exports_external.object({
|
|
|
11012
11012
|
SessionEnd: exports_external.array(HookEntrySchema).optional()
|
|
11013
11013
|
}).catchall(exports_external.array(HookEntrySchema)).optional();
|
|
11014
11014
|
var SubagentSchema = exports_external.object({
|
|
11015
|
-
description: exports_external.string().describe("When the main agent should delegate to this sub-agent"),
|
|
11015
|
+
description: exports_external.string().optional().describe("When the main agent should delegate to this sub-agent"),
|
|
11016
11016
|
model: exports_external.string().optional().describe("Model: 'sonnet', 'opus', 'haiku', full ID, or 'inherit' (default)"),
|
|
11017
11017
|
background: exports_external.boolean().optional().describe("Run in background by default (non-blocking). Default false"),
|
|
11018
11018
|
isolation: exports_external.enum(["worktree"]).optional().describe("'worktree' gives the sub-agent its own git branch"),
|
|
@@ -11091,13 +11091,20 @@ var ChannelsSchema = exports_external.object({
|
|
|
11091
11091
|
}).optional();
|
|
11092
11092
|
var TIMEZONE_REGEX = /^UTC$|^[A-Z][A-Za-z0-9_+-]+(\/[A-Z][A-Za-z0-9_+-]+){1,2}$/;
|
|
11093
11093
|
var ApproverIdSchema = exports_external.union([exports_external.number(), exports_external.string().regex(/^\d+$/)]);
|
|
11094
|
-
var
|
|
11094
|
+
var GoogleWorkspaceTierSchema = exports_external.enum([
|
|
11095
|
+
"core",
|
|
11096
|
+
"extended",
|
|
11097
|
+
"complete"
|
|
11098
|
+
]);
|
|
11099
|
+
var GoogleWorkspaceConfigSchema = exports_external.object({
|
|
11095
11100
|
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')"),
|
|
11096
11101
|
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')"),
|
|
11097
|
-
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.")
|
|
11102
|
+
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."),
|
|
11103
|
+
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).")
|
|
11098
11104
|
}).optional();
|
|
11099
|
-
var
|
|
11100
|
-
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.")
|
|
11105
|
+
var AgentGoogleWorkspaceConfigSchema = exports_external.object({
|
|
11106
|
+
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."),
|
|
11107
|
+
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.")
|
|
11101
11108
|
}).optional();
|
|
11102
11109
|
var ReactionsSchema = exports_external.object({
|
|
11103
11110
|
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."),
|
|
@@ -11151,6 +11158,12 @@ var profileFields = {
|
|
|
11151
11158
|
claude_md_raw: exports_external.string().optional(),
|
|
11152
11159
|
cli_args: exports_external.array(exports_external.string()).optional(),
|
|
11153
11160
|
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']."),
|
|
11161
|
+
resources: exports_external.object({
|
|
11162
|
+
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)."),
|
|
11163
|
+
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."),
|
|
11164
|
+
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)."),
|
|
11165
|
+
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).")
|
|
11166
|
+
}).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."),
|
|
11154
11167
|
experimental: exports_external.object({
|
|
11155
11168
|
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)."),
|
|
11156
11169
|
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.")
|
|
@@ -11164,13 +11177,13 @@ var AgentSchema = exports_external.object({
|
|
|
11164
11177
|
bot_token: exports_external.string().optional().describe("Per-agent Telegram bot token or vault reference (overrides global telegram.bot_token)"),
|
|
11165
11178
|
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')."),
|
|
11166
11179
|
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."),
|
|
11167
|
-
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 ...'."),
|
|
11168
11180
|
auth: exports_external.object({
|
|
11169
|
-
|
|
11170
|
-
}).optional().describe("Account routing for switchroom-auth-broker.
|
|
11181
|
+
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.")
|
|
11182
|
+
}).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)."),
|
|
11171
11183
|
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."),
|
|
11172
11184
|
topic_name: exports_external.string().describe("Telegram forum topic display name"),
|
|
11173
11185
|
topic_emoji: exports_external.string().optional().describe("Emoji for the topic (e.g., '\uD83C\uDFCB️')"),
|
|
11186
|
+
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."),
|
|
11174
11187
|
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."),
|
|
11175
11188
|
topic_id: exports_external.number().optional().describe("Telegram topic thread ID (auto-populated by switchroom topics sync)"),
|
|
11176
11189
|
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."),
|
|
@@ -11217,7 +11230,8 @@ var AgentSchema = exports_external.object({
|
|
|
11217
11230
|
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."),
|
|
11218
11231
|
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']."),
|
|
11219
11232
|
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."),
|
|
11220
|
-
drive:
|
|
11233
|
+
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."),
|
|
11234
|
+
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)."),
|
|
11221
11235
|
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({
|
|
11222
11236
|
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."),
|
|
11223
11237
|
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.")
|
|
@@ -11225,7 +11239,13 @@ var AgentSchema = exports_external.object({
|
|
|
11225
11239
|
experimental: exports_external.object({
|
|
11226
11240
|
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."),
|
|
11227
11241
|
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.")
|
|
11228
|
-
}).optional().describe("Opt-in flags for experimental / legacy behaviours. Cascades through " + "defaults → profile → per-agent.")
|
|
11242
|
+
}).optional().describe("Opt-in flags for experimental / legacy behaviours. Cascades through " + "defaults → profile → per-agent."),
|
|
11243
|
+
resources: exports_external.object({
|
|
11244
|
+
memory: exports_external.string().regex(/^\d+(\.\d+)?[kmgKMG]?$/).optional(),
|
|
11245
|
+
memory_reservation: exports_external.string().regex(/^\d+(\.\d+)?[kmgKMG]?$/).optional(),
|
|
11246
|
+
pids_limit: exports_external.number().int().positive().optional(),
|
|
11247
|
+
cpus: exports_external.number().positive().optional()
|
|
11248
|
+
}).optional()
|
|
11229
11249
|
});
|
|
11230
11250
|
var TelegramConfigSchema = exports_external.object({
|
|
11231
11251
|
bot_token: exports_external.string().describe("Telegram bot token or vault reference (e.g., 'vault:telegram-bot-token')"),
|
|
@@ -11266,7 +11286,7 @@ var QuotaConfigSchema = exports_external.object({
|
|
|
11266
11286
|
monthly_budget_usd: exports_external.number().positive().optional().describe("Monthly USD spend budget. If unset, the greeting shows raw usage only.")
|
|
11267
11287
|
});
|
|
11268
11288
|
var HostControlConfigSchema = exports_external.object({
|
|
11269
|
-
enabled: exports_external.boolean().optional().describe("Opt-in to the host-control daemon. Default: false
|
|
11289
|
+
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).")
|
|
11270
11290
|
});
|
|
11271
11291
|
var SwitchroomConfigSchema = exports_external.object({
|
|
11272
11292
|
switchroom: exports_external.object({
|
|
@@ -11278,9 +11298,28 @@ var SwitchroomConfigSchema = exports_external.object({
|
|
|
11278
11298
|
telegram: TelegramConfigSchema,
|
|
11279
11299
|
memory: MemoryBackendConfigSchema.optional(),
|
|
11280
11300
|
vault: VaultConfigSchema.optional(),
|
|
11281
|
-
|
|
11301
|
+
auth: exports_external.object({
|
|
11302
|
+
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>`."),
|
|
11303
|
+
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."),
|
|
11304
|
+
consumers: exports_external.array(exports_external.object({
|
|
11305
|
+
name: exports_external.string().regex(/^[a-zA-Z0-9][a-zA-Z0-9_-]*$/, {
|
|
11306
|
+
message: "Consumer name must be a path-safe slug (letters, digits, underscore, hyphen)"
|
|
11307
|
+
}).describe("Socket-path identity; binds at /run/switchroom/auth-broker/<name>/sock"),
|
|
11308
|
+
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."),
|
|
11309
|
+
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).")
|
|
11310
|
+
})).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.")
|
|
11311
|
+
}).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)."),
|
|
11312
|
+
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."),
|
|
11313
|
+
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)."),
|
|
11282
11314
|
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."),
|
|
11283
11315
|
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."),
|
|
11316
|
+
google_accounts: exports_external.record(exports_external.string().regex(/^[^@\s:]+@[^@\s:]+\.[^@\s:]+$/, {
|
|
11317
|
+
message: "Account key must be a Google account email like 'alice@example.com' (colons not allowed)"
|
|
11318
|
+
}).transform((v) => v.trim().toLowerCase()), exports_external.object({
|
|
11319
|
+
enabled_for: exports_external.array(exports_external.string().regex(/^[a-z0-9][a-z0-9_-]{0,50}$/, {
|
|
11320
|
+
message: "Agent name must match the standard agent-name pattern"
|
|
11321
|
+
})).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.")
|
|
11322
|
+
})).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')."),
|
|
11284
11323
|
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."),
|
|
11285
11324
|
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."),
|
|
11286
11325
|
agents: exports_external.record(exports_external.string().regex(/^[a-z0-9][a-z0-9_-]{0,50}$/, {
|
|
@@ -11288,6 +11327,158 @@ var SwitchroomConfigSchema = exports_external.object({
|
|
|
11288
11327
|
}), AgentSchema).describe("Map of agent name to agent configuration")
|
|
11289
11328
|
});
|
|
11290
11329
|
|
|
11330
|
+
// src/config/paths.ts
|
|
11331
|
+
import { existsSync } from "node:fs";
|
|
11332
|
+
import { resolve } from "node:path";
|
|
11333
|
+
var DEFAULT_STATE_DIR = ".switchroom";
|
|
11334
|
+
var LEGACY_STATE_DIR = ".clerk";
|
|
11335
|
+
function home() {
|
|
11336
|
+
return process.env.HOME ?? "/root";
|
|
11337
|
+
}
|
|
11338
|
+
function resolveDualPath(pathStr) {
|
|
11339
|
+
const h = home();
|
|
11340
|
+
if (pathStr.startsWith("~/")) {
|
|
11341
|
+
const rest = pathStr.slice(2);
|
|
11342
|
+
const absolute = resolve(h, rest);
|
|
11343
|
+
if (rest.startsWith(`${DEFAULT_STATE_DIR}/`)) {
|
|
11344
|
+
const frag = rest.slice(DEFAULT_STATE_DIR.length + 1);
|
|
11345
|
+
if (!existsSync(absolute)) {
|
|
11346
|
+
const legacy = resolve(h, LEGACY_STATE_DIR, frag);
|
|
11347
|
+
if (existsSync(legacy))
|
|
11348
|
+
return legacy;
|
|
11349
|
+
}
|
|
11350
|
+
}
|
|
11351
|
+
return absolute;
|
|
11352
|
+
}
|
|
11353
|
+
return resolve(pathStr);
|
|
11354
|
+
}
|
|
11355
|
+
|
|
11356
|
+
// src/config/overlay-loader.ts
|
|
11357
|
+
import { existsSync as existsSync2, readFileSync, readdirSync, statSync } from "node:fs";
|
|
11358
|
+
import { resolve as resolve2 } from "node:path";
|
|
11359
|
+
|
|
11360
|
+
// src/config/overlay-schema.ts
|
|
11361
|
+
var OverlayDocSchema = exports_external.object({
|
|
11362
|
+
schedule: exports_external.array(ScheduleEntrySchema).optional(),
|
|
11363
|
+
skills: exports_external.array(exports_external.string()).optional()
|
|
11364
|
+
}).strict();
|
|
11365
|
+
|
|
11366
|
+
// src/config/overlay-loader.ts
|
|
11367
|
+
var OVERLAY_SOURCE = Symbol.for("switchroom.config.overlay-source");
|
|
11368
|
+
function overlayDirFor(agentName, subdir) {
|
|
11369
|
+
const base = resolveDualPath(`~/.switchroom/agents/${agentName}/${subdir}`);
|
|
11370
|
+
return resolve2(base);
|
|
11371
|
+
}
|
|
11372
|
+
function listYamlFiles(dir) {
|
|
11373
|
+
if (!existsSync2(dir))
|
|
11374
|
+
return [];
|
|
11375
|
+
let entries;
|
|
11376
|
+
try {
|
|
11377
|
+
entries = readdirSync(dir);
|
|
11378
|
+
} catch {
|
|
11379
|
+
return [];
|
|
11380
|
+
}
|
|
11381
|
+
const out = [];
|
|
11382
|
+
for (const name of entries) {
|
|
11383
|
+
if (!/\.ya?ml$/i.test(name))
|
|
11384
|
+
continue;
|
|
11385
|
+
const full = resolve2(dir, name);
|
|
11386
|
+
try {
|
|
11387
|
+
if (statSync(full).isFile())
|
|
11388
|
+
out.push(full);
|
|
11389
|
+
} catch {}
|
|
11390
|
+
}
|
|
11391
|
+
return out.sort();
|
|
11392
|
+
}
|
|
11393
|
+
function stampOverlay(entry) {
|
|
11394
|
+
Object.defineProperty(entry, OVERLAY_SOURCE, {
|
|
11395
|
+
value: true,
|
|
11396
|
+
enumerable: false,
|
|
11397
|
+
configurable: false,
|
|
11398
|
+
writable: false
|
|
11399
|
+
});
|
|
11400
|
+
return entry;
|
|
11401
|
+
}
|
|
11402
|
+
function applyAgentOverlays(config) {
|
|
11403
|
+
const warnings = [];
|
|
11404
|
+
const agents = config.agents ?? {};
|
|
11405
|
+
for (const [agentName, agentCfg] of Object.entries(agents)) {
|
|
11406
|
+
try {
|
|
11407
|
+
const scheduleDir = overlayDirFor(agentName, "schedule.d");
|
|
11408
|
+
const files = listYamlFiles(scheduleDir);
|
|
11409
|
+
if (files.length > 0) {
|
|
11410
|
+
const merged = [...agentCfg.schedule ?? []];
|
|
11411
|
+
for (const file of files) {
|
|
11412
|
+
try {
|
|
11413
|
+
const raw = readFileSync(file, "utf-8");
|
|
11414
|
+
const parsed = $parse(raw);
|
|
11415
|
+
const doc = OverlayDocSchema.parse(parsed);
|
|
11416
|
+
for (const entry of doc.schedule ?? []) {
|
|
11417
|
+
if (entry.secrets && entry.secrets.length > 0) {
|
|
11418
|
+
const w = {
|
|
11419
|
+
agent: agentName,
|
|
11420
|
+
file,
|
|
11421
|
+
reason: "Overlay schedule entry declares secrets — dropped pending Phase E operator approval"
|
|
11422
|
+
};
|
|
11423
|
+
warnings.push(w);
|
|
11424
|
+
console.warn(`[switchroom] overlay-loader: agent='${agentName}' file='${file}': ${w.reason}`);
|
|
11425
|
+
continue;
|
|
11426
|
+
}
|
|
11427
|
+
merged.push(stampOverlay(entry));
|
|
11428
|
+
}
|
|
11429
|
+
} catch (err) {
|
|
11430
|
+
const reason = err instanceof ZodError ? `schema rejection: ${err.errors.map((e) => `${e.path.join(".")}: ${e.message}`).join("; ")}` : `parse error: ${err.message}`;
|
|
11431
|
+
warnings.push({ agent: agentName, file, reason });
|
|
11432
|
+
console.warn(`[switchroom] overlay-loader: agent='${agentName}' file='${file}': ${reason}`);
|
|
11433
|
+
}
|
|
11434
|
+
}
|
|
11435
|
+
agentCfg.schedule = merged;
|
|
11436
|
+
}
|
|
11437
|
+
} catch (err) {
|
|
11438
|
+
warnings.push({
|
|
11439
|
+
agent: agentName,
|
|
11440
|
+
file: "(agent schedule overlay scan)",
|
|
11441
|
+
reason: `unexpected error: ${err.message}`
|
|
11442
|
+
});
|
|
11443
|
+
console.warn(`[switchroom] overlay-loader: agent='${agentName}' schedule.d: unexpected error: ${err.message}`);
|
|
11444
|
+
}
|
|
11445
|
+
try {
|
|
11446
|
+
const skillsDir = overlayDirFor(agentName, "skills.d");
|
|
11447
|
+
const skillFiles = listYamlFiles(skillsDir);
|
|
11448
|
+
if (skillFiles.length === 0) {} else {
|
|
11449
|
+
const merged = [...agentCfg.skills ?? []];
|
|
11450
|
+
const seen = new Set(merged);
|
|
11451
|
+
for (const file of skillFiles) {
|
|
11452
|
+
try {
|
|
11453
|
+
const raw = readFileSync(file, "utf-8");
|
|
11454
|
+
const parsed = $parse(raw);
|
|
11455
|
+
const doc = OverlayDocSchema.parse(parsed);
|
|
11456
|
+
for (const skillName of doc.skills ?? []) {
|
|
11457
|
+
if (seen.has(skillName))
|
|
11458
|
+
continue;
|
|
11459
|
+
seen.add(skillName);
|
|
11460
|
+
merged.push(skillName);
|
|
11461
|
+
}
|
|
11462
|
+
} catch (err) {
|
|
11463
|
+
const reason = err instanceof ZodError ? `schema rejection: ${err.errors.map((e) => `${e.path.join(".")}: ${e.message}`).join("; ")}` : `parse error: ${err.message}`;
|
|
11464
|
+
warnings.push({ agent: agentName, file, reason });
|
|
11465
|
+
console.warn(`[switchroom] overlay-loader: agent='${agentName}' file='${file}': ${reason}`);
|
|
11466
|
+
}
|
|
11467
|
+
}
|
|
11468
|
+
agentCfg.skills = merged;
|
|
11469
|
+
}
|
|
11470
|
+
} catch (err) {
|
|
11471
|
+
warnings.push({
|
|
11472
|
+
agent: agentName,
|
|
11473
|
+
file: "(agent skills overlay scan)",
|
|
11474
|
+
reason: `unexpected error: ${err.message}`
|
|
11475
|
+
});
|
|
11476
|
+
console.warn(`[switchroom] overlay-loader: agent='${agentName}' skills.d: unexpected error: ${err.message}`);
|
|
11477
|
+
}
|
|
11478
|
+
}
|
|
11479
|
+
return { config, warnings };
|
|
11480
|
+
}
|
|
11481
|
+
|
|
11291
11482
|
// src/config/loader.ts
|
|
11292
11483
|
class ConfigError extends Error {
|
|
11293
11484
|
details;
|
|
@@ -11303,27 +11494,65 @@ function formatZodErrors(error) {
|
|
|
11303
11494
|
return ` ${path}: ${e.message}`;
|
|
11304
11495
|
});
|
|
11305
11496
|
}
|
|
11497
|
+
function coerceLegacyGoogleWorkspaceKeys(parsed, filePath) {
|
|
11498
|
+
const stableStringify = (v) => {
|
|
11499
|
+
if (v === null || typeof v !== "object")
|
|
11500
|
+
return JSON.stringify(v);
|
|
11501
|
+
if (Array.isArray(v))
|
|
11502
|
+
return `[${v.map(stableStringify).join(",")}]`;
|
|
11503
|
+
const obj = v;
|
|
11504
|
+
const keys = Object.keys(obj).sort();
|
|
11505
|
+
return `{${keys.map((k) => `${JSON.stringify(k)}:${stableStringify(obj[k])}`).join(",")}}`;
|
|
11506
|
+
};
|
|
11507
|
+
const aliasInPlace = (obj, where) => {
|
|
11508
|
+
const a = obj.drive;
|
|
11509
|
+
const b = obj.google_workspace;
|
|
11510
|
+
if (a !== undefined && b !== undefined) {
|
|
11511
|
+
if (stableStringify(a) !== stableStringify(b)) {
|
|
11512
|
+
throw new ConfigError(`Both \`drive:\` and \`google_workspace:\` are set on ${where} in ${filePath} with different values.`, [
|
|
11513
|
+
" These are aliases — pick one and remove the other.",
|
|
11514
|
+
" `google_workspace:` is the RFC G canonical key; `drive:` is the legacy alias.",
|
|
11515
|
+
" Allowed during transition: setting both with identical values."
|
|
11516
|
+
]);
|
|
11517
|
+
}
|
|
11518
|
+
return;
|
|
11519
|
+
}
|
|
11520
|
+
if (a !== undefined && b === undefined)
|
|
11521
|
+
obj.google_workspace = a;
|
|
11522
|
+
if (b !== undefined && a === undefined)
|
|
11523
|
+
obj.drive = b;
|
|
11524
|
+
};
|
|
11525
|
+
aliasInPlace(parsed, "the top level");
|
|
11526
|
+
const agents = parsed.agents;
|
|
11527
|
+
if (agents && typeof agents === "object" && !Array.isArray(agents)) {
|
|
11528
|
+
for (const [name, agent] of Object.entries(agents)) {
|
|
11529
|
+
if (agent && typeof agent === "object" && !Array.isArray(agent)) {
|
|
11530
|
+
aliasInPlace(agent, `agent \`${name}\``);
|
|
11531
|
+
}
|
|
11532
|
+
}
|
|
11533
|
+
}
|
|
11534
|
+
}
|
|
11306
11535
|
function findConfigFile(startDir) {
|
|
11307
11536
|
const envPath = process.env.SWITCHROOM_CONFIG;
|
|
11308
|
-
const
|
|
11309
|
-
const userDir =
|
|
11537
|
+
const home2 = homedir();
|
|
11538
|
+
const userDir = resolve3(home2, ".switchroom");
|
|
11310
11539
|
const searchPaths = [
|
|
11311
|
-
envPath ?
|
|
11312
|
-
startDir ?
|
|
11313
|
-
startDir ?
|
|
11314
|
-
startDir ?
|
|
11315
|
-
startDir ?
|
|
11316
|
-
|
|
11317
|
-
|
|
11318
|
-
|
|
11319
|
-
|
|
11320
|
-
|
|
11321
|
-
|
|
11322
|
-
|
|
11323
|
-
|
|
11540
|
+
envPath ? resolve3(envPath) : null,
|
|
11541
|
+
startDir ? resolve3(startDir, "switchroom.yaml") : null,
|
|
11542
|
+
startDir ? resolve3(startDir, "switchroom.yml") : null,
|
|
11543
|
+
startDir ? resolve3(startDir, "clerk.yaml") : null,
|
|
11544
|
+
startDir ? resolve3(startDir, "clerk.yml") : null,
|
|
11545
|
+
resolve3(process.cwd(), "switchroom.yaml"),
|
|
11546
|
+
resolve3(process.cwd(), "switchroom.yml"),
|
|
11547
|
+
resolve3(process.cwd(), "clerk.yaml"),
|
|
11548
|
+
resolve3(process.cwd(), "clerk.yml"),
|
|
11549
|
+
resolve3(userDir, "switchroom.yaml"),
|
|
11550
|
+
resolve3(userDir, "switchroom.yml"),
|
|
11551
|
+
resolve3(userDir, "clerk.yaml"),
|
|
11552
|
+
resolve3(userDir, "clerk.yml")
|
|
11324
11553
|
].filter(Boolean);
|
|
11325
11554
|
for (const path of searchPaths) {
|
|
11326
|
-
if (
|
|
11555
|
+
if (existsSync3(path)) {
|
|
11327
11556
|
return path;
|
|
11328
11557
|
}
|
|
11329
11558
|
}
|
|
@@ -11331,12 +11560,12 @@ function findConfigFile(startDir) {
|
|
|
11331
11560
|
}
|
|
11332
11561
|
function loadConfig(configPath) {
|
|
11333
11562
|
const filePath = configPath ?? findConfigFile();
|
|
11334
|
-
if (!
|
|
11563
|
+
if (!existsSync3(filePath)) {
|
|
11335
11564
|
throw new ConfigError(`Config file not found: ${filePath}`);
|
|
11336
11565
|
}
|
|
11337
11566
|
let raw;
|
|
11338
11567
|
try {
|
|
11339
|
-
raw =
|
|
11568
|
+
raw = readFileSync2(filePath, "utf-8");
|
|
11340
11569
|
} catch (err) {
|
|
11341
11570
|
throw new ConfigError(`Failed to read config file: ${filePath}`, [
|
|
11342
11571
|
` ${err.message}`
|
|
@@ -11355,14 +11584,20 @@ function loadConfig(configPath) {
|
|
|
11355
11584
|
obj.switchroom = obj.clerk;
|
|
11356
11585
|
delete obj.clerk;
|
|
11357
11586
|
}
|
|
11587
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
11588
|
+
coerceLegacyGoogleWorkspaceKeys(parsed, filePath);
|
|
11589
|
+
}
|
|
11590
|
+
let config;
|
|
11358
11591
|
try {
|
|
11359
|
-
|
|
11592
|
+
config = SwitchroomConfigSchema.parse(parsed);
|
|
11360
11593
|
} catch (err) {
|
|
11361
11594
|
if (err instanceof ZodError) {
|
|
11362
11595
|
throw new ConfigError("Invalid switchroom.yaml configuration", formatZodErrors(err));
|
|
11363
11596
|
}
|
|
11364
11597
|
throw err;
|
|
11365
11598
|
}
|
|
11599
|
+
applyAgentOverlays(config);
|
|
11600
|
+
return config;
|
|
11366
11601
|
}
|
|
11367
11602
|
|
|
11368
11603
|
// src/scheduler/dispatch.ts
|
|
@@ -11657,6 +11892,11 @@ function mergeAgentConfig(defaultsIn, agentIn) {
|
|
|
11657
11892
|
}
|
|
11658
11893
|
merged.reactions = combined;
|
|
11659
11894
|
}
|
|
11895
|
+
if (defaults.resources || merged.resources) {
|
|
11896
|
+
const d = defaults.resources ?? {};
|
|
11897
|
+
const a = merged.resources ?? {};
|
|
11898
|
+
merged.resources = { ...d, ...a };
|
|
11899
|
+
}
|
|
11660
11900
|
if (defaults.experimental || merged.experimental) {
|
|
11661
11901
|
const d = defaults.experimental ?? {};
|
|
11662
11902
|
const a = merged.experimental ?? {};
|
|
@@ -11830,17 +12070,17 @@ function createInjectIpcClient(options) {
|
|
|
11830
12070
|
return Promise.resolve(true);
|
|
11831
12071
|
if (closed)
|
|
11832
12072
|
return Promise.resolve(false);
|
|
11833
|
-
return new Promise((
|
|
12073
|
+
return new Promise((resolve4) => {
|
|
11834
12074
|
const start = Date.now();
|
|
11835
12075
|
const timer = setInterval(() => {
|
|
11836
12076
|
if (connected) {
|
|
11837
12077
|
clearInterval(timer);
|
|
11838
|
-
|
|
12078
|
+
resolve4(true);
|
|
11839
12079
|
return;
|
|
11840
12080
|
}
|
|
11841
12081
|
if (closed || Date.now() - start >= timeoutMs) {
|
|
11842
12082
|
clearInterval(timer);
|
|
11843
|
-
|
|
12083
|
+
resolve4(connected);
|
|
11844
12084
|
}
|
|
11845
12085
|
}, 50);
|
|
11846
12086
|
if (typeof timer.unref === "function") {
|
|
@@ -11867,8 +12107,8 @@ function createInjectIpcClient(options) {
|
|
|
11867
12107
|
import {
|
|
11868
12108
|
closeSync,
|
|
11869
12109
|
openSync,
|
|
11870
|
-
readFileSync as
|
|
11871
|
-
statSync,
|
|
12110
|
+
readFileSync as readFileSync3,
|
|
12111
|
+
statSync as statSync2,
|
|
11872
12112
|
unlinkSync,
|
|
11873
12113
|
writeSync,
|
|
11874
12114
|
mkdirSync as mkdirSync2
|
|
@@ -11876,7 +12116,7 @@ import {
|
|
|
11876
12116
|
import { dirname as dirname2 } from "node:path";
|
|
11877
12117
|
function readContainerBootTimeMs() {
|
|
11878
12118
|
try {
|
|
11879
|
-
const stat1 =
|
|
12119
|
+
const stat1 = readFileSync3("/proc/1/stat", "utf8");
|
|
11880
12120
|
const lastParen = stat1.lastIndexOf(")");
|
|
11881
12121
|
if (lastParen < 0)
|
|
11882
12122
|
return null;
|
|
@@ -11884,7 +12124,7 @@ function readContainerBootTimeMs() {
|
|
|
11884
12124
|
const starttimeTicks = Number(after[19]);
|
|
11885
12125
|
if (!Number.isFinite(starttimeTicks))
|
|
11886
12126
|
return null;
|
|
11887
|
-
const procStat =
|
|
12127
|
+
const procStat = readFileSync3("/proc/stat", "utf8");
|
|
11888
12128
|
const btimeLine = procStat.split(`
|
|
11889
12129
|
`).find((l) => l.startsWith("btime "));
|
|
11890
12130
|
if (!btimeLine)
|
|
@@ -11919,7 +12159,7 @@ function acquireLock(path, pid = process.pid, options = {}) {
|
|
|
11919
12159
|
}
|
|
11920
12160
|
if (bootTimeMs != null) {
|
|
11921
12161
|
try {
|
|
11922
|
-
const lockMtime =
|
|
12162
|
+
const lockMtime = statSync2(path).mtimeMs;
|
|
11923
12163
|
if (lockMtime < bootTimeMs - FRESHNESS_MARGIN_MS) {
|
|
11924
12164
|
try {
|
|
11925
12165
|
unlinkSync(path);
|
|
@@ -11930,7 +12170,7 @@ function acquireLock(path, pid = process.pid, options = {}) {
|
|
|
11930
12170
|
}
|
|
11931
12171
|
let holderPid;
|
|
11932
12172
|
try {
|
|
11933
|
-
const raw =
|
|
12173
|
+
const raw = readFileSync3(path, "utf8").trim();
|
|
11934
12174
|
const parsed = Number.parseInt(raw, 10);
|
|
11935
12175
|
if (Number.isInteger(parsed) && parsed > 0)
|
|
11936
12176
|
holderPid = parsed;
|
|
@@ -12224,7 +12464,7 @@ async function main() {
|
|
|
12224
12464
|
process.once("SIGINT", cleanup);
|
|
12225
12465
|
return;
|
|
12226
12466
|
}
|
|
12227
|
-
const sink = new JsonlAuditSink(
|
|
12467
|
+
const sink = new JsonlAuditSink(resolve4(jsonlPath));
|
|
12228
12468
|
const ipcClient = createInjectIpcClient({
|
|
12229
12469
|
socketPath,
|
|
12230
12470
|
log: (m) => process.stderr.write(`agent-scheduler: ${m}
|
|
@@ -12232,7 +12472,7 @@ async function main() {
|
|
|
12232
12472
|
});
|
|
12233
12473
|
const dispatcher = ipcDispatcher(ipcClient);
|
|
12234
12474
|
const replayWindowMin = Number.parseInt(process.env.SWITCHROOM_AGENT_SCHEDULER_REPLAY_MIN ?? "30", 10);
|
|
12235
|
-
const recentFires = readRecentFires(
|
|
12475
|
+
const recentFires = readRecentFires(resolve4(jsonlPath));
|
|
12236
12476
|
const missed = findMissedFires({
|
|
12237
12477
|
entries,
|
|
12238
12478
|
recentFires,
|