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
@@ -27,6 +27,7 @@ import {
27
27
  type PtyTailHandle,
28
28
  } from '../pty-tail.js'
29
29
  import { createIpcClient, type IpcClientHandle } from './ipc-client.js'
30
+ import { buildEffectiveToolSchemas, LINEAR_ENV } from './tool-filter.js'
30
31
  import type { InboundMessage, PermissionEvent, StatusEvent } from '../gateway/ipc-protocol.js'
31
32
  import { matchesAllowRule } from '../permission-rule.js'
32
33
 
@@ -510,9 +511,51 @@ const TOOL_SCHEMAS = [
510
511
  required: ['title', 'body'],
511
512
  },
512
513
  },
514
+ {
515
+ name: 'linear_agent_setup',
516
+ description:
517
+ 'Provision THIS agent as a Linear app actor (actor=app OAuth) from inside the container — 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 — 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 — never stored in config or logged.',
518
+ inputSchema: {
519
+ type: 'object',
520
+ properties: {
521
+ action: {
522
+ type: 'string',
523
+ enum: ['authorize_url', 'complete'],
524
+ description: '"authorize_url" to get the browser consent URL; "complete" to exchange the code and store the credentials.',
525
+ },
526
+ client_id: {
527
+ type: 'string',
528
+ description: 'Linear OAuth app client id (from Linear → Settings → API → your agent app).',
529
+ },
530
+ redirect_uri: {
531
+ type: 'string',
532
+ description: 'The redirect URI registered on the Linear OAuth app (e.g. http://localhost:3000/callback). Must match exactly in both steps.',
533
+ },
534
+ client_secret: {
535
+ type: 'string',
536
+ description: 'Linear OAuth app client secret. Required for action="complete"; used in-process for the token exchange, never stored or logged.',
537
+ },
538
+ code: {
539
+ type: 'string',
540
+ description: 'The authorization code from the redirect URL (the `code=` query param). Required for action="complete"; single-use.',
541
+ },
542
+ },
543
+ required: ['action', 'client_id', 'redirect_uri'],
544
+ },
545
+ },
513
546
  ]
514
547
 
515
- mcp.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: TOOL_SCHEMAS }))
548
+ // Tool-surface right-sizing (P4): connection-gate linear_* + per-tool
549
+ // alwaysLoad pins for the hot path. See tool-filter.ts for the rationale.
550
+ // Computed once at startup — SWITCHROOM_TELEGRAM_LINEAR is fixed for the
551
+ // process lifetime (set by the gateway in .mcp.json when Linear is wired).
552
+ const EFFECTIVE_TOOL_SCHEMAS = buildEffectiveToolSchemas(TOOL_SCHEMAS, {
553
+ linearEnabled: process.env[LINEAR_ENV] === '1',
554
+ })
555
+
556
+ mcp.setRequestHandler(ListToolsRequestSchema, async () => ({
557
+ tools: EFFECTIVE_TOOL_SCHEMAS,
558
+ }))
516
559
 
517
560
  // ─── MCP CallTool → IPC forward ─────────────────────────────────────────
518
561
 
@@ -659,6 +702,12 @@ function onPermission(msg: PermissionEvent): void {
659
702
  params: {
660
703
  request_id: msg.requestId,
661
704
  behavior: msg.behavior,
705
+ // `message` (deny only) is rendered by claude's channel as
706
+ // "…the user said: ${message}". We use it to tell the model a deny was
707
+ // a TIMEOUT, not a human denial — so it doesn't retry the identical
708
+ // call and re-raise a duplicate card. Omitted → claude's default
709
+ // "Denied" (safe degradation).
710
+ ...(msg.message ? { message: msg.message } : {}),
662
711
  },
663
712
  }).catch((err) => {
664
713
  process.stderr.write(`telegram bridge: failed to deliver permission to Claude: ${err}\n`)
@@ -26,7 +26,10 @@ export function validateGatewayMessage(msg: unknown): msg is GatewayToClient {
26
26
  && (m.behavior === "allow" || m.behavior === "deny")
27
27
  // `rule` is optional (only sent on "🔁 Always allow"); when present
28
28
  // it must be a non-empty string. #1138 wire extension.
29
- && (m.rule === undefined || (typeof m.rule === "string" && m.rule.length > 0));
29
+ && (m.rule === undefined || (typeof m.rule === "string" && m.rule.length > 0))
30
+ // `message` is optional (deny only — the timeout-vs-denial reason);
31
+ // when present it must be a non-empty string.
32
+ && (m.message === undefined || (typeof m.message === "string" && m.message.length > 0));
30
33
  case "status":
31
34
  return typeof m.status === "string";
32
35
  case "tool_call_result":
@@ -0,0 +1,77 @@
1
+ /**
2
+ * Tool-surface right-sizing for the switchroom-telegram MCP server (P4).
3
+ *
4
+ * switchroom-telegram is the primary interface, so its tool schemas are
5
+ * always-loaded on EVERY agent, every turn (~8k tokens). Two reductions,
6
+ * both native to Claude Code tool-search (honored in 2.1.177; the MCP SDK
7
+ * preserves `_meta` on tools/list — @modelcontextprotocol/sdk ToolSchema
8
+ * has `_meta: z.record(...).optional()`):
9
+ *
10
+ * A. Connection gating — linear_* tools are advertised only when the
11
+ * agent has the Linear connection configured (the gateway sets
12
+ * SWITCHROOM_TELEGRAM_LINEAR=1 from channels.telegram.linear_agent.
13
+ * enabled). A non-Linear agent never carries their schema at all.
14
+ *
15
+ * B. Per-tool deferral — the server is no longer pinned wholesale
16
+ * (scaffold sets alwaysLoad:false). We pin ONLY the load-bearing hot
17
+ * tools via `_meta["anthropic/alwaysLoad"]` so they never defer (the
18
+ * reply path must never pay a ToolSearch round-trip before it can
19
+ * answer); the ~14 cold tools defer and load on first use.
20
+ *
21
+ * Pure + side-effect free so it's unit-testable in isolation (bridge.ts
22
+ * has import-time side effects). Both reductions are non-destructive — every
23
+ * tool the agent is entitled to still works; cold ones just load on demand.
24
+ */
25
+
26
+ /** Minimal shape we need from a tool schema. */
27
+ export interface NamedTool {
28
+ name: string
29
+ [k: string]: unknown
30
+ }
31
+
32
+ /**
33
+ * Hot tools pinned loaded — must never defer. The reply path
34
+ * (reply/stream_reply) plus the frequently-used early-turn ops. Everything
35
+ * NOT in this set defers under tool-search.
36
+ */
37
+ export const ALWAYS_LOAD_TOOLS: ReadonlySet<string> = new Set([
38
+ 'reply',
39
+ 'stream_reply',
40
+ 'get_recent_messages',
41
+ 'react',
42
+ 'edit_message',
43
+ 'send_typing',
44
+ 'download_attachment',
45
+ ])
46
+
47
+ /** Linear tools — advertised only when the Linear connection is configured. */
48
+ export const LINEAR_TOOLS: ReadonlySet<string> = new Set([
49
+ 'linear_agent_activity',
50
+ 'linear_create_issue',
51
+ 'linear_agent_setup',
52
+ ])
53
+
54
+ /** The env var the gateway sets (per .mcp.json) when Linear is configured. */
55
+ export const LINEAR_ENV = 'SWITCHROOM_TELEGRAM_LINEAR'
56
+
57
+ export interface ToolFilterOpts {
58
+ /** True when the Linear connection is configured for this agent. */
59
+ linearEnabled: boolean
60
+ }
61
+
62
+ /**
63
+ * Apply connection gating (A) + per-tool deferral pins (B) to a tool list.
64
+ * Returns a NEW array; inputs are not mutated. Order is preserved.
65
+ */
66
+ export function buildEffectiveToolSchemas<T extends NamedTool>(
67
+ schemas: ReadonlyArray<T>,
68
+ opts: ToolFilterOpts,
69
+ ): Array<T & { _meta?: { 'anthropic/alwaysLoad': true } }> {
70
+ return schemas
71
+ .filter((t) => opts.linearEnabled || !LINEAR_TOOLS.has(t.name))
72
+ .map((t) =>
73
+ ALWAYS_LOAD_TOOLS.has(t.name)
74
+ ? { ...t, _meta: { 'anthropic/alwaysLoad': true as const } }
75
+ : t,
76
+ )
77
+ }
@@ -41,7 +41,7 @@
41
41
  * autoRetry transformer; if two topics in the same chat both burst
42
42
  * past the limit, grammY backs off per the Retry-After header — same
43
43
  * worst-case behavior as the old per-chat lock, just hit differently.
44
- * See docs/rfcs/supergroup-mode.md and the
44
+ * See reference/rfcs/supergroup-mode.md and the
45
45
  * `tests/outbound-ordering.test.ts` 429-isolation row.
46
46
  */
47
47
 
@@ -13,7 +13,7 @@
13
13
  * silently failed (stdout to /dev/null), and the user only noticed
14
14
  * hours later when they wondered why their morning brief never came.
15
15
  * Direct violation of the #1 product principle (silent failure is
16
- * the worst case — see reference/know-what-my-agent-is-doing.md).
16
+ * the worst case — see reference/jobs/know-what-my-agent-is-doing.md).
17
17
  *
18
18
  * This module is a pure decision layer. It reads the file, compares
19
19
  * against the last-notified state on disk, and tells the caller
@@ -24211,7 +24211,7 @@ function validateGatewayMessage(msg) {
24211
24211
  case "inbound":
24212
24212
  return typeof m.chatId === "string" && typeof m.text === "string";
24213
24213
  case "permission":
24214
- return typeof m.requestId === "string" && (m.behavior === "allow" || m.behavior === "deny") && (m.rule === undefined || typeof m.rule === "string" && m.rule.length > 0);
24214
+ 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);
24215
24215
  case "status":
24216
24216
  return typeof m.status === "string";
24217
24217
  case "tool_call_result":
@@ -24448,6 +24448,26 @@ function createIpcClient(options) {
24448
24448
  });
24449
24449
  }
24450
24450
 
24451
+ // bridge/tool-filter.ts
24452
+ var ALWAYS_LOAD_TOOLS = new Set([
24453
+ "reply",
24454
+ "stream_reply",
24455
+ "get_recent_messages",
24456
+ "react",
24457
+ "edit_message",
24458
+ "send_typing",
24459
+ "download_attachment"
24460
+ ]);
24461
+ var LINEAR_TOOLS = new Set([
24462
+ "linear_agent_activity",
24463
+ "linear_create_issue",
24464
+ "linear_agent_setup"
24465
+ ]);
24466
+ var LINEAR_ENV = "SWITCHROOM_TELEGRAM_LINEAR";
24467
+ function buildEffectiveToolSchemas(schemas3, opts) {
24468
+ return schemas3.filter((t) => opts.linearEnabled || !LINEAR_TOOLS.has(t.name)).map((t) => ALWAYS_LOAD_TOOLS.has(t.name) ? { ...t, _meta: { "anthropic/alwaysLoad": true } } : t);
24469
+ }
24470
+
24451
24471
  // permission-rule.ts
24452
24472
  import { basename as basename2 } from "node:path";
24453
24473
  var FILE_TOOLS = new Set([
@@ -24987,9 +25007,45 @@ var TOOL_SCHEMAS = [
24987
25007
  },
24988
25008
  required: ["title", "body"]
24989
25009
  }
25010
+ },
25011
+ {
25012
+ name: "linear_agent_setup",
25013
+ 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.',
25014
+ inputSchema: {
25015
+ type: "object",
25016
+ properties: {
25017
+ action: {
25018
+ type: "string",
25019
+ enum: ["authorize_url", "complete"],
25020
+ description: '"authorize_url" to get the browser consent URL; "complete" to exchange the code and store the credentials.'
25021
+ },
25022
+ client_id: {
25023
+ type: "string",
25024
+ description: "Linear OAuth app client id (from Linear \u2192 Settings \u2192 API \u2192 your agent app)."
25025
+ },
25026
+ redirect_uri: {
25027
+ type: "string",
25028
+ description: "The redirect URI registered on the Linear OAuth app (e.g. http://localhost:3000/callback). Must match exactly in both steps."
25029
+ },
25030
+ client_secret: {
25031
+ type: "string",
25032
+ description: 'Linear OAuth app client secret. Required for action="complete"; used in-process for the token exchange, never stored or logged.'
25033
+ },
25034
+ code: {
25035
+ type: "string",
25036
+ description: 'The authorization code from the redirect URL (the `code=` query param). Required for action="complete"; single-use.'
25037
+ }
25038
+ },
25039
+ required: ["action", "client_id", "redirect_uri"]
25040
+ }
24990
25041
  }
24991
25042
  ];
24992
- mcp.setRequestHandler(ListToolsRequestSchema2, async () => ({ tools: TOOL_SCHEMAS }));
25043
+ var EFFECTIVE_TOOL_SCHEMAS = buildEffectiveToolSchemas(TOOL_SCHEMAS, {
25044
+ linearEnabled: process.env[LINEAR_ENV] === "1"
25045
+ });
25046
+ mcp.setRequestHandler(ListToolsRequestSchema2, async () => ({
25047
+ tools: EFFECTIVE_TOOL_SCHEMAS
25048
+ }));
24993
25049
  mcp.setRequestHandler(CallToolRequestSchema2, async (req) => {
24994
25050
  const tool = req.params.name;
24995
25051
  const args = req.params.arguments ?? {};
@@ -25077,7 +25133,8 @@ function onPermission(msg) {
25077
25133
  method: "notifications/claude/channel/permission",
25078
25134
  params: {
25079
25135
  request_id: msg.requestId,
25080
- behavior: msg.behavior
25136
+ behavior: msg.behavior,
25137
+ ...msg.message ? { message: msg.message } : {}
25081
25138
  }
25082
25139
  }).catch((err) => {
25083
25140
  process.stderr.write(`telegram bridge: failed to deliver permission to Claude: ${err}