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
@@ -474,7 +474,14 @@
474
474
  }
475
475
 
476
476
  @media (min-width: 701px) {
477
- .accounts-grid { display: none; }
477
+ /* The Accounts tab shows a wide-screen TABLE and a mobile card grid,
478
+ hiding the grid on desktop. Scope that hide to #accounts: the
479
+ Connections tab reuses .accounts-grid for its OAuth/Notion cards but
480
+ has NO table fallback, so an unscoped rule blanked every connection
481
+ card on any screen wider than 700px (the cards rendered but were
482
+ display:none, while the empty-state text — not in a grid — still
483
+ showed). */
484
+ #accounts .accounts-grid { display: none; }
478
485
  }
479
486
 
480
487
  @media (max-width: 600px) {
@@ -1029,20 +1036,21 @@
1029
1036
  .then(r => r.ok ? r.json().then(d => ({ ok: true, data: d })) : { ok: false, data: fallback })
1030
1037
  .catch(() => ({ ok: false, data: fallback }));
1031
1038
  try {
1032
- const [g, ms, n, ag] = await Promise.all([
1039
+ const [g, ms, n, li, ag] = await Promise.all([
1033
1040
  safe(fetch(`${API}/api/google-accounts`, { headers: authHeaders() }), []),
1034
1041
  safe(fetch(`${API}/api/microsoft-accounts`, { headers: authHeaders() }), []),
1035
1042
  safe(fetch(`${API}/api/notion-workspace`, { headers: authHeaders() }), { configured: false, databases: [] }),
1043
+ safe(fetch(`${API}/api/linear-agents`, { headers: authHeaders() }), { configured: false, agents: [] }),
1036
1044
  safe(fetch(`${API}/api/agents`, { headers: authHeaders() }), []),
1037
1045
  ]);
1038
- // Notion legitimately reports unconfigured — not a failure. The OAuth
1039
- // providers + the agent list are the ones whose failure must not read
1040
- // as "empty".
1046
+ // Notion + Linear legitimately report unconfigured — not a failure. The
1047
+ // OAuth providers + the agent list are the ones whose failure must not
1048
+ // read as "empty".
1041
1049
  const fetchFailed = !g.ok || !ms.ok || !ag.ok;
1042
1050
  renderConnections({
1043
- google: g.data, microsoft: ms.data, notion: n.data,
1051
+ google: g.data, microsoft: ms.data, notion: n.data, linear: li.data,
1044
1052
  agentNames: (ag.data || []).map(a => a.name).sort(),
1045
- googleFailed: !g.ok, microsoftFailed: !ms.ok, fetchFailed,
1053
+ googleFailed: !g.ok, microsoftFailed: !ms.ok, linearFailed: !li.ok, fetchFailed,
1046
1054
  });
1047
1055
  clearError();
1048
1056
  // Self-heal a transient blip without a manual re-click. Bounded
@@ -1240,12 +1248,33 @@
1240
1248
  }
1241
1249
  }
1242
1250
 
1243
- function switchTab(tab) {
1244
- const tabs = ['summary', 'agents', 'accounts', 'system', 'memory', 'connections', 'schedule', 'approvals'];
1245
- for (const t of tabs) {
1251
+ // Canonical tab list — kept in sync with the nav buttons above and with
1252
+ // SPA_TAB_ROUTES in src/web/server.ts (the server serves the SPA shell for
1253
+ // these paths so a reload/deep-link doesn't 404).
1254
+ const TAB_NAMES = ['summary', 'agents', 'accounts', 'system', 'memory', 'connections', 'schedule', 'approvals'];
1255
+
1256
+ // Derive the active tab from the URL path (`/connections` → 'connections',
1257
+ // '/' → 'summary'). Unknown paths fall back to summary.
1258
+ function tabFromPath() {
1259
+ const name = location.pathname.replace(/^\/+/, '').replace(/\/+$/, '');
1260
+ return TAB_NAMES.includes(name) ? name : 'summary';
1261
+ }
1262
+
1263
+ // opts.push === false suppresses the history push — used when the switch is
1264
+ // itself driven by navigation (initial load, back/forward) so we don't
1265
+ // double-stack history entries.
1266
+ function switchTab(tab, opts) {
1267
+ if (!TAB_NAMES.includes(tab)) tab = 'summary';
1268
+ for (const t of TAB_NAMES) {
1246
1269
  document.getElementById(`tab-${t}`).classList.toggle('active', tab === t);
1247
1270
  document.getElementById(t).style.display = tab === t ? '' : 'none';
1248
1271
  }
1272
+ // Mirror the tab into the URL path so reload/deep-link/back-forward all
1273
+ // land here. Summary canonicalises to '/'.
1274
+ if (!opts || opts.push !== false) {
1275
+ const path = tab === 'summary' ? '/' : '/' + tab;
1276
+ if (location.pathname !== path) history.pushState({ tab }, '', path);
1277
+ }
1249
1278
  if (tab === 'summary') fetchSummary();
1250
1279
  if (tab === 'accounts') fetchAccounts();
1251
1280
  if (tab === 'system') fetchSystemHealth();
@@ -1255,6 +1284,11 @@
1255
1284
  if (tab === 'approvals') fetchApprovals();
1256
1285
  }
1257
1286
 
1287
+ // Back/forward: restore the tab from the history entry (or the path).
1288
+ window.addEventListener('popstate', (e) => {
1289
+ switchTab((e.state && e.state.tab) || tabFromPath(), { push: false });
1290
+ });
1291
+
1258
1292
  // Fleet overview — ONE round-trip to the server-side aggregate
1259
1293
  // (/api/summary), which pulls each part through the same per-tab
1260
1294
  // cache so a Summary open warms the tabs and they can never disagree.
@@ -2117,6 +2151,7 @@
2117
2151
  const google = data.google || [];
2118
2152
  const microsoft = data.microsoft || [];
2119
2153
  const notion = data.notion || { configured: false, databases: [] };
2154
+ const linear = data.linear || { configured: false, agents: [] };
2120
2155
  const agentNames = data.agentNames || [];
2121
2156
 
2122
2157
  // When a provider's fetch FAILED (not genuinely empty), its empty-state
@@ -2180,6 +2215,37 @@
2180
2215
  ${fullAccessBanner}${notionBody}
2181
2216
  </div>`;
2182
2217
 
2218
+ // Linear — first-class OAuth app actors (config-only; the web tier never
2219
+ // reads the per-agent vault, so no token/expiry is shown — and never the
2220
+ // secret). One card per agent with linear_agent.enabled.
2221
+ const linearCards = (linear.agents || []).map(a => {
2222
+ const meta = [];
2223
+ if (a.workspaceId) meta.push(`<div class="meta-item"><label>Workspace </label><span>${escapeHtml(a.workspaceId)}</span></div>`);
2224
+ if (a.defaultTeamId) meta.push(`<div class="meta-item"><label>Default team </label><span>${escapeHtml(a.defaultTeamId)}</span></div>`);
2225
+ if (a.tokenVaultKey) meta.push(`<div class="meta-item"><label>Token </label><span><code>${escapeHtml(a.tokenVaultKey)}</code></span></div>`);
2226
+ return `
2227
+ <div class="account-card">
2228
+ <div class="account-card-header">
2229
+ <div class="account-label">${escapeHtml(a.agent)}</div>
2230
+ <span style="margin-left:auto" class="usage-pill primary">OAuth agent</span>
2231
+ </div>
2232
+ <div class="card-meta" style="padding:0">${meta.join('') || _dimC('—')}</div>
2233
+ </div>`;
2234
+ }).join('');
2235
+ const linearNote = (linear.agents && linear.agents.length)
2236
+ ? `<div style="margin:0 0 .6rem;font-size:.85rem;color:var(--text-dim)">Connected as Linear <b>OAuth app actors</b> — each appears in the workspace as its own actor (not a personal API token for MCP).</div>`
2237
+ : '';
2238
+ const linearBody = linearCards
2239
+ ? `<div class="accounts-grid">${linearCards}</div>`
2240
+ : `<div class="loading" style="padding:.8rem">${data.linearFailed
2241
+ ? "Couldn't load Linear agents — the data source was unreachable (retrying)."
2242
+ : 'No Linear agents connected. Run <code>switchroom linear-agent setup --agent &lt;name&gt;</code> to connect one as a Linear OAuth app actor.'}</div>`;
2243
+ const linearSection = `
2244
+ <div style="margin-bottom:1.5rem">
2245
+ <h3 style="margin:0 0 .6rem;font-size:.95rem;color:var(--text-dim);text-transform:uppercase;letter-spacing:.04em">Linear</h3>
2246
+ ${linearNote}${linearBody}
2247
+ </div>`;
2248
+
2183
2249
  // Tab-level degraded banner: when every OAuth account (Google +
2184
2250
  // Microsoft) is config-only (no live broker slot) and there's at least
2185
2251
  // one, the broker data is unavailable — surface that ABOVE the sections
@@ -2197,7 +2263,7 @@
2197
2263
  ? renderProblem(problemFor('connections-unreachable', {}))
2198
2264
  : '';
2199
2265
 
2200
- container.innerHTML = unreachableBanner + degradedBanner + googleSection + microsoftSection + notionSection;
2266
+ container.innerHTML = unreachableBanner + degradedBanner + googleSection + microsoftSection + notionSection + linearSection;
2201
2267
  }
2202
2268
 
2203
2269
  function renderSchedule(data) {
@@ -2635,12 +2701,16 @@
2635
2701
  }
2636
2702
  }
2637
2703
 
2638
- // Init. Summary is the default visible tab; fetchAgents still runs
2639
- // (populates the agents tab + keeps the 10s fleet poll warm + the
2640
- // log WS depends on it). Summary is fetched on init + on tab-switch
2641
- // only deliberately NOT on the 10s interval (it fans out to
2642
- // system-health etc.; on-demand refresh is enough).
2643
- fetchSummary();
2704
+ // Init. The initial tab comes from the URL path (so a reload/deep-link of
2705
+ // `/connections` opens Connections, not Summary); '/' Summary. We stamp
2706
+ // the history entry so the first back/forward works, then switchTab
2707
+ // (push:false) shows the tab and fetches just its data. fetchAgents still
2708
+ // runs unconditionally (keeps the 10s fleet poll warm + the log WS depends
2709
+ // on it). Per-tab data is fetched on switch only — deliberately NOT on the
2710
+ // 10s interval (it fans out to system-health etc.; on-demand is enough).
2711
+ const initialTab = tabFromPath();
2712
+ history.replaceState({ tab: initialTab }, '', location.pathname);
2713
+ switchTab(initialTab, { push: false });
2644
2714
  fetchAgents();
2645
2715
  connectWebSocket();
2646
2716
  // Fleet poll — gated on tab visibility. A phone with the dashboard
@@ -13736,7 +13736,7 @@ var ActionSpecSchema = exports_external.discriminatedUnion("type", [
13736
13736
  var ScheduleEntrySchema = exports_external.object({
13737
13737
  cron: exports_external.string().describe("Cron expression (e.g., '0 8 * * *')"),
13738
13738
  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)."),
13739
- 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)."),
13739
+ 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)."),
13740
13740
  poll: PollSpecSchema.optional().describe("Required iff kind=poll. The declarative poll spec."),
13741
13741
  action: ActionSpecSchema.optional().describe("Required iff kind=action. The declarative action spec (telegram-message or webhook)."),
13742
13742
  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."),
@@ -13745,7 +13745,7 @@ var ScheduleEntrySchema = exports_external.object({
13745
13745
  topic: exports_external.union([
13746
13746
  exports_external.string().min(1, "topic alias must be non-empty"),
13747
13747
  exports_external.number().int().positive("topic ID must be a positive integer")
13748
- ]).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.")
13748
+ ]).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.")
13749
13749
  }).superRefine((entry, ctx) => {
13750
13750
  const kind = entry.kind ?? "prompt";
13751
13751
  if (kind === "poll" && !entry.poll) {
@@ -13918,15 +13918,15 @@ var TelegramChannelSchema = exports_external.object({
13918
13918
  webhook_rate_limit: exports_external.object({
13919
13919
  rpm: exports_external.number().int().positive()
13920
13920
  }).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."),
13921
- 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."),
13922
- 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."),
13921
+ 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."),
13922
+ 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."),
13923
13923
  linear_agent: exports_external.object({
13924
13924
  enabled: exports_external.boolean(),
13925
13925
  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."),
13926
13926
  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."),
13927
13927
  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>`.")
13928
13928
  }).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."),
13929
- 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."),
13929
+ 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."),
13930
13930
  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`."),
13931
13931
  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.")
13932
13932
  }).optional().superRefine((tg, ctx) => {
@@ -14152,7 +14152,7 @@ var AgentSchema = exports_external.object({
14152
14152
  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."),
14153
14153
  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)."),
14154
14154
  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."),
14155
- 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."),
14155
+ 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."),
14156
14156
  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({
14157
14157
  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."),
14158
14158
  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.")
@@ -14287,9 +14287,9 @@ var SwitchroomConfigSchema = exports_external.object({
14287
14287
  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."),
14288
14288
  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)."),
14289
14289
  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."),
14290
- 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."),
14290
+ 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."),
14291
14291
  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."),
14292
- 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)."),
14292
+ 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)."),
14293
14293
  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)."),
14294
14294
  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`."),
14295
14295
  google_accounts: exports_external.record(exports_external.string().regex(/^[^@\s:]+@[^@\s:]+\.[^@\s:]+$/, {
@@ -14311,7 +14311,7 @@ var SwitchroomConfigSchema = exports_external.object({
14311
14311
  agents: exports_external.record(exports_external.string().regex(/^[a-z0-9][a-z0-9_-]{0,50}$/, {
14312
14312
  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)"
14313
14313
  }), AgentSchema).describe("Map of agent name to agent configuration"),
14314
- 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.")
14314
+ 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.")
14315
14315
  });
14316
14316
 
14317
14317
  // src/config/paths.ts
@@ -21680,7 +21680,7 @@ class HostdServer {
21680
21680
  async handleAgentExec(req, started) {
21681
21681
  const argv0 = req.args.argv[0];
21682
21682
  if (!isAllowlistedReadOnlyArgv(argv0)) {
21683
- return deniedResponse(req.request_id, `agent_exec: "${argv0}" is not on the read-only allowlist. ` + `Allowed: ${READONLY_EXEC_ALLOWLIST.join(", ")}. ` + `Writes inside peer containers require the host_os.exec ` + `approval-kernel scope, which is not yet wired — see ` + `docs/rfcs/approval-kernel.md §6 (deferred follow-up).`, Date.now() - started);
21683
+ return deniedResponse(req.request_id, `agent_exec: "${argv0}" is not on the read-only allowlist. ` + `Allowed: ${READONLY_EXEC_ALLOWLIST.join(", ")}. ` + `Writes inside peer containers require the host_os.exec ` + `approval-kernel scope, which is not yet wired — see ` + `reference/rfcs/approval-kernel.md §6 (deferred follow-up).`, Date.now() - started);
21684
21684
  }
21685
21685
  if (!req.args.argv.every(isSafeExecArgvElement)) {
21686
21686
  return deniedResponse(req.request_id, `agent_exec: an argv element contains a control character ` + `(C0 / DEL) or exceeds ${MAX_EXEC_ARGV_ELEMENT_BYTES} bytes, ` + `which is not permitted (#1401 / #1400).`, Date.now() - started);
@@ -11344,7 +11344,7 @@ var init_schema = __esm(() => {
11344
11344
  ScheduleEntrySchema = exports_external.object({
11345
11345
  cron: exports_external.string().describe("Cron expression (e.g., '0 8 * * *')"),
11346
11346
  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)."),
11347
- 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)."),
11347
+ 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)."),
11348
11348
  poll: PollSpecSchema.optional().describe("Required iff kind=poll. The declarative poll spec."),
11349
11349
  action: ActionSpecSchema.optional().describe("Required iff kind=action. The declarative action spec (telegram-message or webhook)."),
11350
11350
  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."),
@@ -11353,7 +11353,7 @@ var init_schema = __esm(() => {
11353
11353
  topic: exports_external.union([
11354
11354
  exports_external.string().min(1, "topic alias must be non-empty"),
11355
11355
  exports_external.number().int().positive("topic ID must be a positive integer")
11356
- ]).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.")
11356
+ ]).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.")
11357
11357
  }).superRefine((entry, ctx) => {
11358
11358
  const kind = entry.kind ?? "prompt";
11359
11359
  if (kind === "poll" && !entry.poll) {
@@ -11526,15 +11526,15 @@ var init_schema = __esm(() => {
11526
11526
  webhook_rate_limit: exports_external.object({
11527
11527
  rpm: exports_external.number().int().positive()
11528
11528
  }).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."),
11529
- 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."),
11530
- 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."),
11529
+ 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."),
11530
+ 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."),
11531
11531
  linear_agent: exports_external.object({
11532
11532
  enabled: exports_external.boolean(),
11533
11533
  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."),
11534
11534
  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."),
11535
11535
  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>`.")
11536
11536
  }).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."),
11537
- 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."),
11537
+ 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."),
11538
11538
  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`."),
11539
11539
  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.")
11540
11540
  }).optional().superRefine((tg, ctx) => {
@@ -11760,7 +11760,7 @@ var init_schema = __esm(() => {
11760
11760
  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."),
11761
11761
  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)."),
11762
11762
  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."),
11763
- 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."),
11763
+ 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."),
11764
11764
  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({
11765
11765
  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."),
11766
11766
  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.")
@@ -11895,9 +11895,9 @@ var init_schema = __esm(() => {
11895
11895
  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."),
11896
11896
  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)."),
11897
11897
  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."),
11898
- 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."),
11898
+ 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."),
11899
11899
  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."),
11900
- 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)."),
11900
+ 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)."),
11901
11901
  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)."),
11902
11902
  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`."),
11903
11903
  google_accounts: exports_external.record(exports_external.string().regex(/^[^@\s:]+@[^@\s:]+\.[^@\s:]+$/, {
@@ -11919,7 +11919,7 @@ var init_schema = __esm(() => {
11919
11919
  agents: exports_external.record(exports_external.string().regex(/^[a-z0-9][a-z0-9_-]{0,50}$/, {
11920
11920
  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)"
11921
11921
  }), AgentSchema).describe("Map of agent name to agent configuration"),
11922
- 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.")
11922
+ 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.")
11923
11923
  });
11924
11924
  });
11925
11925
 
@@ -11344,7 +11344,7 @@ var init_schema = __esm(() => {
11344
11344
  ScheduleEntrySchema = exports_external.object({
11345
11345
  cron: exports_external.string().describe("Cron expression (e.g., '0 8 * * *')"),
11346
11346
  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)."),
11347
- 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)."),
11347
+ 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)."),
11348
11348
  poll: PollSpecSchema.optional().describe("Required iff kind=poll. The declarative poll spec."),
11349
11349
  action: ActionSpecSchema.optional().describe("Required iff kind=action. The declarative action spec (telegram-message or webhook)."),
11350
11350
  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."),
@@ -11353,7 +11353,7 @@ var init_schema = __esm(() => {
11353
11353
  topic: exports_external.union([
11354
11354
  exports_external.string().min(1, "topic alias must be non-empty"),
11355
11355
  exports_external.number().int().positive("topic ID must be a positive integer")
11356
- ]).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.")
11356
+ ]).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.")
11357
11357
  }).superRefine((entry, ctx) => {
11358
11358
  const kind = entry.kind ?? "prompt";
11359
11359
  if (kind === "poll" && !entry.poll) {
@@ -11526,15 +11526,15 @@ var init_schema = __esm(() => {
11526
11526
  webhook_rate_limit: exports_external.object({
11527
11527
  rpm: exports_external.number().int().positive()
11528
11528
  }).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."),
11529
- 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."),
11530
- 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."),
11529
+ 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."),
11530
+ 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."),
11531
11531
  linear_agent: exports_external.object({
11532
11532
  enabled: exports_external.boolean(),
11533
11533
  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."),
11534
11534
  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."),
11535
11535
  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>`.")
11536
11536
  }).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."),
11537
- 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."),
11537
+ 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."),
11538
11538
  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`."),
11539
11539
  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.")
11540
11540
  }).optional().superRefine((tg, ctx) => {
@@ -11760,7 +11760,7 @@ var init_schema = __esm(() => {
11760
11760
  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."),
11761
11761
  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)."),
11762
11762
  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."),
11763
- 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."),
11763
+ 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."),
11764
11764
  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({
11765
11765
  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."),
11766
11766
  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.")
@@ -11895,9 +11895,9 @@ var init_schema = __esm(() => {
11895
11895
  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."),
11896
11896
  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)."),
11897
11897
  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."),
11898
- 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."),
11898
+ 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."),
11899
11899
  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."),
11900
- 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)."),
11900
+ 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)."),
11901
11901
  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)."),
11902
11902
  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`."),
11903
11903
  google_accounts: exports_external.record(exports_external.string().regex(/^[^@\s:]+@[^@\s:]+\.[^@\s:]+$/, {
@@ -11919,7 +11919,7 @@ var init_schema = __esm(() => {
11919
11919
  agents: exports_external.record(exports_external.string().regex(/^[a-z0-9][a-z0-9_-]{0,50}$/, {
11920
11920
  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)"
11921
11921
  }), AgentSchema).describe("Map of agent name to agent configuration"),
11922
- 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.")
11922
+ 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.")
11923
11923
  });
11924
11924
  });
11925
11925
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "switchroom",
3
- "version": "0.15.36",
3
+ "version": "0.15.38",
4
4
  "description": "Run Claude Code 24/7 on your Claude Pro/Max subscription over Telegram. Open-source alternative to OpenClaw and NanoClaw — no API keys.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env bash
2
- # Tier-1 cheap cron SESSION launcher — docs/rfcs/cheap-cron-sessions.md §2.2.
2
+ # Tier-1 cheap cron SESSION launcher — reference/rfcs/cheap-cron-sessions.md §2.2.
3
3
  #
4
4
  # A SECOND interactive `claude` (no -p — compliance pillar 3) in the agent
5
5
  # container, dedicated to cheap cron fires. It registers to the SAME gateway
@@ -164,7 +164,7 @@ if [ "$SWITCHROOM_RUNTIME" = "docker" ] && [ -z "$SWITCHROOM_DOCKER_TMUX_INNER"
164
164
  fi
165
165
 
166
166
  {{#if cronSessionEnabled}}
167
- # 4) cheap cron SESSION (Tier 1, docs/rfcs/cheap-cron-sessions.md §2.2).
167
+ # 4) cheap cron SESSION (Tier 1, reference/rfcs/cheap-cron-sessions.md §2.2).
168
168
  # A SECOND interactive claude (no -p) dedicated to context:fresh cron
169
169
  # fires, registering to the gateway as the cron-suffixed bridge. This
170
170
  # block is rendered ONLY for an agent that has a Tier-1 cron entry; for
@@ -168,6 +168,31 @@ When you get such a reaction turn:
168
168
 
169
169
  Don't acknowledge with only a reaction or a bare "done" — the operator wants
170
170
  the link (or the honest reason it didn't file).
171
+
172
+ ### If your Linear auth breaks (a 401 / "can't reach Linear")
173
+
174
+ Linear `actor=app` access tokens expire (~24h) and renew from a stored
175
+ **refresh bundle** (`linear/<you>/oauth`). If that bundle is missing or its
176
+ refresh token was revoked, your Linear calls 401 and the operator gets a
177
+ "🔑 Linear auth needs you" alert. You can re-authorize yourself — it's an
178
+ operator-approved, in-container flow (no host shell needed):
179
+
180
+ 1. Ask the operator for the Linear **OAuth app client_id + redirect_uri** (and
181
+ they'll have the client_secret ready for step 3).
182
+ 2. Call **`linear_agent_setup`** with `action: "authorize_url"`, the
183
+ `client_id`, and `redirect_uri`. Relay the returned URL — the operator opens
184
+ it, consents, and copies the `code=` value from the redirect.
185
+ 3. Call **`linear_agent_setup`** with `action: "complete"` + `client_id`,
186
+ `client_secret`, `redirect_uri`, and that `code`. It exchanges the code and
187
+ stores the access token + refresh bundle via the vault broker.
188
+ - If it returns **vault_request_access** instructions, the keys need a
189
+ write-grant — make those calls, the operator approves, then re-run
190
+ `complete` (re-open the authorize URL first if the code went stale).
191
+ - If it returns **config_propose_edit** guidance (durability/ACL), propose
192
+ that edit so the change survives restarts and auto-refresh keeps working.
193
+
194
+ The client_secret and code are used only for the exchange — never store them in
195
+ config or paste them into a normal message; pass them straight to the tool.
171
196
  {{/if}}
172
197
 
173
198
  ### Don't lie about scheduling
@@ -38,7 +38,7 @@ When the user says "add a new agent", "add an agent to my switchroom setup", or
38
38
 
39
39
  ### Anthropic accounts (one OAuth, many agents)
40
40
 
41
- The auth model treats the Anthropic account as the unit of authentication: one OAuth flow per account, then every agent in the fleet inherits the fleet-wide active account. The `switchroom-auth-broker` daemon owns the refresh loop and is the sole writer of every `credentials.json`. See `docs/auth.md` for the operator guide and `reference/share-auth-across-the-fleet.md` for the design.
41
+ The auth model treats the Anthropic account as the unit of authentication: one OAuth flow per account, then every agent in the fleet inherits the fleet-wide active account. The `switchroom-auth-broker` daemon owns the refresh loop and is the sole writer of every `credentials.json`. See `docs/auth.md` for the operator guide and `reference/jobs/share-auth-across-the-fleet.md` for the design.
42
42
 
43
43
  **Bootstrap flow when the user wants to share one Pro/Max subscription across agents:**
44
44
 
@@ -145,7 +145,7 @@ Doubled `!!` (typo / emphasis) reaches you verbatim. Empty `!` gets a "Send your
145
145
 
146
146
  ## "status?" / "still there?" — UX-failure signal
147
147
 
148
- **Trigger:** the user sends a short, low-content message asking whether you're alive — "status?", "still there?", "any update?", "you working?". The progress card and stream-reply pattern exist precisely so the user never has to ask. When you see one of those messages, treat it as a defect signal: something about the in-flight turn made the user feel uncertain. The product expectation (per `reference/know-what-my-agent-is-doing.md`) is that this rate trends to zero.
148
+ **Trigger:** the user sends a short, low-content message asking whether you're alive — "status?", "still there?", "any update?", "you working?". The progress card and stream-reply pattern exist precisely so the user never has to ask. When you see one of those messages, treat it as a defect signal: something about the in-flight turn made the user feel uncertain. The product expectation (per `reference/jobs/know-what-my-agent-is-doing.md`) is that this rate trends to zero.
149
149
 
150
150
  Your response should:
151
151
 
@@ -11,7 +11,7 @@ import {
11
11
  * Answer-lane incremental streaming for long Telegram replies.
12
12
  *
13
13
  * This module implements the "narrative" liveness layer described in
14
- * `reference/know-what-my-agent-is-doing.md`:
14
+ * `reference/jobs/know-what-my-agent-is-doing.md`:
15
15
  *
16
16
  * ambient → 👀 ack reaction
17
17
  * structured → progress card (existing, via stream-reply-handler.ts lane:'progress')