switchroom 0.15.36 → 0.15.38

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. package/dist/agent-scheduler/index.js +10 -9
  2. package/dist/auth-broker/index.js +9 -9
  3. package/dist/cli/autoaccept-poll.js +13 -7
  4. package/dist/cli/notion-write-pretool.mjs +9 -9
  5. package/dist/cli/switchroom.js +480 -217
  6. package/dist/cli/ui/index.html +87 -17
  7. package/dist/host-control/main.js +10 -10
  8. package/dist/vault/approvals/kernel-server.js +9 -9
  9. package/dist/vault/broker/server.js +9 -9
  10. package/package.json +1 -1
  11. package/profiles/_base/cron-session.sh.hbs +1 -1
  12. package/profiles/_base/start.sh.hbs +1 -1
  13. package/profiles/_shared/agent-self-service.md.hbs +25 -0
  14. package/skills/switchroom-manage/SKILL.md +1 -1
  15. package/skills/switchroom-runtime/SKILL.md +1 -1
  16. package/telegram-plugin/answer-stream.ts +1 -1
  17. package/telegram-plugin/bridge/bridge.ts +50 -1
  18. package/telegram-plugin/bridge/ipc-client.ts +4 -1
  19. package/telegram-plugin/bridge/tool-filter.ts +77 -0
  20. package/telegram-plugin/chat-lock.ts +1 -1
  21. package/telegram-plugin/credits-watch.ts +1 -1
  22. package/telegram-plugin/dist/bridge/bridge.js +60 -3
  23. package/telegram-plugin/dist/gateway/gateway.js +753 -207
  24. package/telegram-plugin/dist/server.js +64 -4
  25. package/telegram-plugin/gateway/auto-classify-mid-turn.ts +1 -1
  26. package/telegram-plugin/gateway/boot-card.ts +5 -1
  27. package/telegram-plugin/gateway/boot-probes.ts +62 -0
  28. package/telegram-plugin/gateway/cron-session.ts +1 -1
  29. package/telegram-plugin/gateway/gateway.ts +254 -15
  30. package/telegram-plugin/gateway/grant-restart.ts +1 -1
  31. package/telegram-plugin/gateway/inbound-delivery-machine-dispatch.ts +1 -1
  32. package/telegram-plugin/gateway/inbound-delivery-machine-shadow.ts +1 -1
  33. package/telegram-plugin/gateway/inbound-delivery-machine.ts +1 -1
  34. package/telegram-plugin/gateway/interrupt-defer.ts +1 -1
  35. package/telegram-plugin/gateway/ipc-protocol.ts +12 -0
  36. package/telegram-plugin/gateway/linear-activity.ts +56 -0
  37. package/telegram-plugin/gateway/linear-auth-watch.ts +102 -0
  38. package/telegram-plugin/gateway/linear-setup.ts +196 -0
  39. package/telegram-plugin/gateway/permission-card-origin.ts +62 -0
  40. package/telegram-plugin/gateway/permission-timeout.ts +70 -0
  41. package/telegram-plugin/gateway/prefix-warmup.ts +1 -1
  42. package/telegram-plugin/gateway/webhook-ingest-server.test.ts +1 -1
  43. package/telegram-plugin/gateway/webhook-ingest-server.ts +1 -1
  44. package/telegram-plugin/hooks/subagent-tracker-pretool.mjs +1 -1
  45. package/telegram-plugin/interrupt-marker.ts +1 -1
  46. package/telegram-plugin/over-ping-safety-net.ts +1 -1
  47. package/telegram-plugin/scoped-approval.ts +1 -1
  48. package/telegram-plugin/secret-detect/vault-error.ts +1 -1
  49. package/telegram-plugin/silence-poke.ts +2 -2
  50. package/telegram-plugin/silent-reply-anchor.ts +1 -1
  51. package/telegram-plugin/slot-banner-driver.ts +1 -1
  52. package/telegram-plugin/startup-reset.ts +1 -1
  53. package/telegram-plugin/tests/boot-probes-connections.test.ts +66 -0
  54. package/telegram-plugin/tests/gateway-startup-reset.test.ts +1 -1
  55. package/telegram-plugin/tests/inbound-delivery-machine.test.ts +1 -1
  56. package/telegram-plugin/tests/linear-agent-activity.test.ts +77 -0
  57. package/telegram-plugin/tests/linear-agent-setup.test.ts +132 -0
  58. package/telegram-plugin/tests/linear-auth-watch.test.ts +79 -0
  59. package/telegram-plugin/tests/linear-create-issue.test.ts +3 -1
  60. package/telegram-plugin/tests/permission-card-origin.test.ts +97 -0
  61. package/telegram-plugin/tests/permission-card-routing.test.ts +23 -0
  62. package/telegram-plugin/tests/permission-no-repeat-wiring.test.ts +76 -0
  63. package/telegram-plugin/tests/permission-timeout.test.ts +87 -0
  64. package/telegram-plugin/tests/scoped-approval.test.ts +1 -1
  65. package/telegram-plugin/tests/silence-poke.test.ts +1 -1
  66. package/telegram-plugin/tests/tool-filter.test.ts +87 -0
  67. package/telegram-plugin/tests/turn-flush-safety.test.ts +1 -1
  68. package/telegram-plugin/turn-flush-safety.ts +1 -1
  69. package/telegram-plugin/uat/assertions.ts +1 -1
  70. package/telegram-plugin/uat/scenarios/bg-sub-agent-dispatch-dm.test.ts +1 -1
  71. package/telegram-plugin/uat/scenarios/fuzz-extended-dm.test.ts +1 -1
  72. package/telegram-plugin/uat/scenarios/jtbd-fast-ack-dm.test.ts +1 -1
  73. package/telegram-plugin/uat/scenarios/jtbd-fast-trivial-dm.test.ts +2 -2
  74. package/telegram-plugin/uat/scenarios/jtbd-forwarded-burst-dm.test.ts +1 -1
  75. package/telegram-plugin/uat/scenarios/jtbd-memory-survives-restart-dm.test.ts +1 -1
  76. package/telegram-plugin/uat/scenarios/jtbd-rapid-followup-dm.test.ts +1 -1
  77. package/telegram-plugin/uat/scenarios/jtbd-reflective-status-reaction-dm.test.ts +1 -1
  78. package/telegram-plugin/uat/scenarios/jtbd-wake-audit-content-dm.test.ts +1 -1
@@ -11001,7 +11001,7 @@ var ActionSpecSchema = exports_external.discriminatedUnion("type", [
11001
11001
  var ScheduleEntrySchema = exports_external.object({
11002
11002
  cron: exports_external.string().describe("Cron expression (e.g., '0 8 * * *')"),
11003
11003
  prompt: exports_external.string().optional().describe("Prompt to send at the scheduled time (the escalation prompt when " + "kind=poll; templated with {{diff}}). Required for kind prompt/poll; " + "absent for kind=action (an action has no model fire, so no prompt)."),
11004
- kind: exports_external.enum(["poll", "prompt", "action"]).optional().describe("Tier-0 routing (docs/rfcs/cheap-cron-sessions.md). 'prompt' (default) " + "fires a model turn every tick (Tier 1/2 per `context`). 'poll' runs a " + "model-free deterministic check (requires `poll`) and only escalates to " + "a model fire on a hit. 'action' runs a model-free deterministic verb " + "(requires `action`) that COMPLETES the work and never escalates — zero " + "tokens, no session. poll/prompt tiering is on by default " + "(SWITCHROOM_CHEAP_CRON=0 is the kill-switch); an action is model-free " + "regardless (the kill-switch governs model tiering, not deterministic " + "actions)."),
11004
+ kind: exports_external.enum(["poll", "prompt", "action"]).optional().describe("Tier-0 routing (reference/rfcs/cheap-cron-sessions.md). 'prompt' (default) " + "fires a model turn every tick (Tier 1/2 per `context`). 'poll' runs a " + "model-free deterministic check (requires `poll`) and only escalates to " + "a model fire on a hit. 'action' runs a model-free deterministic verb " + "(requires `action`) that COMPLETES the work and never escalates — zero " + "tokens, no session. poll/prompt tiering is on by default " + "(SWITCHROOM_CHEAP_CRON=0 is the kill-switch); an action is model-free " + "regardless (the kill-switch governs model tiering, not deterministic " + "actions)."),
11005
11005
  poll: PollSpecSchema.optional().describe("Required iff kind=poll. The declarative poll spec."),
11006
11006
  action: ActionSpecSchema.optional().describe("Required iff kind=action. The declarative action spec (telegram-message or webhook)."),
11007
11007
  model: exports_external.string().optional().describe("Cron model hint. Reactivated by SWITCHROOM_CHEAP_CRON (was DEPRECATED/" + "IGNORED in v0.8). A known-cheap id (sonnet/haiku family) routes the " + "fire to a fresh cheap cron session (Tier 1, `context: fresh`); 'opus', " + "a custom id, or unset routes to the agent's live session (Tier 2, " + "`context: agent`) — the conservative default that preserves pre-v0.8 " + "behaviour. Note: a live session's model is fixed at launch, so on Tier " + "2 this is informational. See docs/scheduling.md."),
@@ -11010,7 +11010,7 @@ var ScheduleEntrySchema = exports_external.object({
11010
11010
  topic: exports_external.union([
11011
11011
  exports_external.string().min(1, "topic alias must be non-empty"),
11012
11012
  exports_external.number().int().positive("topic ID must be a positive integer")
11013
- ]).optional().describe("Forum topic this cron fires into when the owning agent is in " + "supergroup-owned mode (channels.telegram.chat_id set). Either a " + 'string alias resolved against `topic_aliases` (e.g. "planning") ' + "or a numeric topic ID. Falls back to the agent's `default_topic_id` " + "when unset. Ignored for agents in fleet-shared or dm_only mode. " + "Alias-resolution happens at config-load — typos surface immediately. " + "See docs/rfcs/supergroup-mode.md.")
11013
+ ]).optional().describe("Forum topic this cron fires into when the owning agent is in " + "supergroup-owned mode (channels.telegram.chat_id set). Either a " + 'string alias resolved against `topic_aliases` (e.g. "planning") ' + "or a numeric topic ID. Falls back to the agent's `default_topic_id` " + "when unset. Ignored for agents in fleet-shared or dm_only mode. " + "Alias-resolution happens at config-load — typos surface immediately. " + "See reference/rfcs/supergroup-mode.md.")
11014
11014
  }).superRefine((entry, ctx) => {
11015
11015
  const kind = entry.kind ?? "prompt";
11016
11016
  if (kind === "poll" && !entry.poll) {
@@ -11183,15 +11183,15 @@ var TelegramChannelSchema = exports_external.object({
11183
11183
  webhook_rate_limit: exports_external.object({
11184
11184
  rpm: exports_external.number().int().positive()
11185
11185
  }).optional().describe("Per-source rate limit for the webhook ingest path (#714). " + "Off by default — when this key is absent the handler skips " + "rate-limit checks entirely. Opt in by setting `rpm` to an " + "integer requests-per-minute (token bucket per (agent, source); " + "burst equal to rpm). When enabled, exceeding the limit returns " + "429 with Retry-After header; first throttle event per " + "(agent, source) per 60s window is written to " + "<agent>/telegram/issues.jsonl. " + "Cascades from defaults.channels.telegram.webhook_rate_limit."),
11186
- webhook_via_gateway: exports_external.boolean().optional().describe("Route verified webhook events to the agent's in-container gateway " + "over a peercred-gated UDS (<agent>/telegram/webhook.sock) instead " + "of having the host-side web receiver write the agent dir directly. " + "Required under the Docker runtime: the receiver runs as the host " + "operator UID and cannot write the per-agent-UID-owned agent dir " + "(EACCES 500) nor connect the gateway socket. When true the gateway " + "(running as the agent UID) becomes the sole writer of " + "webhook-events.jsonl + dedup/cooldown state and also fires " + "webhook_dispatch. Off by default for back-compat with host-runtime " + "installs. See docs/rfcs/webhook-via-gateway-socket.md."),
11187
- webhook_require_edge: exports_external.boolean().optional().describe("Cloudflare-only edge lock: require the X-Switchroom-Edge header " + "(injected by a Cloudflare Transform Rule on hooks.switchroom.ai) to " + "match the operator's edge secret at ~/.switchroom/webhook-edge-secret " + "before any HMAC verification; reject 403 otherwise. Proves the " + "request entered through our Cloudflare edge — the per-agent HMAC " + "alone can't (it proves body provenance, not network path). Stacks " + "on the GitHub-IP WAF + per-agent HMAC. Fail-closed: when required " + "but the secret file is missing/empty every request is rejected. Off " + "by default. See docs/rfcs/webhook-cloudflare-edge-lock.md."),
11186
+ webhook_via_gateway: exports_external.boolean().optional().describe("Route verified webhook events to the agent's in-container gateway " + "over a peercred-gated UDS (<agent>/telegram/webhook.sock) instead " + "of having the host-side web receiver write the agent dir directly. " + "Required under the Docker runtime: the receiver runs as the host " + "operator UID and cannot write the per-agent-UID-owned agent dir " + "(EACCES 500) nor connect the gateway socket. When true the gateway " + "(running as the agent UID) becomes the sole writer of " + "webhook-events.jsonl + dedup/cooldown state and also fires " + "webhook_dispatch. Off by default for back-compat with host-runtime " + "installs. See reference/rfcs/webhook-via-gateway-socket.md."),
11187
+ webhook_require_edge: exports_external.boolean().optional().describe("Cloudflare-only edge lock: require the X-Switchroom-Edge header " + "(injected by a Cloudflare Transform Rule on hooks.switchroom.ai) to " + "match the operator's edge secret at ~/.switchroom/webhook-edge-secret " + "before any HMAC verification; reject 403 otherwise. Proves the " + "request entered through our Cloudflare edge — the per-agent HMAC " + "alone can't (it proves body provenance, not network path). Stacks " + "on the GitHub-IP WAF + per-agent HMAC. Fail-closed: when required " + "but the secret file is missing/empty every request is rejected. Off " + "by default. See reference/rfcs/webhook-cloudflare-edge-lock.md."),
11188
11188
  linear_agent: exports_external.object({
11189
11189
  enabled: exports_external.boolean(),
11190
11190
  token: exports_external.string().describe("vault:<key> reference to the Linear OAuth app token (actor=app). " + "Resolved at runtime via the vault broker (canonically " + "vault:linear/<agent>/token). Never an inline literal."),
11191
11191
  workspace_id: exports_external.string().optional().describe("Optional Linear workspace (organization) id this agent is " + "installed into. Informational — used for setup hints and " + "multi-workspace disambiguation; the token already scopes the " + "app to its workspace."),
11192
11192
  default_team_id: exports_external.string().optional().describe("Optional Linear team id new captured issues file into when the " + "agent doesn't pass an explicit team_id. Unnecessary for a " + "single-team workspace (auto-resolved); set it only when the " + "workspace has multiple teams. Manage via " + "`switchroom linear-agent set-team <agent> <team>`.")
11193
11193
  }).optional().describe("Linear first-class agent integration (#2298). When enabled, the " + "agent appears in a Linear workspace as an app actor (own name/" + "avatar, @-mentionable, delegate-assignable). Linear AgentSessionEvent " + "webhooks (mention / delegation) wake the agent instantly via the " + "same gateway inject path as webhook_dispatch, tagged " + 'meta.source="linear" with the agent_session_id, and the agent ' + "responds with structured AgentActivity (thought/message/complete/" + "error) via the linear_agent_activity MCP tool. Builds the " + "session-lifecycle layer on top of the plain webhook_sources:[linear] " + "+ webhook_dispatch support (#2272). The OAuth app token is stored in " + "the vault and referenced here as vault:linear/<agent>/token; run " + "`switchroom linear-agent setup <agent>` to provision it. Off by " + "default — opt in per agent. Cascades from " + "defaults.channels.telegram.linear_agent."),
11194
- chat_id: exports_external.string().regex(/^-\d+$/, 'supergroup chat_id must be a negative integer as a string (e.g. "-1001234567890")').optional().describe("Per-agent supergroup ID — overrides fleet `telegram.forum_chat_id`. " + "When set, requires `default_topic_id`. Negative integer as string. " + "Forbidden when `dm_only: true`. See docs/rfcs/supergroup-mode.md."),
11194
+ chat_id: exports_external.string().regex(/^-\d+$/, 'supergroup chat_id must be a negative integer as a string (e.g. "-1001234567890")').optional().describe("Per-agent supergroup ID — overrides fleet `telegram.forum_chat_id`. " + "When set, requires `default_topic_id`. Negative integer as string. " + "Forbidden when `dm_only: true`. See reference/rfcs/supergroup-mode.md."),
11195
11195
  default_topic_id: exports_external.number().int().positive().optional().describe("Forum topic ID this agent's automated outbounds default to when " + "no more-specific alias resolves. Defaults to General (topic 1) when " + "`chat_id` is set and this is omitted — set it only to pin a different " + "fallback topic. " + "Telegram's General topic is `id=1` at MTProto but sends omit the " + "field — the outbound wrapper strips `message_thread_id === 1` " + "on send. Forbidden when `dm_only: true`."),
11196
11196
  topic_aliases: exports_external.record(exports_external.string(), exports_external.number().int().positive()).optional().describe("Operator-friendly names for forum topic IDs (e.g. " + "`{ general: 1, planning: 17, cron: 23, admin: 31, alerts: 41 }`). " + "Referenced from per-cron `topic:` fields and the outbound router " + "for autonomous events (boot → alerts, hostd → admin, etc.). " + "Cascades per-key through defaults → profile → agent.")
11197
11197
  }).optional().superRefine((tg, ctx) => {
@@ -11417,7 +11417,7 @@ var AgentSchema = exports_external.object({
11417
11417
  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."),
11418
11418
  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)."),
11419
11419
  microsoft_workspace: AgentMicrosoftWorkspaceConfigSchema.describe("RFC #1873 (Microsoft 365 integration). Per-agent Microsoft Workspace " + "override — pins the Microsoft account this agent reads via the " + "auth-broker (must be a key in top-level `microsoft_accounts:` with " + "this agent in its `enabled_for[]`) and optionally overrides org_mode. " + "microsoft_client_id/secret are not per-agent."),
11420
- notion_workspace: AgentNotionWorkspaceConfigSchema.describe("RFC docs/rfcs/notion-integration.md. Per-agent Notion access. " + "Presence opts the agent IN (launcher scaffolded, MCP entry emitted, " + "broker grants the integration token). Optional `databases:` filter " + "narrows which DBs this agent may read/write — names must resolve in " + "top-level notion_workspace.databases. Absence opts the agent OUT."),
11420
+ notion_workspace: AgentNotionWorkspaceConfigSchema.describe("RFC reference/rfcs/notion-integration.md. Per-agent Notion access. " + "Presence opts the agent IN (launcher scaffolded, MCP entry emitted, " + "broker grants the integration token). Optional `databases:` filter " + "narrows which DBs this agent may read/write — names must resolve in " + "top-level notion_workspace.databases. Absence opts the agent OUT."),
11421
11421
  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({
11422
11422
  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."),
11423
11423
  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.")
@@ -11552,9 +11552,9 @@ var SwitchroomConfigSchema = exports_external.object({
11552
11552
  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."),
11553
11553
  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)."),
11554
11554
  microsoft_workspace: MicrosoftWorkspaceConfigSchema.describe("RFC #1873 (Microsoft 365 integration). Top-level Microsoft Workspace " + "configuration — OAuth client credentials (Entra app), authority " + "endpoint (defaults to /common for personal MSA + work), and the " + "org_mode opt-in for Teams/SharePoint surfaces. Block is optional; " + "when omitted the broker does not register the Microsoft provider."),
11555
- notion_workspace: NotionWorkspaceConfigSchema.describe("RFC docs/rfcs/notion-integration.md. Top-level Notion integration " + "config — vault key for the integration token, friendly-name → " + "database UUID map, optional MCP-package version pin, and optional " + "global rate-limit override (default 3 rps, Notion's documented " + "public-API limit). Block is optional; when omitted no agent gets a " + "Notion MCP entry regardless of per-agent config."),
11555
+ notion_workspace: NotionWorkspaceConfigSchema.describe("RFC reference/rfcs/notion-integration.md. Top-level Notion integration " + "config — vault key for the integration token, friendly-name → " + "database UUID map, optional MCP-package version pin, and optional " + "global rate-limit override (default 3 rps, Notion's documented " + "public-API limit). Block is optional; when omitted no agent gets a " + "Notion MCP entry regardless of per-agent config."),
11556
11556
  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."),
11557
- host_control: HostControlConfigSchema.default({}).describe("Host-control daemon configuration. Defaults to enabled=true since " + "RFC C Phase 2 (docs/rfcs/host-control-daemon.md). Omit the block " + "to accept defaults; set `enabled: false` only on legacy systemd-" + "mode installs (removal tracked as RFC C Phase 3)."),
11557
+ host_control: HostControlConfigSchema.default({}).describe("Host-control daemon configuration. Defaults to enabled=true since " + "RFC C Phase 2 (reference/rfcs/host-control-daemon.md). Omit the block " + "to accept defaults; set `enabled: false` only on legacy systemd-" + "mode installs (removal tracked as RFC C Phase 3)."),
11558
11558
  hostd: HostdConfigSchema.default({}).describe("hostd verb-level knobs (RFC admin-agent-config-edit). Distinct " + "from `host_control:` which governs whether the daemon runs at " + "all. Scopes the opt-in flag and rate cap for the " + "`config_propose_edit` verb (disabled by default)."),
11559
11559
  web_service: WebServiceConfigSchema.default({}).describe("Web-service container (dashboard + GitHub-webhook receiver) config. " + "Defaults to managed=false so existing systemd-mode installs are " + "untouched. Set managed: true after cutting over to the " + "`switchroom-web` container — then `switchroom update` keeps it " + "refreshed. See `switchroom webd install`."),
11560
11560
  google_accounts: exports_external.record(exports_external.string().regex(/^[^@\s:]+@[^@\s:]+\.[^@\s:]+$/, {
@@ -11576,7 +11576,7 @@ var SwitchroomConfigSchema = exports_external.object({
11576
11576
  agents: exports_external.record(exports_external.string().regex(/^[a-z0-9][a-z0-9_-]{0,50}$/, {
11577
11577
  message: "Agent name must start with a letter/digit, contain only lowercase letters/digits/hyphens/underscores, and be at most 51 characters (Telegram callback_data byte limit)"
11578
11578
  }), AgentSchema).describe("Map of agent name to agent configuration"),
11579
- cron: CronConfigSchema.optional().describe("Cheap-cron settings (docs/rfcs/cheap-cron-sessions.md). Operator-owned " + "egress allowlist + host-pinned secret bindings for Tier-0 http-diff " + "polls (§6.1). Required to enable any http-diff poll; not agent-writable.")
11579
+ cron: CronConfigSchema.optional().describe("Cheap-cron settings (reference/rfcs/cheap-cron-sessions.md). Operator-owned " + "egress allowlist + host-pinned secret bindings for Tier-0 http-diff " + "polls (§6.1). Required to enable any http-diff poll; not agent-writable.")
11580
11580
  });
11581
11581
 
11582
11582
  // src/config/paths.ts
@@ -13386,6 +13386,7 @@ function resolveOutboundTopic(config, event) {
13386
13386
  }
13387
13387
  case "boot":
13388
13388
  case "compact-watchdog":
13389
+ case "linear-auth":
13389
13390
  if (!inSupergroupMode)
13390
13391
  return;
13391
13392
  return aliasToId(cfg, ALERTS_ALIAS) ?? cfg.default_topic_id;
@@ -11001,7 +11001,7 @@ var ActionSpecSchema = exports_external.discriminatedUnion("type", [
11001
11001
  var ScheduleEntrySchema = exports_external.object({
11002
11002
  cron: exports_external.string().describe("Cron expression (e.g., '0 8 * * *')"),
11003
11003
  prompt: exports_external.string().optional().describe("Prompt to send at the scheduled time (the escalation prompt when " + "kind=poll; templated with {{diff}}). Required for kind prompt/poll; " + "absent for kind=action (an action has no model fire, so no prompt)."),
11004
- kind: exports_external.enum(["poll", "prompt", "action"]).optional().describe("Tier-0 routing (docs/rfcs/cheap-cron-sessions.md). 'prompt' (default) " + "fires a model turn every tick (Tier 1/2 per `context`). 'poll' runs a " + "model-free deterministic check (requires `poll`) and only escalates to " + "a model fire on a hit. 'action' runs a model-free deterministic verb " + "(requires `action`) that COMPLETES the work and never escalates — zero " + "tokens, no session. poll/prompt tiering is on by default " + "(SWITCHROOM_CHEAP_CRON=0 is the kill-switch); an action is model-free " + "regardless (the kill-switch governs model tiering, not deterministic " + "actions)."),
11004
+ kind: exports_external.enum(["poll", "prompt", "action"]).optional().describe("Tier-0 routing (reference/rfcs/cheap-cron-sessions.md). 'prompt' (default) " + "fires a model turn every tick (Tier 1/2 per `context`). 'poll' runs a " + "model-free deterministic check (requires `poll`) and only escalates to " + "a model fire on a hit. 'action' runs a model-free deterministic verb " + "(requires `action`) that COMPLETES the work and never escalates — zero " + "tokens, no session. poll/prompt tiering is on by default " + "(SWITCHROOM_CHEAP_CRON=0 is the kill-switch); an action is model-free " + "regardless (the kill-switch governs model tiering, not deterministic " + "actions)."),
11005
11005
  poll: PollSpecSchema.optional().describe("Required iff kind=poll. The declarative poll spec."),
11006
11006
  action: ActionSpecSchema.optional().describe("Required iff kind=action. The declarative action spec (telegram-message or webhook)."),
11007
11007
  model: exports_external.string().optional().describe("Cron model hint. Reactivated by SWITCHROOM_CHEAP_CRON (was DEPRECATED/" + "IGNORED in v0.8). A known-cheap id (sonnet/haiku family) routes the " + "fire to a fresh cheap cron session (Tier 1, `context: fresh`); 'opus', " + "a custom id, or unset routes to the agent's live session (Tier 2, " + "`context: agent`) — the conservative default that preserves pre-v0.8 " + "behaviour. Note: a live session's model is fixed at launch, so on Tier " + "2 this is informational. See docs/scheduling.md."),
@@ -11010,7 +11010,7 @@ var ScheduleEntrySchema = exports_external.object({
11010
11010
  topic: exports_external.union([
11011
11011
  exports_external.string().min(1, "topic alias must be non-empty"),
11012
11012
  exports_external.number().int().positive("topic ID must be a positive integer")
11013
- ]).optional().describe("Forum topic this cron fires into when the owning agent is in " + "supergroup-owned mode (channels.telegram.chat_id set). Either a " + 'string alias resolved against `topic_aliases` (e.g. "planning") ' + "or a numeric topic ID. Falls back to the agent's `default_topic_id` " + "when unset. Ignored for agents in fleet-shared or dm_only mode. " + "Alias-resolution happens at config-load — typos surface immediately. " + "See docs/rfcs/supergroup-mode.md.")
11013
+ ]).optional().describe("Forum topic this cron fires into when the owning agent is in " + "supergroup-owned mode (channels.telegram.chat_id set). Either a " + 'string alias resolved against `topic_aliases` (e.g. "planning") ' + "or a numeric topic ID. Falls back to the agent's `default_topic_id` " + "when unset. Ignored for agents in fleet-shared or dm_only mode. " + "Alias-resolution happens at config-load — typos surface immediately. " + "See reference/rfcs/supergroup-mode.md.")
11014
11014
  }).superRefine((entry, ctx) => {
11015
11015
  const kind = entry.kind ?? "prompt";
11016
11016
  if (kind === "poll" && !entry.poll) {
@@ -11183,15 +11183,15 @@ var TelegramChannelSchema = exports_external.object({
11183
11183
  webhook_rate_limit: exports_external.object({
11184
11184
  rpm: exports_external.number().int().positive()
11185
11185
  }).optional().describe("Per-source rate limit for the webhook ingest path (#714). " + "Off by default — when this key is absent the handler skips " + "rate-limit checks entirely. Opt in by setting `rpm` to an " + "integer requests-per-minute (token bucket per (agent, source); " + "burst equal to rpm). When enabled, exceeding the limit returns " + "429 with Retry-After header; first throttle event per " + "(agent, source) per 60s window is written to " + "<agent>/telegram/issues.jsonl. " + "Cascades from defaults.channels.telegram.webhook_rate_limit."),
11186
- webhook_via_gateway: exports_external.boolean().optional().describe("Route verified webhook events to the agent's in-container gateway " + "over a peercred-gated UDS (<agent>/telegram/webhook.sock) instead " + "of having the host-side web receiver write the agent dir directly. " + "Required under the Docker runtime: the receiver runs as the host " + "operator UID and cannot write the per-agent-UID-owned agent dir " + "(EACCES 500) nor connect the gateway socket. When true the gateway " + "(running as the agent UID) becomes the sole writer of " + "webhook-events.jsonl + dedup/cooldown state and also fires " + "webhook_dispatch. Off by default for back-compat with host-runtime " + "installs. See docs/rfcs/webhook-via-gateway-socket.md."),
11187
- webhook_require_edge: exports_external.boolean().optional().describe("Cloudflare-only edge lock: require the X-Switchroom-Edge header " + "(injected by a Cloudflare Transform Rule on hooks.switchroom.ai) to " + "match the operator's edge secret at ~/.switchroom/webhook-edge-secret " + "before any HMAC verification; reject 403 otherwise. Proves the " + "request entered through our Cloudflare edge — the per-agent HMAC " + "alone can't (it proves body provenance, not network path). Stacks " + "on the GitHub-IP WAF + per-agent HMAC. Fail-closed: when required " + "but the secret file is missing/empty every request is rejected. Off " + "by default. See docs/rfcs/webhook-cloudflare-edge-lock.md."),
11186
+ webhook_via_gateway: exports_external.boolean().optional().describe("Route verified webhook events to the agent's in-container gateway " + "over a peercred-gated UDS (<agent>/telegram/webhook.sock) instead " + "of having the host-side web receiver write the agent dir directly. " + "Required under the Docker runtime: the receiver runs as the host " + "operator UID and cannot write the per-agent-UID-owned agent dir " + "(EACCES 500) nor connect the gateway socket. When true the gateway " + "(running as the agent UID) becomes the sole writer of " + "webhook-events.jsonl + dedup/cooldown state and also fires " + "webhook_dispatch. Off by default for back-compat with host-runtime " + "installs. See reference/rfcs/webhook-via-gateway-socket.md."),
11187
+ webhook_require_edge: exports_external.boolean().optional().describe("Cloudflare-only edge lock: require the X-Switchroom-Edge header " + "(injected by a Cloudflare Transform Rule on hooks.switchroom.ai) to " + "match the operator's edge secret at ~/.switchroom/webhook-edge-secret " + "before any HMAC verification; reject 403 otherwise. Proves the " + "request entered through our Cloudflare edge — the per-agent HMAC " + "alone can't (it proves body provenance, not network path). Stacks " + "on the GitHub-IP WAF + per-agent HMAC. Fail-closed: when required " + "but the secret file is missing/empty every request is rejected. Off " + "by default. See reference/rfcs/webhook-cloudflare-edge-lock.md."),
11188
11188
  linear_agent: exports_external.object({
11189
11189
  enabled: exports_external.boolean(),
11190
11190
  token: exports_external.string().describe("vault:<key> reference to the Linear OAuth app token (actor=app). " + "Resolved at runtime via the vault broker (canonically " + "vault:linear/<agent>/token). Never an inline literal."),
11191
11191
  workspace_id: exports_external.string().optional().describe("Optional Linear workspace (organization) id this agent is " + "installed into. Informational — used for setup hints and " + "multi-workspace disambiguation; the token already scopes the " + "app to its workspace."),
11192
11192
  default_team_id: exports_external.string().optional().describe("Optional Linear team id new captured issues file into when the " + "agent doesn't pass an explicit team_id. Unnecessary for a " + "single-team workspace (auto-resolved); set it only when the " + "workspace has multiple teams. Manage via " + "`switchroom linear-agent set-team <agent> <team>`.")
11193
11193
  }).optional().describe("Linear first-class agent integration (#2298). When enabled, the " + "agent appears in a Linear workspace as an app actor (own name/" + "avatar, @-mentionable, delegate-assignable). Linear AgentSessionEvent " + "webhooks (mention / delegation) wake the agent instantly via the " + "same gateway inject path as webhook_dispatch, tagged " + 'meta.source="linear" with the agent_session_id, and the agent ' + "responds with structured AgentActivity (thought/message/complete/" + "error) via the linear_agent_activity MCP tool. Builds the " + "session-lifecycle layer on top of the plain webhook_sources:[linear] " + "+ webhook_dispatch support (#2272). The OAuth app token is stored in " + "the vault and referenced here as vault:linear/<agent>/token; run " + "`switchroom linear-agent setup <agent>` to provision it. Off by " + "default — opt in per agent. Cascades from " + "defaults.channels.telegram.linear_agent."),
11194
- chat_id: exports_external.string().regex(/^-\d+$/, 'supergroup chat_id must be a negative integer as a string (e.g. "-1001234567890")').optional().describe("Per-agent supergroup ID — overrides fleet `telegram.forum_chat_id`. " + "When set, requires `default_topic_id`. Negative integer as string. " + "Forbidden when `dm_only: true`. See docs/rfcs/supergroup-mode.md."),
11194
+ chat_id: exports_external.string().regex(/^-\d+$/, 'supergroup chat_id must be a negative integer as a string (e.g. "-1001234567890")').optional().describe("Per-agent supergroup ID — overrides fleet `telegram.forum_chat_id`. " + "When set, requires `default_topic_id`. Negative integer as string. " + "Forbidden when `dm_only: true`. See reference/rfcs/supergroup-mode.md."),
11195
11195
  default_topic_id: exports_external.number().int().positive().optional().describe("Forum topic ID this agent's automated outbounds default to when " + "no more-specific alias resolves. Defaults to General (topic 1) when " + "`chat_id` is set and this is omitted — set it only to pin a different " + "fallback topic. " + "Telegram's General topic is `id=1` at MTProto but sends omit the " + "field — the outbound wrapper strips `message_thread_id === 1` " + "on send. Forbidden when `dm_only: true`."),
11196
11196
  topic_aliases: exports_external.record(exports_external.string(), exports_external.number().int().positive()).optional().describe("Operator-friendly names for forum topic IDs (e.g. " + "`{ general: 1, planning: 17, cron: 23, admin: 31, alerts: 41 }`). " + "Referenced from per-cron `topic:` fields and the outbound router " + "for autonomous events (boot → alerts, hostd → admin, etc.). " + "Cascades per-key through defaults → profile → agent.")
11197
11197
  }).optional().superRefine((tg, ctx) => {
@@ -11417,7 +11417,7 @@ var AgentSchema = exports_external.object({
11417
11417
  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."),
11418
11418
  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)."),
11419
11419
  microsoft_workspace: AgentMicrosoftWorkspaceConfigSchema.describe("RFC #1873 (Microsoft 365 integration). Per-agent Microsoft Workspace " + "override — pins the Microsoft account this agent reads via the " + "auth-broker (must be a key in top-level `microsoft_accounts:` with " + "this agent in its `enabled_for[]`) and optionally overrides org_mode. " + "microsoft_client_id/secret are not per-agent."),
11420
- notion_workspace: AgentNotionWorkspaceConfigSchema.describe("RFC docs/rfcs/notion-integration.md. Per-agent Notion access. " + "Presence opts the agent IN (launcher scaffolded, MCP entry emitted, " + "broker grants the integration token). Optional `databases:` filter " + "narrows which DBs this agent may read/write — names must resolve in " + "top-level notion_workspace.databases. Absence opts the agent OUT."),
11420
+ notion_workspace: AgentNotionWorkspaceConfigSchema.describe("RFC reference/rfcs/notion-integration.md. Per-agent Notion access. " + "Presence opts the agent IN (launcher scaffolded, MCP entry emitted, " + "broker grants the integration token). Optional `databases:` filter " + "narrows which DBs this agent may read/write — names must resolve in " + "top-level notion_workspace.databases. Absence opts the agent OUT."),
11421
11421
  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({
11422
11422
  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."),
11423
11423
  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.")
@@ -11552,9 +11552,9 @@ var SwitchroomConfigSchema = exports_external.object({
11552
11552
  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."),
11553
11553
  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)."),
11554
11554
  microsoft_workspace: MicrosoftWorkspaceConfigSchema.describe("RFC #1873 (Microsoft 365 integration). Top-level Microsoft Workspace " + "configuration — OAuth client credentials (Entra app), authority " + "endpoint (defaults to /common for personal MSA + work), and the " + "org_mode opt-in for Teams/SharePoint surfaces. Block is optional; " + "when omitted the broker does not register the Microsoft provider."),
11555
- notion_workspace: NotionWorkspaceConfigSchema.describe("RFC docs/rfcs/notion-integration.md. Top-level Notion integration " + "config — vault key for the integration token, friendly-name → " + "database UUID map, optional MCP-package version pin, and optional " + "global rate-limit override (default 3 rps, Notion's documented " + "public-API limit). Block is optional; when omitted no agent gets a " + "Notion MCP entry regardless of per-agent config."),
11555
+ notion_workspace: NotionWorkspaceConfigSchema.describe("RFC reference/rfcs/notion-integration.md. Top-level Notion integration " + "config — vault key for the integration token, friendly-name → " + "database UUID map, optional MCP-package version pin, and optional " + "global rate-limit override (default 3 rps, Notion's documented " + "public-API limit). Block is optional; when omitted no agent gets a " + "Notion MCP entry regardless of per-agent config."),
11556
11556
  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."),
11557
- host_control: HostControlConfigSchema.default({}).describe("Host-control daemon configuration. Defaults to enabled=true since " + "RFC C Phase 2 (docs/rfcs/host-control-daemon.md). Omit the block " + "to accept defaults; set `enabled: false` only on legacy systemd-" + "mode installs (removal tracked as RFC C Phase 3)."),
11557
+ host_control: HostControlConfigSchema.default({}).describe("Host-control daemon configuration. Defaults to enabled=true since " + "RFC C Phase 2 (reference/rfcs/host-control-daemon.md). Omit the block " + "to accept defaults; set `enabled: false` only on legacy systemd-" + "mode installs (removal tracked as RFC C Phase 3)."),
11558
11558
  hostd: HostdConfigSchema.default({}).describe("hostd verb-level knobs (RFC admin-agent-config-edit). Distinct " + "from `host_control:` which governs whether the daemon runs at " + "all. Scopes the opt-in flag and rate cap for the " + "`config_propose_edit` verb (disabled by default)."),
11559
11559
  web_service: WebServiceConfigSchema.default({}).describe("Web-service container (dashboard + GitHub-webhook receiver) config. " + "Defaults to managed=false so existing systemd-mode installs are " + "untouched. Set managed: true after cutting over to the " + "`switchroom-web` container — then `switchroom update` keeps it " + "refreshed. See `switchroom webd install`."),
11560
11560
  google_accounts: exports_external.record(exports_external.string().regex(/^[^@\s:]+@[^@\s:]+\.[^@\s:]+$/, {
@@ -11576,7 +11576,7 @@ var SwitchroomConfigSchema = exports_external.object({
11576
11576
  agents: exports_external.record(exports_external.string().regex(/^[a-z0-9][a-z0-9_-]{0,50}$/, {
11577
11577
  message: "Agent name must start with a letter/digit, contain only lowercase letters/digits/hyphens/underscores, and be at most 51 characters (Telegram callback_data byte limit)"
11578
11578
  }), AgentSchema).describe("Map of agent name to agent configuration"),
11579
- cron: CronConfigSchema.optional().describe("Cheap-cron settings (docs/rfcs/cheap-cron-sessions.md). Operator-owned " + "egress allowlist + host-pinned secret bindings for Tier-0 http-diff " + "polls (§6.1). Required to enable any http-diff poll; not agent-writable.")
11579
+ cron: CronConfigSchema.optional().describe("Cheap-cron settings (reference/rfcs/cheap-cron-sessions.md). Operator-owned " + "egress allowlist + host-pinned secret bindings for Tier-0 http-diff " + "polls (§6.1). Required to enable any http-diff poll; not agent-writable.")
11580
11580
  });
11581
11581
 
11582
11582
  // src/config/paths.ts
@@ -40,7 +40,8 @@ var PROMPTS = [
40
40
  keys: ["Enter"]
41
41
  }
42
42
  ];
43
- var DEFAULT_IDLE_MS = 30000;
43
+ var REPL_READY_SIGNATURE = /\u2190 for agents|accept edits on|\? for shortcuts|esc to interrupt/;
44
+ var DEFAULT_BOOT_HARD_CAP_MS = 600000;
44
45
  var DEFAULT_POLL_MS = 250;
45
46
  function defaultSleep(ms) {
46
47
  return new Promise((r) => setTimeout(r, ms));
@@ -70,8 +71,9 @@ function sendKeys(agentName, keys) {
70
71
  }
71
72
  }
72
73
  async function runAutoaccept(opts) {
73
- const idleTimeoutMs = opts.idleTimeoutMs ?? DEFAULT_IDLE_MS;
74
+ const bootHardCapMs = opts.bootHardCapMs ?? DEFAULT_BOOT_HARD_CAP_MS;
74
75
  const pollIntervalMs = opts.pollIntervalMs ?? DEFAULT_POLL_MS;
76
+ const replReady = opts.replReadyMatch ?? REPL_READY_SIGNATURE;
75
77
  const rules = (opts.prompts ?? PROMPTS).map((r) => ({
76
78
  rule: r,
77
79
  fired: 0,
@@ -81,10 +83,11 @@ async function runAutoaccept(opts) {
81
83
  const now = opts.now ?? Date.now;
82
84
  const sleep = opts.sleep ?? defaultSleep;
83
85
  const maxPolls = opts.maxPolls ?? Number.POSITIVE_INFINITY;
84
- let lastFire = now();
86
+ const startTime = now();
85
87
  let polls = 0;
86
88
  while (polls < maxPolls) {
87
89
  polls++;
90
+ const t = now();
88
91
  const text = capturePane(opts.agentName);
89
92
  let matchedThisPoll = false;
90
93
  if (text) {
@@ -100,10 +103,13 @@ async function runAutoaccept(opts) {
100
103
  }
101
104
  }
102
105
  }
103
- if (matchedThisPoll) {
104
- lastFire = now();
105
- } else if (now() - lastFire >= idleTimeoutMs) {
106
- return { fired, reason: "idle-timeout" };
106
+ if (!matchedThisPoll) {
107
+ if (text && replReady.test(text)) {
108
+ return { fired, reason: "repl-ready" };
109
+ }
110
+ if (t - startTime >= bootHardCapMs) {
111
+ return { fired, reason: "idle-timeout" };
112
+ }
107
113
  }
108
114
  await sleep(pollIntervalMs);
109
115
  }
@@ -11749,7 +11749,7 @@ var ActionSpecSchema = exports_external.discriminatedUnion("type", [
11749
11749
  var ScheduleEntrySchema = exports_external.object({
11750
11750
  cron: exports_external.string().describe("Cron expression (e.g., '0 8 * * *')"),
11751
11751
  prompt: exports_external.string().optional().describe("Prompt to send at the scheduled time (the escalation prompt when " + "kind=poll; templated with {{diff}}). Required for kind prompt/poll; " + "absent for kind=action (an action has no model fire, so no prompt)."),
11752
- kind: exports_external.enum(["poll", "prompt", "action"]).optional().describe("Tier-0 routing (docs/rfcs/cheap-cron-sessions.md). 'prompt' (default) " + "fires a model turn every tick (Tier 1/2 per `context`). 'poll' runs a " + "model-free deterministic check (requires `poll`) and only escalates to " + "a model fire on a hit. 'action' runs a model-free deterministic verb " + "(requires `action`) that COMPLETES the work and never escalates \u2014 zero " + "tokens, no session. poll/prompt tiering is on by default " + "(SWITCHROOM_CHEAP_CRON=0 is the kill-switch); an action is model-free " + "regardless (the kill-switch governs model tiering, not deterministic " + "actions)."),
11752
+ kind: exports_external.enum(["poll", "prompt", "action"]).optional().describe("Tier-0 routing (reference/rfcs/cheap-cron-sessions.md). 'prompt' (default) " + "fires a model turn every tick (Tier 1/2 per `context`). 'poll' runs a " + "model-free deterministic check (requires `poll`) and only escalates to " + "a model fire on a hit. 'action' runs a model-free deterministic verb " + "(requires `action`) that COMPLETES the work and never escalates \u2014 zero " + "tokens, no session. poll/prompt tiering is on by default " + "(SWITCHROOM_CHEAP_CRON=0 is the kill-switch); an action is model-free " + "regardless (the kill-switch governs model tiering, not deterministic " + "actions)."),
11753
11753
  poll: PollSpecSchema.optional().describe("Required iff kind=poll. The declarative poll spec."),
11754
11754
  action: ActionSpecSchema.optional().describe("Required iff kind=action. The declarative action spec (telegram-message or webhook)."),
11755
11755
  model: exports_external.string().optional().describe("Cron model hint. Reactivated by SWITCHROOM_CHEAP_CRON (was DEPRECATED/" + "IGNORED in v0.8). A known-cheap id (sonnet/haiku family) routes the " + "fire to a fresh cheap cron session (Tier 1, `context: fresh`); 'opus', " + "a custom id, or unset routes to the agent's live session (Tier 2, " + "`context: agent`) \u2014 the conservative default that preserves pre-v0.8 " + "behaviour. Note: a live session's model is fixed at launch, so on Tier " + "2 this is informational. See docs/scheduling.md."),
@@ -11758,7 +11758,7 @@ var ScheduleEntrySchema = exports_external.object({
11758
11758
  topic: exports_external.union([
11759
11759
  exports_external.string().min(1, "topic alias must be non-empty"),
11760
11760
  exports_external.number().int().positive("topic ID must be a positive integer")
11761
- ]).optional().describe("Forum topic this cron fires into when the owning agent is in " + "supergroup-owned mode (channels.telegram.chat_id set). Either a " + 'string alias resolved against `topic_aliases` (e.g. "planning") ' + "or a numeric topic ID. Falls back to the agent's `default_topic_id` " + "when unset. Ignored for agents in fleet-shared or dm_only mode. " + "Alias-resolution happens at config-load \u2014 typos surface immediately. " + "See docs/rfcs/supergroup-mode.md.")
11761
+ ]).optional().describe("Forum topic this cron fires into when the owning agent is in " + "supergroup-owned mode (channels.telegram.chat_id set). Either a " + 'string alias resolved against `topic_aliases` (e.g. "planning") ' + "or a numeric topic ID. Falls back to the agent's `default_topic_id` " + "when unset. Ignored for agents in fleet-shared or dm_only mode. " + "Alias-resolution happens at config-load \u2014 typos surface immediately. " + "See reference/rfcs/supergroup-mode.md.")
11762
11762
  }).superRefine((entry, ctx) => {
11763
11763
  const kind = entry.kind ?? "prompt";
11764
11764
  if (kind === "poll" && !entry.poll) {
@@ -11931,15 +11931,15 @@ var TelegramChannelSchema = exports_external.object({
11931
11931
  webhook_rate_limit: exports_external.object({
11932
11932
  rpm: exports_external.number().int().positive()
11933
11933
  }).optional().describe("Per-source rate limit for the webhook ingest path (#714). " + "Off by default \u2014 when this key is absent the handler skips " + "rate-limit checks entirely. Opt in by setting `rpm` to an " + "integer requests-per-minute (token bucket per (agent, source); " + "burst equal to rpm). When enabled, exceeding the limit returns " + "429 with Retry-After header; first throttle event per " + "(agent, source) per 60s window is written to " + "<agent>/telegram/issues.jsonl. " + "Cascades from defaults.channels.telegram.webhook_rate_limit."),
11934
- webhook_via_gateway: exports_external.boolean().optional().describe("Route verified webhook events to the agent's in-container gateway " + "over a peercred-gated UDS (<agent>/telegram/webhook.sock) instead " + "of having the host-side web receiver write the agent dir directly. " + "Required under the Docker runtime: the receiver runs as the host " + "operator UID and cannot write the per-agent-UID-owned agent dir " + "(EACCES 500) nor connect the gateway socket. When true the gateway " + "(running as the agent UID) becomes the sole writer of " + "webhook-events.jsonl + dedup/cooldown state and also fires " + "webhook_dispatch. Off by default for back-compat with host-runtime " + "installs. See docs/rfcs/webhook-via-gateway-socket.md."),
11935
- webhook_require_edge: exports_external.boolean().optional().describe("Cloudflare-only edge lock: require the X-Switchroom-Edge header " + "(injected by a Cloudflare Transform Rule on hooks.switchroom.ai) to " + "match the operator's edge secret at ~/.switchroom/webhook-edge-secret " + "before any HMAC verification; reject 403 otherwise. Proves the " + "request entered through our Cloudflare edge \u2014 the per-agent HMAC " + "alone can't (it proves body provenance, not network path). Stacks " + "on the GitHub-IP WAF + per-agent HMAC. Fail-closed: when required " + "but the secret file is missing/empty every request is rejected. Off " + "by default. See docs/rfcs/webhook-cloudflare-edge-lock.md."),
11934
+ webhook_via_gateway: exports_external.boolean().optional().describe("Route verified webhook events to the agent's in-container gateway " + "over a peercred-gated UDS (<agent>/telegram/webhook.sock) instead " + "of having the host-side web receiver write the agent dir directly. " + "Required under the Docker runtime: the receiver runs as the host " + "operator UID and cannot write the per-agent-UID-owned agent dir " + "(EACCES 500) nor connect the gateway socket. When true the gateway " + "(running as the agent UID) becomes the sole writer of " + "webhook-events.jsonl + dedup/cooldown state and also fires " + "webhook_dispatch. Off by default for back-compat with host-runtime " + "installs. See reference/rfcs/webhook-via-gateway-socket.md."),
11935
+ webhook_require_edge: exports_external.boolean().optional().describe("Cloudflare-only edge lock: require the X-Switchroom-Edge header " + "(injected by a Cloudflare Transform Rule on hooks.switchroom.ai) to " + "match the operator's edge secret at ~/.switchroom/webhook-edge-secret " + "before any HMAC verification; reject 403 otherwise. Proves the " + "request entered through our Cloudflare edge \u2014 the per-agent HMAC " + "alone can't (it proves body provenance, not network path). Stacks " + "on the GitHub-IP WAF + per-agent HMAC. Fail-closed: when required " + "but the secret file is missing/empty every request is rejected. Off " + "by default. See reference/rfcs/webhook-cloudflare-edge-lock.md."),
11936
11936
  linear_agent: exports_external.object({
11937
11937
  enabled: exports_external.boolean(),
11938
11938
  token: exports_external.string().describe("vault:<key> reference to the Linear OAuth app token (actor=app). " + "Resolved at runtime via the vault broker (canonically " + "vault:linear/<agent>/token). Never an inline literal."),
11939
11939
  workspace_id: exports_external.string().optional().describe("Optional Linear workspace (organization) id this agent is " + "installed into. Informational \u2014 used for setup hints and " + "multi-workspace disambiguation; the token already scopes the " + "app to its workspace."),
11940
11940
  default_team_id: exports_external.string().optional().describe("Optional Linear team id new captured issues file into when the " + "agent doesn't pass an explicit team_id. Unnecessary for a " + "single-team workspace (auto-resolved); set it only when the " + "workspace has multiple teams. Manage via " + "`switchroom linear-agent set-team <agent> <team>`.")
11941
11941
  }).optional().describe("Linear first-class agent integration (#2298). When enabled, the " + "agent appears in a Linear workspace as an app actor (own name/" + "avatar, @-mentionable, delegate-assignable). Linear AgentSessionEvent " + "webhooks (mention / delegation) wake the agent instantly via the " + "same gateway inject path as webhook_dispatch, tagged " + 'meta.source="linear" with the agent_session_id, and the agent ' + "responds with structured AgentActivity (thought/message/complete/" + "error) via the linear_agent_activity MCP tool. Builds the " + "session-lifecycle layer on top of the plain webhook_sources:[linear] " + "+ webhook_dispatch support (#2272). The OAuth app token is stored in " + "the vault and referenced here as vault:linear/<agent>/token; run " + "`switchroom linear-agent setup <agent>` to provision it. Off by " + "default \u2014 opt in per agent. Cascades from " + "defaults.channels.telegram.linear_agent."),
11942
- chat_id: exports_external.string().regex(/^-\d+$/, 'supergroup chat_id must be a negative integer as a string (e.g. "-1001234567890")').optional().describe("Per-agent supergroup ID \u2014 overrides fleet `telegram.forum_chat_id`. " + "When set, requires `default_topic_id`. Negative integer as string. " + "Forbidden when `dm_only: true`. See docs/rfcs/supergroup-mode.md."),
11942
+ chat_id: exports_external.string().regex(/^-\d+$/, 'supergroup chat_id must be a negative integer as a string (e.g. "-1001234567890")').optional().describe("Per-agent supergroup ID \u2014 overrides fleet `telegram.forum_chat_id`. " + "When set, requires `default_topic_id`. Negative integer as string. " + "Forbidden when `dm_only: true`. See reference/rfcs/supergroup-mode.md."),
11943
11943
  default_topic_id: exports_external.number().int().positive().optional().describe("Forum topic ID this agent's automated outbounds default to when " + "no more-specific alias resolves. Defaults to General (topic 1) when " + "`chat_id` is set and this is omitted \u2014 set it only to pin a different " + "fallback topic. " + "Telegram's General topic is `id=1` at MTProto but sends omit the " + "field \u2014 the outbound wrapper strips `message_thread_id === 1` " + "on send. Forbidden when `dm_only: true`."),
11944
11944
  topic_aliases: exports_external.record(exports_external.string(), exports_external.number().int().positive()).optional().describe("Operator-friendly names for forum topic IDs (e.g. " + "`{ general: 1, planning: 17, cron: 23, admin: 31, alerts: 41 }`). " + "Referenced from per-cron `topic:` fields and the outbound router " + "for autonomous events (boot \u2192 alerts, hostd \u2192 admin, etc.). " + "Cascades per-key through defaults \u2192 profile \u2192 agent.")
11945
11945
  }).optional().superRefine((tg, ctx) => {
@@ -12165,7 +12165,7 @@ var AgentSchema = exports_external.object({
12165
12165
  drive: AgentGoogleWorkspaceConfigSchema.describe("RFC D legacy key \u2014 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 \u2014 they live at the top level."),
12166
12166
  google_workspace: AgentGoogleWorkspaceConfigSchema.describe("RFC G canonical key. Per-agent Google Workspace overrides \u2014 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 \u2014 they live at the top level. " + "Mutually exclusive with `drive:` on the same agent (loader fails fast " + "if both are set)."),
12167
12167
  microsoft_workspace: AgentMicrosoftWorkspaceConfigSchema.describe("RFC #1873 (Microsoft 365 integration). Per-agent Microsoft Workspace " + "override \u2014 pins the Microsoft account this agent reads via the " + "auth-broker (must be a key in top-level `microsoft_accounts:` with " + "this agent in its `enabled_for[]`) and optionally overrides org_mode. " + "microsoft_client_id/secret are not per-agent."),
12168
- notion_workspace: AgentNotionWorkspaceConfigSchema.describe("RFC docs/rfcs/notion-integration.md. Per-agent Notion access. " + "Presence opts the agent IN (launcher scaffolded, MCP entry emitted, " + "broker grants the integration token). Optional `databases:` filter " + "narrows which DBs this agent may read/write \u2014 names must resolve in " + "top-level notion_workspace.databases. Absence opts the agent OUT."),
12168
+ notion_workspace: AgentNotionWorkspaceConfigSchema.describe("RFC reference/rfcs/notion-integration.md. Per-agent Notion access. " + "Presence opts the agent IN (launcher scaffolded, MCP entry emitted, " + "broker grants the integration token). Optional `databases:` filter " + "narrows which DBs this agent may read/write \u2014 names must resolve in " + "top-level notion_workspace.databases. Absence opts the agent OUT."),
12169
12169
  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({
12170
12170
  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."),
12171
12171
  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.")
@@ -12300,9 +12300,9 @@ var SwitchroomConfigSchema = exports_external.object({
12300
12300
  drive: GoogleWorkspaceConfigSchema.describe("RFC D legacy key \u2014 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."),
12301
12301
  google_workspace: GoogleWorkspaceConfigSchema.describe("RFC G canonical key. Top-level Google Workspace configuration \u2014 " + "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)."),
12302
12302
  microsoft_workspace: MicrosoftWorkspaceConfigSchema.describe("RFC #1873 (Microsoft 365 integration). Top-level Microsoft Workspace " + "configuration \u2014 OAuth client credentials (Entra app), authority " + "endpoint (defaults to /common for personal MSA + work), and the " + "org_mode opt-in for Teams/SharePoint surfaces. Block is optional; " + "when omitted the broker does not register the Microsoft provider."),
12303
- notion_workspace: NotionWorkspaceConfigSchema.describe("RFC docs/rfcs/notion-integration.md. Top-level Notion integration " + "config \u2014 vault key for the integration token, friendly-name \u2192 " + "database UUID map, optional MCP-package version pin, and optional " + "global rate-limit override (default 3 rps, Notion's documented " + "public-API limit). Block is optional; when omitted no agent gets a " + "Notion MCP entry regardless of per-agent config."),
12303
+ notion_workspace: NotionWorkspaceConfigSchema.describe("RFC reference/rfcs/notion-integration.md. Top-level Notion integration " + "config \u2014 vault key for the integration token, friendly-name \u2192 " + "database UUID map, optional MCP-package version pin, and optional " + "global rate-limit override (default 3 rps, Notion's documented " + "public-API limit). Block is optional; when omitted no agent gets a " + "Notion MCP entry regardless of per-agent config."),
12304
12304
  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."),
12305
- host_control: HostControlConfigSchema.default({}).describe("Host-control daemon configuration. Defaults to enabled=true since " + "RFC C Phase 2 (docs/rfcs/host-control-daemon.md). Omit the block " + "to accept defaults; set `enabled: false` only on legacy systemd-" + "mode installs (removal tracked as RFC C Phase 3)."),
12305
+ host_control: HostControlConfigSchema.default({}).describe("Host-control daemon configuration. Defaults to enabled=true since " + "RFC C Phase 2 (reference/rfcs/host-control-daemon.md). Omit the block " + "to accept defaults; set `enabled: false` only on legacy systemd-" + "mode installs (removal tracked as RFC C Phase 3)."),
12306
12306
  hostd: HostdConfigSchema.default({}).describe("hostd verb-level knobs (RFC admin-agent-config-edit). Distinct " + "from `host_control:` which governs whether the daemon runs at " + "all. Scopes the opt-in flag and rate cap for the " + "`config_propose_edit` verb (disabled by default)."),
12307
12307
  web_service: WebServiceConfigSchema.default({}).describe("Web-service container (dashboard + GitHub-webhook receiver) config. " + "Defaults to managed=false so existing systemd-mode installs are " + "untouched. Set managed: true after cutting over to the " + "`switchroom-web` container \u2014 then `switchroom update` keeps it " + "refreshed. See `switchroom webd install`."),
12308
12308
  google_accounts: exports_external.record(exports_external.string().regex(/^[^@\s:]+@[^@\s:]+\.[^@\s:]+$/, {
@@ -12324,7 +12324,7 @@ var SwitchroomConfigSchema = exports_external.object({
12324
12324
  agents: exports_external.record(exports_external.string().regex(/^[a-z0-9][a-z0-9_-]{0,50}$/, {
12325
12325
  message: "Agent name must start with a letter/digit, contain only lowercase letters/digits/hyphens/underscores, and be at most 51 characters (Telegram callback_data byte limit)"
12326
12326
  }), AgentSchema).describe("Map of agent name to agent configuration"),
12327
- cron: CronConfigSchema.optional().describe("Cheap-cron settings (docs/rfcs/cheap-cron-sessions.md). Operator-owned " + "egress allowlist + host-pinned secret bindings for Tier-0 http-diff " + "polls (\u00a76.1). Required to enable any http-diff poll; not agent-writable.")
12327
+ cron: CronConfigSchema.optional().describe("Cheap-cron settings (reference/rfcs/cheap-cron-sessions.md). Operator-owned " + "egress allowlist + host-pinned secret bindings for Tier-0 http-diff " + "polls (\u00a76.1). Required to enable any http-diff poll; not agent-writable.")
12328
12328
  });
12329
12329
 
12330
12330
  // src/config/paths.ts