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
@@ -23809,7 +23809,7 @@ function validateGatewayMessage(msg) {
23809
23809
  case "inbound":
23810
23810
  return typeof m.chatId === "string" && typeof m.text === "string";
23811
23811
  case "permission":
23812
- return typeof m.requestId === "string" && (m.behavior === "allow" || m.behavior === "deny") && (m.rule === undefined || typeof m.rule === "string" && m.rule.length > 0);
23812
+ return typeof m.requestId === "string" && (m.behavior === "allow" || m.behavior === "deny") && (m.rule === undefined || typeof m.rule === "string" && m.rule.length > 0) && (m.message === undefined || typeof m.message === "string" && m.message.length > 0);
23813
23813
  case "status":
23814
23814
  return typeof m.status === "string";
23815
23815
  case "tool_call_result":
@@ -24047,6 +24047,28 @@ function createIpcClient(options) {
24047
24047
  }
24048
24048
  var init_ipc_client = () => {};
24049
24049
 
24050
+ // bridge/tool-filter.ts
24051
+ function buildEffectiveToolSchemas(schemas4, opts) {
24052
+ return schemas4.filter((t) => opts.linearEnabled || !LINEAR_TOOLS.has(t.name)).map((t) => ALWAYS_LOAD_TOOLS.has(t.name) ? { ...t, _meta: { "anthropic/alwaysLoad": true } } : t);
24053
+ }
24054
+ var ALWAYS_LOAD_TOOLS, LINEAR_TOOLS, LINEAR_ENV = "SWITCHROOM_TELEGRAM_LINEAR";
24055
+ var init_tool_filter = __esm(() => {
24056
+ ALWAYS_LOAD_TOOLS = new Set([
24057
+ "reply",
24058
+ "stream_reply",
24059
+ "get_recent_messages",
24060
+ "react",
24061
+ "edit_message",
24062
+ "send_typing",
24063
+ "download_attachment"
24064
+ ]);
24065
+ LINEAR_TOOLS = new Set([
24066
+ "linear_agent_activity",
24067
+ "linear_create_issue",
24068
+ "linear_agent_setup"
24069
+ ]);
24070
+ });
24071
+
24050
24072
  // permission-rule.ts
24051
24073
  import { basename as basename2 } from "node:path";
24052
24074
  function resolveSkillName(input) {
@@ -24171,7 +24193,8 @@ function onPermission(msg) {
24171
24193
  method: "notifications/claude/channel/permission",
24172
24194
  params: {
24173
24195
  request_id: msg.requestId,
24174
- behavior: msg.behavior
24196
+ behavior: msg.behavior,
24197
+ ...msg.message ? { message: msg.message } : {}
24175
24198
  }
24176
24199
  }).catch((err) => {
24177
24200
  process.stderr.write(`telegram bridge: failed to deliver permission to Claude: ${err}
@@ -24232,7 +24255,7 @@ async function main() {
24232
24255
  }
24233
24256
  await mcp.connect(new StdioServerTransport);
24234
24257
  }
24235
- var STATE_DIR, SOCKET_PATH, TOPIC_ID, AGENT_NAME, mcp, TOOL_SCHEMAS, sessionAllowRules, ipc = null, sessionTailEnabled, sessionTailHandle = null, ptyTailEnabled, ptyTailHandle = null;
24258
+ var STATE_DIR, SOCKET_PATH, TOPIC_ID, AGENT_NAME, mcp, TOOL_SCHEMAS, EFFECTIVE_TOOL_SCHEMAS, sessionAllowRules, ipc = null, sessionTailEnabled, sessionTailHandle = null, ptyTailEnabled, ptyTailHandle = null;
24236
24259
  var init_bridge = __esm(async () => {
24237
24260
  init_server2();
24238
24261
  init_stdio2();
@@ -24242,6 +24265,7 @@ var init_bridge = __esm(async () => {
24242
24265
  init_session_tail();
24243
24266
  init_pty_tail();
24244
24267
  init_ipc_client();
24268
+ init_tool_filter();
24245
24269
  init_permission_rule();
24246
24270
  installPluginLogger2();
24247
24271
  STATE_DIR = process.env.TELEGRAM_STATE_DIR ?? join5(homedir4(), ".claude", "channels", "telegram");
@@ -24684,9 +24708,45 @@ var init_bridge = __esm(async () => {
24684
24708
  },
24685
24709
  required: ["title", "body"]
24686
24710
  }
24711
+ },
24712
+ {
24713
+ name: "linear_agent_setup",
24714
+ description: 'Provision THIS agent as a Linear app actor (actor=app OAuth) from inside the container \u2014 the operator-approved in-container path that replaces the host-only `switchroom linear-agent setup` (which silently no-ops in a sandbox). Two steps. action="authorize_url": pass the OAuth app client_id + redirect_uri; returns the browser URL the operator opens to consent. action="complete": pass client_id, client_secret, redirect_uri, and the code from the redirect; exchanges it and stores the access token (linear/<agent>/token) + the durable refresh bundle (linear/<agent>/oauth) via the vault broker so the token auto-renews. Writing those NEW keys needs a write-grant \u2014 if the broker denies, the tool returns the exact vault_request_access calls to make (operator approves), then re-run "complete". After it stores the values, follow the returned guidance to config_propose_edit the linear_agent block + secrets[] ACL (also operator-approved) to make it durable. The client_secret and code are used in-process only \u2014 never stored in config or logged.',
24715
+ inputSchema: {
24716
+ type: "object",
24717
+ properties: {
24718
+ action: {
24719
+ type: "string",
24720
+ enum: ["authorize_url", "complete"],
24721
+ description: '"authorize_url" to get the browser consent URL; "complete" to exchange the code and store the credentials.'
24722
+ },
24723
+ client_id: {
24724
+ type: "string",
24725
+ description: "Linear OAuth app client id (from Linear \u2192 Settings \u2192 API \u2192 your agent app)."
24726
+ },
24727
+ redirect_uri: {
24728
+ type: "string",
24729
+ description: "The redirect URI registered on the Linear OAuth app (e.g. http://localhost:3000/callback). Must match exactly in both steps."
24730
+ },
24731
+ client_secret: {
24732
+ type: "string",
24733
+ description: 'Linear OAuth app client secret. Required for action="complete"; used in-process for the token exchange, never stored or logged.'
24734
+ },
24735
+ code: {
24736
+ type: "string",
24737
+ description: 'The authorization code from the redirect URL (the `code=` query param). Required for action="complete"; single-use.'
24738
+ }
24739
+ },
24740
+ required: ["action", "client_id", "redirect_uri"]
24741
+ }
24687
24742
  }
24688
24743
  ];
24689
- mcp.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: TOOL_SCHEMAS }));
24744
+ EFFECTIVE_TOOL_SCHEMAS = buildEffectiveToolSchemas(TOOL_SCHEMAS, {
24745
+ linearEnabled: process.env[LINEAR_ENV] === "1"
24746
+ });
24747
+ mcp.setRequestHandler(ListToolsRequestSchema, async () => ({
24748
+ tools: EFFECTIVE_TOOL_SCHEMAS
24749
+ }));
24690
24750
  mcp.setRequestHandler(CallToolRequestSchema, async (req) => {
24691
24751
  const tool = req.params.name;
24692
24752
  const args = req.params.arguments ?? {};
@@ -7,7 +7,7 @@
7
7
  *
8
8
  * Today a no-prefix mid-turn message always QUEUES (the default flipped
9
9
  * 2026-04-17 away from the blunt "everything steers" — see
10
- * reference/steer-or-queue-mid-flight.md). This module is the basis for a
10
+ * reference/jobs/steer-or-queue-mid-flight.md). This module is the basis for a
11
11
  * smarter default. It ships first in SHADOW mode (the gateway logs what it WOULD
12
12
  * decide but still queues), to gather real-world data — how often mid-turn
13
13
  * messages are same-topic continuations vs cross-topic new tasks, and the
@@ -46,6 +46,7 @@ import {
46
46
  probeBroker,
47
47
  probeKernel,
48
48
  probeSkills,
49
+ probeConnections,
49
50
  watchAgentProcess,
50
51
  AGENT_LIVE_WINDOW_MS,
51
52
  AGENT_LIVE_POLL_INTERVAL_MS,
@@ -120,6 +121,7 @@ export type ProbeKey =
120
121
  | 'broker'
121
122
  | 'kernel'
122
123
  | 'skills'
124
+ | 'connections'
123
125
 
124
126
  export type ProbeMap = Partial<Record<ProbeKey, ProbeResult | null>>
125
127
 
@@ -253,11 +255,12 @@ const PROBE_LABELS: Record<ProbeKey, string> = {
253
255
  broker: 'Broker',
254
256
  kernel: 'Kernel',
255
257
  skills: 'Skills',
258
+ connections: 'Connections',
256
259
  }
257
260
 
258
261
  const PROBE_KEYS: ReadonlyArray<ProbeKey> = [
259
262
  'account', 'agent', 'gateway', 'quota', 'hindsight',
260
- 'scheduler', 'broker', 'kernel', 'skills',
263
+ 'scheduler', 'broker', 'kernel', 'skills', 'connections',
261
264
  ]
262
265
 
263
266
  const REASON_EMOJI: Record<RestartReason, string> = {
@@ -617,6 +620,7 @@ export async function runAllProbes(opts: RunProbesOpts): Promise<ProbeMap> {
617
620
  probeBroker(undefined, { dockerMode: opts.dockerMode }).then(r => { probes.broker = r }),
618
621
  probeKernel(undefined, { dockerMode: opts.dockerMode }).then(r => { probes.kernel = r }),
619
622
  probeSkills(opts.agentDir, { agentName: opts.agentSlug ?? opts.agentName }).then(r => { probes.skills = r }),
623
+ probeConnections(opts.agentDir).then(r => { probes.connections = r }),
620
624
  ])
621
625
 
622
626
  return probes
@@ -1421,6 +1421,68 @@ function renderBucketedSkills(switchroom: string[], agent: string[]): string {
1421
1421
  return parts.length === 0 ? 'none resolved' : parts.join(' · ')
1422
1422
  }
1423
1423
 
1424
+ // ─── Probe: Connections (configured-but-unauthed MCP integrations) ───────────
1425
+
1426
+ /**
1427
+ * Surface configured-but-unauthed MCP connections at agent start. The auth
1428
+ * verdict can't be computed in-container (this boot probe must not do
1429
+ * vault/grant work — see the module header), so `switchroom apply` computes
1430
+ * it host-side and drops a snapshot at
1431
+ * `<agentDir>/.claude/connection-health.json` (src/agents/connection-health.ts).
1432
+ * This probe just reads it.
1433
+ *
1434
+ * ok — snapshot missing/unparseable (not yet computed → assume
1435
+ * healthy, don't nag) OR zero issues
1436
+ * degraded — ≥1 connection configured but not authed; detail names the
1437
+ * servers, nextStep carries the first fix
1438
+ *
1439
+ * Never `fail`: an unauthed integration degrades that one capability, it
1440
+ * doesn't take the agent down, and the silent-when-healthy boot card
1441
+ * should not red an agent over a missing third-party token.
1442
+ */
1443
+ export interface ConnectionIssueShape {
1444
+ server: string
1445
+ key: string
1446
+ kind: string
1447
+ detail: string
1448
+ fix: string
1449
+ }
1450
+
1451
+ export async function probeConnections(
1452
+ agentDir: string,
1453
+ opts: { readFileImpl?: (path: string) => string } = {},
1454
+ ): Promise<ProbeResult> {
1455
+ return withTimeout('Connections', (async (): Promise<ProbeResult> => {
1456
+ const path = join(agentDir, '.claude', 'connection-health.json')
1457
+ const read = opts.readFileImpl ?? ((p: string) => readFileSync(p, 'utf8'))
1458
+ let issues: ConnectionIssueShape[] = []
1459
+ try {
1460
+ const parsed = JSON.parse(read(path)) as { issues?: ConnectionIssueShape[] }
1461
+ issues = Array.isArray(parsed.issues) ? parsed.issues : []
1462
+ } catch {
1463
+ // ENOENT (never applied with this build) or malformed — assume healthy.
1464
+ return { status: 'ok', label: 'Connections', detail: 'no issues' }
1465
+ }
1466
+ if (issues.length === 0) {
1467
+ return { status: 'ok', label: 'Connections', detail: 'all authed' }
1468
+ }
1469
+ const servers = [...new Set(issues.map((i) => i.server))]
1470
+ const named = servers.slice(0, 4).join(', ')
1471
+ const more = servers.length > 4 ? ` +${servers.length - 4} more` : ''
1472
+ const first = issues[0]
1473
+ const extra =
1474
+ issues.length > 1
1475
+ ? ` (+${issues.length - 1} more — run \`switchroom doctor\`)`
1476
+ : ''
1477
+ return {
1478
+ status: 'degraded',
1479
+ label: 'Connections',
1480
+ detail: `${servers.length} integration(s) configured but not authed: ${named}${more}`,
1481
+ nextStep: `${first.fix}${extra}`,
1482
+ }
1483
+ })())
1484
+ }
1485
+
1424
1486
  export interface SkillsFsImpl {
1425
1487
  readdir: (p: string) => string[]
1426
1488
  exists: (p: string) => boolean
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Cheap-cron session identity — docs/rfcs/cheap-cron-sessions.md §3.3.
2
+ * Cheap-cron session identity — reference/rfcs/cheap-cron-sessions.md §3.3.
3
3
  *
4
4
  * Rather than rekey the gateway's hardened single-bridge machinery
5
5
  * (agentIndex / pendingInboundBuffer / handleRegister, each carrying