@openparachute/agent 0.1.2 → 0.2.2

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 (608) hide show
  1. package/.parachute/module.json +124 -8
  2. package/LICENSE +2 -16
  3. package/README.md +118 -166
  4. package/package.json +35 -42
  5. package/scripts/spawn-agent.ts +371 -0
  6. package/src/_parked/interactive-spawn.test.ts +324 -0
  7. package/src/_parked/interactive-spawn.ts +701 -0
  8. package/src/agent-defs.test.ts +1504 -0
  9. package/src/agent-defs.ts +1702 -0
  10. package/src/agent-mcp-config.test.ts +115 -0
  11. package/src/agent-mcp-config.ts +115 -0
  12. package/src/agents.test.ts +360 -0
  13. package/src/agents.ts +379 -0
  14. package/src/auth.test.ts +46 -0
  15. package/src/auth.ts +140 -0
  16. package/src/backends/attached-queue.test.ts +376 -0
  17. package/src/backends/attached-queue.ts +372 -0
  18. package/src/backends/programmatic.test.ts +1715 -0
  19. package/src/backends/programmatic.ts +927 -0
  20. package/src/backends/registry.test.ts +1494 -0
  21. package/src/backends/registry.ts +1202 -0
  22. package/src/backends/stream-json.test.ts +570 -0
  23. package/src/backends/stream-json.ts +392 -0
  24. package/src/backends/types.ts +223 -0
  25. package/src/bridge.ts +417 -0
  26. package/src/channel-backend-wiring.test.ts +237 -0
  27. package/src/credentials.test.ts +274 -0
  28. package/src/credentials.ts +380 -0
  29. package/src/cron.test.ts +342 -0
  30. package/src/cron.ts +380 -0
  31. package/src/daemon-agent-def-api.test.ts +166 -0
  32. package/src/daemon-agent-defs-api.test.ts +953 -0
  33. package/src/daemon-agent-env-api.test.ts +338 -0
  34. package/src/daemon-attached-queue-store.test.ts +65 -0
  35. package/src/daemon-config-api.test.ts +962 -0
  36. package/src/daemon-jobs-api.test.ts +271 -0
  37. package/src/daemon-vault-chat.test.ts +250 -0
  38. package/src/daemon.test.ts +746 -0
  39. package/src/daemon.ts +3314 -0
  40. package/src/def-vaults.test.ts +136 -0
  41. package/src/def-vaults.ts +165 -0
  42. package/src/delivery-state.test.ts +110 -0
  43. package/src/delivery-state.ts +154 -0
  44. package/src/effective-env.test.ts +114 -0
  45. package/src/effective-env.ts +184 -0
  46. package/src/env-compat.ts +39 -0
  47. package/src/grants.test.ts +638 -0
  48. package/src/grants.ts +675 -0
  49. package/src/hub-jwt.test.ts +161 -0
  50. package/src/hub-jwt.ts +182 -0
  51. package/src/jobs.test.ts +245 -0
  52. package/src/jobs.ts +266 -0
  53. package/src/mcp-http.test.ts +265 -0
  54. package/src/mcp-http.ts +771 -0
  55. package/src/mint-token.test.ts +152 -0
  56. package/src/mint-token.ts +139 -0
  57. package/src/module-manifest.test.ts +158 -0
  58. package/src/oauth-discovery.ts +134 -0
  59. package/src/programmatic-wiring.test.ts +838 -0
  60. package/src/registry.test.ts +227 -0
  61. package/src/registry.ts +228 -0
  62. package/src/resolve-port.test.ts +64 -0
  63. package/src/routing.test.ts +184 -0
  64. package/src/routing.ts +76 -0
  65. package/src/runner.test.ts +506 -0
  66. package/src/runner.ts +255 -0
  67. package/src/sandbox/config.test.ts +150 -0
  68. package/src/sandbox/config.ts +102 -0
  69. package/src/sandbox/egress.test.ts +113 -0
  70. package/src/sandbox/egress.ts +123 -0
  71. package/src/sandbox/index.ts +180 -0
  72. package/src/sandbox/live-seatbelt.test.ts +277 -0
  73. package/src/sandbox/mounts.test.ts +154 -0
  74. package/src/sandbox/mounts.ts +133 -0
  75. package/src/sandbox/sandbox.test.ts +168 -0
  76. package/src/sandbox/types.ts +382 -0
  77. package/src/services-manifest.test.ts +106 -0
  78. package/src/services-manifest.ts +95 -0
  79. package/src/spa-serve.test.ts +116 -0
  80. package/src/spa-serve.ts +116 -0
  81. package/src/spawn-agent-cli.test.ts +172 -0
  82. package/src/spawn-agent.test.ts +1218 -0
  83. package/src/spawn-agent.ts +569 -0
  84. package/src/spawn-deps.test.ts +54 -0
  85. package/src/spawn-deps.ts +166 -0
  86. package/src/telegram/api.ts +153 -0
  87. package/src/terminal-assets.test.ts +50 -0
  88. package/src/terminal-assets.ts +79 -0
  89. package/src/terminal-ui.ts +305 -0
  90. package/src/terminal.test.ts +530 -0
  91. package/src/terminal.ts +458 -0
  92. package/src/transport.ts +270 -0
  93. package/src/transports/http-ui.test.ts +455 -0
  94. package/src/transports/http-ui.ts +201 -0
  95. package/src/transports/telegram.test.ts +174 -0
  96. package/src/transports/telegram.ts +426 -0
  97. package/src/transports/vault.test.ts +2011 -0
  98. package/src/transports/vault.ts +1790 -0
  99. package/src/ui-kit.test.ts +178 -0
  100. package/src/ui-kit.ts +402 -0
  101. package/tsconfig.json +8 -14
  102. package/web/ui/dist/assets/index-C-iWdFFV.css +1 -0
  103. package/web/ui/dist/assets/index-VFETBk0a.js +60 -0
  104. package/web/ui/dist/index.html +15 -0
  105. package/web/ui/tsconfig.json +2 -1
  106. package/.claude/scheduled_tasks.lock +0 -1
  107. package/.claude/settings.json +0 -5
  108. package/.claude/skills/add-atomic-chat-tool/SKILL.md +0 -243
  109. package/.claude/skills/add-atomic-chat-tool/atomic-chat-mcp-stdio.ts +0 -229
  110. package/.claude/skills/add-codex/SKILL.md +0 -161
  111. package/.claude/skills/add-dashboard/SKILL.md +0 -138
  112. package/.claude/skills/add-dashboard/resources/dashboard-pusher.ts +0 -495
  113. package/.claude/skills/add-emacs/SKILL.md +0 -296
  114. package/.claude/skills/add-gcal-tool/SKILL.md +0 -210
  115. package/.claude/skills/add-gchat/REMOVE.md +0 -6
  116. package/.claude/skills/add-gchat/SKILL.md +0 -92
  117. package/.claude/skills/add-gchat/VERIFY.md +0 -3
  118. package/.claude/skills/add-github/REMOVE.md +0 -6
  119. package/.claude/skills/add-github/SKILL.md +0 -148
  120. package/.claude/skills/add-github/VERIFY.md +0 -3
  121. package/.claude/skills/add-gmail-tool/SKILL.md +0 -229
  122. package/.claude/skills/add-imessage/REMOVE.md +0 -6
  123. package/.claude/skills/add-imessage/SKILL.md +0 -113
  124. package/.claude/skills/add-imessage/VERIFY.md +0 -3
  125. package/.claude/skills/add-karpathy-llm-wiki/SKILL.md +0 -110
  126. package/.claude/skills/add-karpathy-llm-wiki/llm-wiki.md +0 -75
  127. package/.claude/skills/add-linear/REMOVE.md +0 -6
  128. package/.claude/skills/add-linear/SKILL.md +0 -168
  129. package/.claude/skills/add-linear/VERIFY.md +0 -3
  130. package/.claude/skills/add-macos-statusbar/SKILL.md +0 -133
  131. package/.claude/skills/add-macos-statusbar/add/src/statusbar.swift +0 -147
  132. package/.claude/skills/add-matrix/REMOVE.md +0 -6
  133. package/.claude/skills/add-matrix/SKILL.md +0 -148
  134. package/.claude/skills/add-matrix/VERIFY.md +0 -3
  135. package/.claude/skills/add-ollama-provider/SKILL.md +0 -179
  136. package/.claude/skills/add-ollama-tool/SKILL.md +0 -193
  137. package/.claude/skills/add-opencode/SKILL.md +0 -229
  138. package/.claude/skills/add-parallel/SKILL.md +0 -290
  139. package/.claude/skills/add-resend/REMOVE.md +0 -6
  140. package/.claude/skills/add-resend/SKILL.md +0 -93
  141. package/.claude/skills/add-resend/VERIFY.md +0 -3
  142. package/.claude/skills/add-signal/REMOVE.md +0 -13
  143. package/.claude/skills/add-signal/SKILL.md +0 -318
  144. package/.claude/skills/add-signal/VERIFY.md +0 -5
  145. package/.claude/skills/add-slack/REMOVE.md +0 -6
  146. package/.claude/skills/add-slack/SKILL.md +0 -112
  147. package/.claude/skills/add-slack/VERIFY.md +0 -3
  148. package/.claude/skills/add-teams/REMOVE.md +0 -6
  149. package/.claude/skills/add-teams/SKILL.md +0 -207
  150. package/.claude/skills/add-teams/VERIFY.md +0 -3
  151. package/.claude/skills/add-vercel/SKILL.md +0 -147
  152. package/.claude/skills/add-vercel/container-skills/vercel-cli/SKILL.md +0 -103
  153. package/.claude/skills/add-webex/REMOVE.md +0 -6
  154. package/.claude/skills/add-webex/SKILL.md +0 -88
  155. package/.claude/skills/add-webex/VERIFY.md +0 -3
  156. package/.claude/skills/add-wechat/REMOVE.md +0 -49
  157. package/.claude/skills/add-wechat/SKILL.md +0 -170
  158. package/.claude/skills/add-wechat/scripts/wire-dm.ts +0 -172
  159. package/.claude/skills/add-whatsapp/SKILL.md +0 -264
  160. package/.claude/skills/add-whatsapp-cloud/REMOVE.md +0 -6
  161. package/.claude/skills/add-whatsapp-cloud/SKILL.md +0 -95
  162. package/.claude/skills/add-whatsapp-cloud/VERIFY.md +0 -3
  163. package/.claude/skills/claw/SKILL.md +0 -131
  164. package/.claude/skills/claw/scripts/claw +0 -374
  165. package/.claude/skills/convert-to-apple-container/SKILL.md +0 -212
  166. package/.claude/skills/customize/SKILL.md +0 -110
  167. package/.claude/skills/debug/SKILL.md +0 -349
  168. package/.claude/skills/get-qodo-rules/SKILL.md +0 -122
  169. package/.claude/skills/get-qodo-rules/references/output-format.md +0 -41
  170. package/.claude/skills/get-qodo-rules/references/pagination.md +0 -33
  171. package/.claude/skills/get-qodo-rules/references/repository-scope.md +0 -26
  172. package/.claude/skills/init-first-agent/SKILL.md +0 -120
  173. package/.claude/skills/init-onecli/SKILL.md +0 -270
  174. package/.claude/skills/manage-channels/SKILL.md +0 -87
  175. package/.claude/skills/manage-mounts/SKILL.md +0 -47
  176. package/.claude/skills/migrate-from-openclaw/MIGRATE_CRONS.md +0 -100
  177. package/.claude/skills/migrate-from-openclaw/SKILL.md +0 -447
  178. package/.claude/skills/migrate-from-openclaw/scripts/discover-openclaw.ts +0 -734
  179. package/.claude/skills/migrate-from-openclaw/scripts/extract-channel-credentials.ts +0 -476
  180. package/.claude/skills/migrate-nanoclaw/SKILL.md +0 -484
  181. package/.claude/skills/migrate-nanoclaw/diagnostics.md +0 -51
  182. package/.claude/skills/qodo-pr-resolver/SKILL.md +0 -326
  183. package/.claude/skills/qodo-pr-resolver/resources/providers.md +0 -329
  184. package/.claude/skills/update-nanoclaw/SKILL.md +0 -243
  185. package/.claude/skills/update-nanoclaw/diagnostics.md +0 -48
  186. package/.claude/skills/update-skills/SKILL.md +0 -130
  187. package/.claude/skills/use-native-credential-proxy/SKILL.md +0 -167
  188. package/.claude/skills/x-integration/SKILL.md +0 -417
  189. package/.claude/skills/x-integration/agent.ts +0 -243
  190. package/.claude/skills/x-integration/host.ts +0 -155
  191. package/.claude/skills/x-integration/lib/browser.ts +0 -148
  192. package/.claude/skills/x-integration/lib/config.ts +0 -62
  193. package/.claude/skills/x-integration/scripts/like.ts +0 -56
  194. package/.claude/skills/x-integration/scripts/post.ts +0 -66
  195. package/.claude/skills/x-integration/scripts/quote.ts +0 -80
  196. package/.claude/skills/x-integration/scripts/reply.ts +0 -74
  197. package/.claude/skills/x-integration/scripts/retweet.ts +0 -62
  198. package/.claude/skills/x-integration/scripts/setup.ts +0 -87
  199. package/.github/CODEOWNERS +0 -10
  200. package/.github/PULL_REQUEST_TEMPLATE.md +0 -18
  201. package/.github/workflows/bump-version.yml +0 -35
  202. package/.github/workflows/ci.yml +0 -39
  203. package/.github/workflows/label-pr.yml +0 -40
  204. package/.github/workflows/update-tokens.yml +0 -43
  205. package/.husky/pre-commit +0 -1
  206. package/.mcp.json +0 -3
  207. package/.nvmrc +0 -1
  208. package/.prettierrc +0 -4
  209. package/CHANGELOG.md +0 -263
  210. package/CLAUDE.md +0 -307
  211. package/CODE_OF_CONDUCT.md +0 -128
  212. package/CONTRIBUTING.md +0 -159
  213. package/CONTRIBUTORS.md +0 -26
  214. package/LICENSE-NANOCLAW-MIT +0 -21
  215. package/README_ja.md +0 -194
  216. package/README_zh.md +0 -194
  217. package/assets/nanoclaw-favicon.png +0 -0
  218. package/assets/nanoclaw-icon.png +0 -0
  219. package/assets/nanoclaw-logo-dark.png +0 -0
  220. package/assets/nanoclaw-logo.png +0 -0
  221. package/assets/nanoclaw-profile.jpeg +0 -0
  222. package/assets/nanoclaw-sales.png +0 -0
  223. package/assets/social-preview.jpg +0 -0
  224. package/config-examples/mount-allowlist.json +0 -25
  225. package/container/.dockerignore +0 -2
  226. package/container/CLAUDE.md +0 -21
  227. package/container/Dockerfile +0 -121
  228. package/container/agent-runner/bun.lock +0 -243
  229. package/container/agent-runner/package.json +0 -22
  230. package/container/agent-runner/scripts/sdk-signal-probe.ts +0 -169
  231. package/container/agent-runner/src/config.ts +0 -55
  232. package/container/agent-runner/src/db/connection.ts +0 -267
  233. package/container/agent-runner/src/db/index.ts +0 -20
  234. package/container/agent-runner/src/db/messages-in.ts +0 -138
  235. package/container/agent-runner/src/db/messages-out.ts +0 -143
  236. package/container/agent-runner/src/db/session-routing.ts +0 -30
  237. package/container/agent-runner/src/db/session-state.test.ts +0 -100
  238. package/container/agent-runner/src/db/session-state.ts +0 -79
  239. package/container/agent-runner/src/destinations.ts +0 -135
  240. package/container/agent-runner/src/formatter.test.ts +0 -167
  241. package/container/agent-runner/src/formatter.ts +0 -260
  242. package/container/agent-runner/src/index.ts +0 -110
  243. package/container/agent-runner/src/integration.test.ts +0 -121
  244. package/container/agent-runner/src/mcp-tools/agents.instructions.md +0 -26
  245. package/container/agent-runner/src/mcp-tools/agents.ts +0 -66
  246. package/container/agent-runner/src/mcp-tools/core.instructions.md +0 -27
  247. package/container/agent-runner/src/mcp-tools/core.ts +0 -262
  248. package/container/agent-runner/src/mcp-tools/index.ts +0 -22
  249. package/container/agent-runner/src/mcp-tools/interactive.instructions.md +0 -22
  250. package/container/agent-runner/src/mcp-tools/interactive.ts +0 -169
  251. package/container/agent-runner/src/mcp-tools/scheduling.instructions.md +0 -40
  252. package/container/agent-runner/src/mcp-tools/scheduling.ts +0 -299
  253. package/container/agent-runner/src/mcp-tools/self-mod.instructions.md +0 -25
  254. package/container/agent-runner/src/mcp-tools/self-mod.ts +0 -120
  255. package/container/agent-runner/src/mcp-tools/server.ts +0 -54
  256. package/container/agent-runner/src/mcp-tools/types.ts +0 -6
  257. package/container/agent-runner/src/poll-loop.test.ts +0 -248
  258. package/container/agent-runner/src/poll-loop.ts +0 -437
  259. package/container/agent-runner/src/providers/claude.ts +0 -379
  260. package/container/agent-runner/src/providers/factory.test.ts +0 -19
  261. package/container/agent-runner/src/providers/factory.ts +0 -13
  262. package/container/agent-runner/src/providers/index.ts +0 -6
  263. package/container/agent-runner/src/providers/mock.ts +0 -77
  264. package/container/agent-runner/src/providers/provider-registry.ts +0 -33
  265. package/container/agent-runner/src/providers/types.ts +0 -82
  266. package/container/agent-runner/src/scheduling/task-script.ts +0 -121
  267. package/container/agent-runner/src/timezone.test.ts +0 -93
  268. package/container/agent-runner/src/timezone.ts +0 -107
  269. package/container/agent-runner/tsconfig.json +0 -14
  270. package/container/build.sh +0 -48
  271. package/container/entrypoint.sh +0 -16
  272. package/container/skills/agent-browser/SKILL.md +0 -159
  273. package/container/skills/frontend-engineer/SKILL.md +0 -157
  274. package/container/skills/self-customize/SKILL.md +0 -87
  275. package/container/skills/slack-formatting/SKILL.md +0 -94
  276. package/container/skills/vercel-cli/SKILL.md +0 -111
  277. package/container/skills/welcome/SKILL.md +0 -85
  278. package/docs/APPLE-CONTAINER-NETWORKING.md +0 -90
  279. package/docs/BRANCH-FORK-MAINTENANCE.md +0 -81
  280. package/docs/README.md +0 -25
  281. package/docs/SDK_DEEP_DIVE.md +0 -643
  282. package/docs/SECURITY.md +0 -162
  283. package/docs/agent-runner-details.md +0 -749
  284. package/docs/api-details.md +0 -365
  285. package/docs/architecture-diagram.html +0 -422
  286. package/docs/architecture-diagram.md +0 -215
  287. package/docs/architecture.md +0 -751
  288. package/docs/audit/2026-04-30-channel-endpoint-audit.md +0 -36
  289. package/docs/build-and-runtime.md +0 -80
  290. package/docs/cross-mount-stress/README.md +0 -112
  291. package/docs/cross-mount-stress/container-writer-retry.mjs +0 -55
  292. package/docs/cross-mount-stress/container-writer-slow.mjs +0 -42
  293. package/docs/cross-mount-stress/container-writer.mjs +0 -47
  294. package/docs/cross-mount-stress/host-writer-retry.mjs +0 -55
  295. package/docs/cross-mount-stress/host-writer-slow.mjs +0 -43
  296. package/docs/cross-mount-stress/host-writer.mjs +0 -47
  297. package/docs/db-central.md +0 -316
  298. package/docs/db-session.md +0 -183
  299. package/docs/db.md +0 -119
  300. package/docs/design/2026-04-29-vault-management-ui.md +0 -231
  301. package/docs/design/2026-04-30-channel-wiring-rework.md +0 -234
  302. package/docs/design/2026-05-01-channel-wiring-approvals-deep-dive.md +0 -272
  303. package/docs/design/2026-05-02-channel-policy-and-approval-routing.md +0 -250
  304. package/docs/docker-sandboxes.md +0 -359
  305. package/docs/isolation-model.md +0 -88
  306. package/docs/ollama.md +0 -79
  307. package/docs/parachute-integration.md +0 -109
  308. package/docs/post-night-rebirth-reflections.md +0 -151
  309. package/eslint.config.js +0 -32
  310. package/pnpm-workspace.yaml +0 -8
  311. package/repo-tokens/README.md +0 -113
  312. package/repo-tokens/action.yml +0 -186
  313. package/repo-tokens/badge.svg +0 -23
  314. package/repo-tokens/examples/green.svg +0 -14
  315. package/repo-tokens/examples/red.svg +0 -14
  316. package/repo-tokens/examples/yellow-green.svg +0 -14
  317. package/repo-tokens/examples/yellow.svg +0 -14
  318. package/scripts/chat.ts +0 -101
  319. package/scripts/cleanup-sessions.sh +0 -150
  320. package/scripts/init-cli-agent.ts +0 -172
  321. package/scripts/init-first-agent.ts +0 -378
  322. package/scripts/parachute.ts +0 -158
  323. package/scripts/run-migrations.ts +0 -105
  324. package/scripts/sanity-live-poll.ts +0 -95
  325. package/scripts/seed-discord.ts +0 -80
  326. package/scripts/test-v2-agent.ts +0 -106
  327. package/scripts/test-v2-channel-e2e.ts +0 -265
  328. package/scripts/test-v2-host.ts +0 -184
  329. package/src/channels/adapter.ts +0 -214
  330. package/src/channels/api-translator.test.ts +0 -306
  331. package/src/channels/api-translator.ts +0 -214
  332. package/src/channels/ask-question.ts +0 -46
  333. package/src/channels/channel-registry.test.ts +0 -421
  334. package/src/channels/channel-registry.ts +0 -313
  335. package/src/channels/chat-sdk-bridge.test.ts +0 -84
  336. package/src/channels/chat-sdk-bridge.ts +0 -652
  337. package/src/channels/cli.ts +0 -276
  338. package/src/channels/discord.ts +0 -90
  339. package/src/channels/index.ts +0 -17
  340. package/src/channels/telegram-markdown-sanitize.test.ts +0 -78
  341. package/src/channels/telegram-markdown-sanitize.ts +0 -55
  342. package/src/channels/telegram-pairing.test.ts +0 -254
  343. package/src/channels/telegram-pairing.ts +0 -339
  344. package/src/channels/telegram.ts +0 -279
  345. package/src/channels/trust-hint.test.ts +0 -48
  346. package/src/channels/trust-hint.ts +0 -75
  347. package/src/claude-md-compose.migrate.test.ts +0 -64
  348. package/src/claude-md-compose.ts +0 -205
  349. package/src/command-gate.ts +0 -63
  350. package/src/config.test.ts +0 -93
  351. package/src/config.ts +0 -128
  352. package/src/container-config.ts +0 -167
  353. package/src/container-runner.test.ts +0 -32
  354. package/src/container-runner.ts +0 -576
  355. package/src/container-runtime.test.ts +0 -269
  356. package/src/container-runtime.ts +0 -167
  357. package/src/db/_bun-sqlite-shim.ts +0 -88
  358. package/src/db/agent-activity.test.ts +0 -155
  359. package/src/db/agent-activity.ts +0 -121
  360. package/src/db/agent-groups.ts +0 -77
  361. package/src/db/connection.migrate.test.ts +0 -176
  362. package/src/db/connection.ts +0 -259
  363. package/src/db/db-v2.test.ts +0 -440
  364. package/src/db/dropped-messages.ts +0 -44
  365. package/src/db/index.ts +0 -40
  366. package/src/db/messaging-groups.ts +0 -252
  367. package/src/db/migrations/001-initial.ts +0 -112
  368. package/src/db/migrations/002-chat-sdk-state.ts +0 -36
  369. package/src/db/migrations/008-dropped-messages.ts +0 -27
  370. package/src/db/migrations/009-drop-pending-credentials.ts +0 -13
  371. package/src/db/migrations/010-engage-modes.ts +0 -103
  372. package/src/db/migrations/011-pending-sender-approvals.ts +0 -40
  373. package/src/db/migrations/012-channel-registration.ts +0 -48
  374. package/src/db/migrations/013-approval-render-metadata.ts +0 -27
  375. package/src/db/migrations/014-secrets.ts +0 -44
  376. package/src/db/migrations/015-secrets-drop-host-pattern.ts +0 -18
  377. package/src/db/migrations/016-secret-assignments.ts +0 -30
  378. package/src/db/migrations/017-agent-activity.ts +0 -40
  379. package/src/db/migrations/018-oauth-app-configs.ts +0 -34
  380. package/src/db/migrations/019-oauth-app-connections.ts +0 -48
  381. package/src/db/migrations/020-agent-app-connections.ts +0 -28
  382. package/src/db/migrations/021-pending-oauth-states.ts +0 -35
  383. package/src/db/migrations/022-app-connections-provider.ts +0 -25
  384. package/src/db/migrations/023-agent-group-secret-mode.test.ts +0 -124
  385. package/src/db/migrations/023-agent-group-secret-mode.ts +0 -65
  386. package/src/db/migrations/024-collapse-approvals.test.ts +0 -249
  387. package/src/db/migrations/024-collapse-approvals.ts +0 -182
  388. package/src/db/migrations/025-secret-mode-check.test.ts +0 -155
  389. package/src/db/migrations/025-secret-mode-check.ts +0 -49
  390. package/src/db/migrations/026-user-dms-bot-id.test.ts +0 -116
  391. package/src/db/migrations/026-user-dms-bot-id.ts +0 -54
  392. package/src/db/migrations/027-provider-credentials.ts +0 -41
  393. package/src/db/migrations/_test-helpers.ts +0 -41
  394. package/src/db/migrations/index.ts +0 -127
  395. package/src/db/migrations/module-agent-to-agent-destinations.ts +0 -84
  396. package/src/db/migrations/module-approvals-pending-approvals.ts +0 -42
  397. package/src/db/migrations/module-approvals-title-options.ts +0 -40
  398. package/src/db/schema.ts +0 -258
  399. package/src/db/session-db.test.ts +0 -93
  400. package/src/db/session-db.ts +0 -325
  401. package/src/db/sessions.ts +0 -241
  402. package/src/delivery.test.ts +0 -148
  403. package/src/delivery.ts +0 -445
  404. package/src/env.ts +0 -74
  405. package/src/group-folder.test.ts +0 -35
  406. package/src/group-folder.ts +0 -44
  407. package/src/group-init.ts +0 -92
  408. package/src/host-core.test.ts +0 -456
  409. package/src/host-sweep.test.ts +0 -146
  410. package/src/host-sweep.ts +0 -287
  411. package/src/index.ts +0 -232
  412. package/src/install-slug.ts +0 -33
  413. package/src/log.test.ts +0 -81
  414. package/src/log.ts +0 -117
  415. package/src/mcp/http.ts +0 -72
  416. package/src/mcp/server.ts +0 -92
  417. package/src/mcp/stdio.ts +0 -51
  418. package/src/mcp/tools/activity.ts +0 -88
  419. package/src/mcp/tools/agent-groups.ts +0 -183
  420. package/src/mcp/tools/approvals.ts +0 -122
  421. package/src/mcp/tools/channels.test.ts +0 -126
  422. package/src/mcp/tools/channels.ts +0 -134
  423. package/src/mcp/tools/index.ts +0 -27
  424. package/src/mcp/tools/oauth.ts +0 -48
  425. package/src/mcp/tools/secrets.ts +0 -169
  426. package/src/mcp/tools/sessions.ts +0 -135
  427. package/src/mcp/types.ts +0 -51
  428. package/src/modules/agent-to-agent/agent-route.test.ts +0 -46
  429. package/src/modules/agent-to-agent/agent-route.ts +0 -223
  430. package/src/modules/agent-to-agent/create-agent.ts +0 -127
  431. package/src/modules/agent-to-agent/db/agent-destinations.ts +0 -135
  432. package/src/modules/agent-to-agent/index.ts +0 -22
  433. package/src/modules/agent-to-agent/write-destinations.ts +0 -59
  434. package/src/modules/approvals/agent.md +0 -45
  435. package/src/modules/approvals/index.ts +0 -21
  436. package/src/modules/approvals/picks.test.ts +0 -291
  437. package/src/modules/approvals/primitive.ts +0 -279
  438. package/src/modules/approvals/project.md +0 -27
  439. package/src/modules/approvals/response-handler.ts +0 -87
  440. package/src/modules/index.ts +0 -24
  441. package/src/modules/interactive/agent.md +0 -21
  442. package/src/modules/interactive/index.ts +0 -69
  443. package/src/modules/interactive/project.md +0 -12
  444. package/src/modules/mount-security/expand-path.test.ts +0 -82
  445. package/src/modules/mount-security/index.ts +0 -459
  446. package/src/modules/mount-security/migrate.test.ts +0 -91
  447. package/src/modules/permissions/access.ts +0 -28
  448. package/src/modules/permissions/channel-approval.test.ts +0 -389
  449. package/src/modules/permissions/channel-approval.ts +0 -188
  450. package/src/modules/permissions/db/agent-group-members.ts +0 -44
  451. package/src/modules/permissions/db/pending-channel-approvals.test.ts +0 -86
  452. package/src/modules/permissions/db/pending-channel-approvals.ts +0 -66
  453. package/src/modules/permissions/db/pending-sender-approvals.ts +0 -60
  454. package/src/modules/permissions/db/user-dms.ts +0 -58
  455. package/src/modules/permissions/db/user-roles.ts +0 -85
  456. package/src/modules/permissions/db/users.ts +0 -38
  457. package/src/modules/permissions/index.ts +0 -421
  458. package/src/modules/permissions/permissions.test.ts +0 -358
  459. package/src/modules/permissions/sender-approval.test.ts +0 -641
  460. package/src/modules/permissions/sender-approval.ts +0 -165
  461. package/src/modules/permissions/user-dm.ts +0 -200
  462. package/src/modules/provider-credentials/db.ts +0 -121
  463. package/src/modules/provider-credentials/index.ts +0 -12
  464. package/src/modules/provider-credentials/spawn.test.ts +0 -206
  465. package/src/modules/provider-credentials/spawn.ts +0 -114
  466. package/src/modules/scheduling/actions.ts +0 -113
  467. package/src/modules/scheduling/db.test.ts +0 -282
  468. package/src/modules/scheduling/db.ts +0 -148
  469. package/src/modules/scheduling/index.ts +0 -34
  470. package/src/modules/scheduling/recurrence.test.ts +0 -98
  471. package/src/modules/scheduling/recurrence.ts +0 -54
  472. package/src/modules/self-mod/agent.md +0 -30
  473. package/src/modules/self-mod/apply.ts +0 -85
  474. package/src/modules/self-mod/index.ts +0 -30
  475. package/src/modules/self-mod/project.md +0 -39
  476. package/src/modules/self-mod/request.ts +0 -91
  477. package/src/modules/typing/index.ts +0 -165
  478. package/src/oauth/agent-app-connections.ts +0 -103
  479. package/src/oauth/app-configs.test.ts +0 -64
  480. package/src/oauth/app-configs.ts +0 -114
  481. package/src/oauth/app-connections.test.ts +0 -109
  482. package/src/oauth/app-connections.ts +0 -178
  483. package/src/oauth/crypto.ts +0 -56
  484. package/src/oauth/flow.ts +0 -104
  485. package/src/oauth/providers/google.test.ts +0 -38
  486. package/src/oauth/providers/google.ts +0 -46
  487. package/src/oauth/providers/index.ts +0 -48
  488. package/src/oauth/state-store.test.ts +0 -54
  489. package/src/oauth/state-store.ts +0 -93
  490. package/src/parachute/README.md +0 -27
  491. package/src/parachute/create-agent.test.ts +0 -83
  492. package/src/parachute/create-agent.ts +0 -122
  493. package/src/parachute/group-status.test.ts +0 -165
  494. package/src/parachute/group-status.ts +0 -136
  495. package/src/parachute/types.ts +0 -41
  496. package/src/parachute/vault-mcp.test.ts +0 -251
  497. package/src/parachute/vault-mcp.ts +0 -232
  498. package/src/platform-id.test.ts +0 -104
  499. package/src/platform-id.ts +0 -109
  500. package/src/providers/index.ts +0 -6
  501. package/src/providers/provider-container-registry.ts +0 -58
  502. package/src/response-registry.ts +0 -45
  503. package/src/router.ts +0 -530
  504. package/src/secrets/crypto.test.ts +0 -45
  505. package/src/secrets/crypto.ts +0 -55
  506. package/src/secrets/index.ts +0 -461
  507. package/src/secrets/master-key.ts +0 -70
  508. package/src/secrets/secrets.test.ts +0 -651
  509. package/src/session-manager.attachments.test.ts +0 -171
  510. package/src/session-manager.dup-skip.test.ts +0 -173
  511. package/src/session-manager.migrate.test.ts +0 -59
  512. package/src/session-manager.ts +0 -451
  513. package/src/startup-bootstrap.test.ts +0 -226
  514. package/src/startup-bootstrap.ts +0 -207
  515. package/src/state-sqlite.ts +0 -182
  516. package/src/timezone.test.ts +0 -64
  517. package/src/timezone.ts +0 -37
  518. package/src/types.ts +0 -233
  519. package/src/web/auth.test.ts +0 -335
  520. package/src/web/auth.ts +0 -214
  521. package/src/web/discord-validate.test.ts +0 -77
  522. package/src/web/discord-validate.ts +0 -88
  523. package/src/web/hub-discovery.test.ts +0 -98
  524. package/src/web/hub-discovery.ts +0 -69
  525. package/src/web/routes/activity.ts +0 -106
  526. package/src/web/routes/agent-provider.test.ts +0 -282
  527. package/src/web/routes/agent-provider.ts +0 -309
  528. package/src/web/routes/approvals.ts +0 -185
  529. package/src/web/routes/apps.ts +0 -434
  530. package/src/web/routes/channels-mg-detail.test.ts +0 -324
  531. package/src/web/routes/channels-mga-detail.test.ts +0 -472
  532. package/src/web/routes/channels.ts +0 -311
  533. package/src/web/routes/oauth-providers.ts +0 -42
  534. package/src/web/routes/secrets.test.ts +0 -220
  535. package/src/web/routes/secrets.ts +0 -317
  536. package/src/web/routes/sessions.ts +0 -123
  537. package/src/web/routes/settings.test.ts +0 -106
  538. package/src/web/routes/settings.ts +0 -247
  539. package/src/web/routes/setup-status.ts +0 -205
  540. package/src/web/routes/vaults.test.ts +0 -389
  541. package/src/web/routes/vaults.ts +0 -225
  542. package/src/web/server-version.test.ts +0 -16
  543. package/src/web/server.ts +0 -1024
  544. package/src/web/services-manifest.test.ts +0 -148
  545. package/src/web/services-manifest.ts +0 -66
  546. package/src/web/static-serve.test.ts +0 -255
  547. package/src/web/static-serve.ts +0 -104
  548. package/src/web/telegram-validate.test.ts +0 -116
  549. package/src/web/telegram-validate.ts +0 -107
  550. package/src/web/vault-proxy.test.ts +0 -214
  551. package/src/web/vault-proxy.ts +0 -120
  552. package/src/web/wire-channel.ts +0 -181
  553. package/src/webhook-server.ts +0 -134
  554. package/vitest.config.ts +0 -18
  555. package/web/README.md +0 -63
  556. package/web/ui/index.html +0 -13
  557. package/web/ui/package.json +0 -35
  558. package/web/ui/pnpm-lock.yaml +0 -2164
  559. package/web/ui/scripts/verify-base.mjs +0 -31
  560. package/web/ui/src/App.tsx +0 -88
  561. package/web/ui/src/components/ActivityFeed.tsx +0 -444
  562. package/web/ui/src/components/AgentGroupPicker.tsx +0 -263
  563. package/web/ui/src/components/AgentProviderCards.tsx +0 -220
  564. package/web/ui/src/components/CredentialForm.tsx +0 -214
  565. package/web/ui/src/components/ScopeGrants.tsx +0 -74
  566. package/web/ui/src/components/StatusDot.tsx +0 -43
  567. package/web/ui/src/components/VaultPicker.tsx +0 -127
  568. package/web/ui/src/components/setup/AdapterInstallStep.tsx +0 -178
  569. package/web/ui/src/components/setup/AgentGroupStep.tsx +0 -43
  570. package/web/ui/src/components/setup/ChannelPickStep.tsx +0 -74
  571. package/web/ui/src/components/setup/DoneStep.tsx +0 -49
  572. package/web/ui/src/components/setup/PrereqStep.tsx +0 -129
  573. package/web/ui/src/components/setup/TestConnectionStep.tsx +0 -108
  574. package/web/ui/src/components/setup/TestMessageStep.tsx +0 -104
  575. package/web/ui/src/components/setup/WireChannelStep.tsx +0 -166
  576. package/web/ui/src/components/setup/types.ts +0 -105
  577. package/web/ui/src/lib/api.test.ts +0 -410
  578. package/web/ui/src/lib/api.ts +0 -1248
  579. package/web/ui/src/lib/auth.test.ts +0 -352
  580. package/web/ui/src/lib/auth.ts +0 -405
  581. package/web/ui/src/lib/channel-adapters.ts +0 -136
  582. package/web/ui/src/main.tsx +0 -19
  583. package/web/ui/src/routes/ApprovalsList.tsx +0 -294
  584. package/web/ui/src/routes/Apps.tsx +0 -613
  585. package/web/ui/src/routes/ChannelWireDetail.test.tsx +0 -233
  586. package/web/ui/src/routes/ChannelWireDetail.tsx +0 -403
  587. package/web/ui/src/routes/ChannelsList.tsx +0 -158
  588. package/web/ui/src/routes/GroupDetail.test.tsx +0 -206
  589. package/web/ui/src/routes/GroupDetail.tsx +0 -880
  590. package/web/ui/src/routes/GroupList.tsx +0 -187
  591. package/web/ui/src/routes/MessagingGroupDetail.test.tsx +0 -233
  592. package/web/ui/src/routes/MessagingGroupDetail.tsx +0 -306
  593. package/web/ui/src/routes/NewGroupWizard.tsx +0 -390
  594. package/web/ui/src/routes/OAuthCallback.tsx +0 -56
  595. package/web/ui/src/routes/SecretsList.tsx +0 -942
  596. package/web/ui/src/routes/SessionsList.tsx +0 -220
  597. package/web/ui/src/routes/SettingsAgentProvider.tsx +0 -109
  598. package/web/ui/src/routes/SettingsApprovals.tsx +0 -234
  599. package/web/ui/src/routes/SetupWizard.tsx +0 -219
  600. package/web/ui/src/routes/VaultDetail.test.tsx +0 -363
  601. package/web/ui/src/routes/VaultDetail.tsx +0 -960
  602. package/web/ui/src/routes/VaultsList.tsx +0 -295
  603. package/web/ui/src/routes/WireChannelPage.tsx +0 -413
  604. package/web/ui/src/styles.css +0 -608
  605. package/web/ui/src/test/setup.ts +0 -23
  606. package/web/ui/src/vite-env.d.ts +0 -10
  607. package/web/ui/vite.config.ts +0 -34
  608. package/web/ui/vitest.config.ts +0 -25
@@ -0,0 +1,771 @@
1
+ /**
2
+ * Stateful HTTP MCP endpoint for parachute-agent.
3
+ *
4
+ * A second, additive way for a Claude Code session to connect to a channel:
5
+ * instead of spawning the stdio `bridge.ts` and consuming the daemon's SSE
6
+ * `/events`, the session adds the channel as a *pure HTTP MCP server* (URL +
7
+ * OAuth) — exactly like the vault. No local file, works on any machine.
8
+ *
9
+ * Why STATEFUL (not stateless like vault's mcp-http.ts): the live WAKE
10
+ * (`pushToChannel`) PUSHes a `notifications/claude/agent` onto a connected
11
+ * session's standalone GET stream — the programmatic backend's "watch it work"
12
+ * interim-text streaming + the live inbound wake for a subscribed session.
13
+ * Stateful Streamable HTTP (a `sessionIdGenerator` + `enableJsonResponse:false`)
14
+ * gives each session an SSE GET stream the server can push onto. This file is the
15
+ * productionized form: per-channel session registry, the push surface, the
16
+ * ATTACHED-backend pull surface (`pending`/`next-message`/`reply`/`release` —
17
+ * design 2026-06-18), and read-vs-write scope enforcement.
18
+ *
19
+ * NOTE — the deaf-on-restart BACKLOG REPLAY (the connect-hook that replayed
20
+ * messages a session missed while idle) was RETIRED with the interactive backend
21
+ * (design 2026-06-19-retire-interactive-backend.md): the programmatic path runs
22
+ * synchronously and the channel path uses the durable note-claim queue, so there's
23
+ * no missed-while-idle backlog to replay onto a reconnecting session.
24
+ *
25
+ * Lifecycle:
26
+ * - POST /mcp/<channel> with no mcp-session-id → a NEW session: build a
27
+ * Server + stateful transport, connect, register under <channel> on
28
+ * onsessioninitialized.
29
+ * - POST/GET /mcp/<channel> with a known mcp-session-id → route to that
30
+ * session's transport (GET opens the SSE push stream).
31
+ * - DELETE /mcp/<channel> (or transport.onclose) → tear the session down and
32
+ * drop it from the channel's set.
33
+ *
34
+ * Auth: the daemon validates `agent:read` BEFORE calling handleMcp (a session
35
+ * needs read to connect + receive the wake). The validated scopes are threaded
36
+ * in and stored ON the session, so the reply/react/edit/download tool handlers
37
+ * can additionally require `agent:write` — a read-only token connects and is
38
+ * woken but cannot send.
39
+ */
40
+
41
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
42
+ import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js";
43
+ import {
44
+ ListToolsRequestSchema,
45
+ CallToolRequestSchema,
46
+ } from "@modelcontextprotocol/sdk/types.js";
47
+ import type {
48
+ Transport,
49
+ ReplyArgs,
50
+ ReactArgs,
51
+ EditArgs,
52
+ DownloadArgs,
53
+ } from "./transport.ts";
54
+ import { SCOPE_WRITE, grantsScope } from "./auth.ts";
55
+ import type { AttachedQueueRegistry } from "./backends/attached-queue.ts";
56
+
57
+ // ---------------------------------------------------------------------------
58
+ // Per-channel session registry
59
+ // ---------------------------------------------------------------------------
60
+
61
+ /** A live HTTP MCP session = a Server + its stateful transport + caller scopes. */
62
+ interface McpSession {
63
+ server: Server;
64
+ transport: WebStandardStreamableHTTPServerTransport;
65
+ /** The scopes the connecting token carried — write-tools check these. */
66
+ scopes: string[];
67
+ }
68
+
69
+ /**
70
+ * channel name → its set of live MCP sessions. A push on channel A reaches only
71
+ * the sessions registered under A. Distinct from the SSE ClientRegistry (which
72
+ * serves stdio bridges over `/events`); the two run side by side.
73
+ */
74
+ const sessionsByChannel = new Map<string, Set<McpSession>>();
75
+
76
+ /** mcp-session-id → session, so a follow-up POST/GET/DELETE finds its transport. */
77
+ const sessionsById = new Map<string, McpSession>();
78
+
79
+ function registerSession(channel: string, id: string, session: McpSession): void {
80
+ let set = sessionsByChannel.get(channel);
81
+ if (!set) {
82
+ set = new Set();
83
+ sessionsByChannel.set(channel, set);
84
+ }
85
+ set.add(session);
86
+ sessionsById.set(id, session);
87
+ }
88
+
89
+ /**
90
+ * Whether a session has a LIVE standalone GET SSE push stream — the stream the
91
+ * SDK writes `notifications/claude/agent` onto. A session that has only POSTed
92
+ * `initialize` (registered) but not yet opened — or has since dropped — its GET
93
+ * stream is NOT deliverable: `transport.send()` silently no-ops for it (no event
94
+ * store is configured, so the message is dropped, not buffered). We read the SAME
95
+ * internal map the SDK's send() consults (`_streamMapping['_GET_stream']`), so our
96
+ * notion of "deliverable" can never disagree with whether the SDK actually writes.
97
+ */
98
+ function sessionHasLivePushStream(session: McpSession): boolean {
99
+ const t = session.transport as unknown as
100
+ | { _streamMapping?: Map<string, unknown> }
101
+ | undefined;
102
+ return !!t && t._streamMapping?.get("_GET_stream") !== undefined;
103
+ }
104
+
105
+ function unregisterSession(channel: string, id: string): void {
106
+ const session = sessionsById.get(id);
107
+ sessionsById.delete(id);
108
+ const set = sessionsByChannel.get(channel);
109
+ if (set && session) {
110
+ set.delete(session);
111
+ if (set.size === 0) sessionsByChannel.delete(channel);
112
+ }
113
+ }
114
+
115
+ /** Count of live MCP sessions on a channel (for /health). */
116
+ export function mcpSessionCount(channel: string): number {
117
+ return sessionsByChannel.get(channel)?.size ?? 0;
118
+ }
119
+
120
+ /**
121
+ * Boot-time guard for the ONE SDK internal `sessionHasLivePushStream` depends on:
122
+ * the standalone GET stream is keyed by `_standaloneSseStreamId === "_GET_stream"`
123
+ * inside the transport's `_streamMapping`. We read that private field (the SDK
124
+ * exposes no public "is the push stream open?" API), so an SDK upgrade that renamed
125
+ * it would make `sessionHasLivePushStream` return false forever — silently breaking
126
+ * the HTTP-MCP live wake (`pushToChannel`), which gates on it. The caret pin (`^1.x`)
127
+ * lets a `bun update` pull such a version, so we verify
128
+ * the contract LOUDLY at boot rather than discover it as silent message loss in
129
+ * production. Verified against @modelcontextprotocol/sdk 1.29.x. Returns true when
130
+ * the contract holds; logs a screaming error and returns false otherwise.
131
+ */
132
+ export function assertMcpSdkStreamContract(): boolean {
133
+ try {
134
+ const probe = new WebStandardStreamableHTTPServerTransport({
135
+ sessionIdGenerator: () => "contract-probe",
136
+ enableJsonResponse: false,
137
+ });
138
+ const id = (probe as unknown as { _standaloneSseStreamId?: unknown })._standaloneSseStreamId;
139
+ const hasMap =
140
+ (probe as unknown as { _streamMapping?: unknown })._streamMapping instanceof Map;
141
+ if (id === "_GET_stream" && hasMap) return true;
142
+ console.error(
143
+ "parachute-agent: FATAL CONTRACT DRIFT — the MCP SDK's standalone-GET-stream " +
144
+ `internals changed (expected _standaloneSseStreamId="_GET_stream" + a _streamMapping Map; ` +
145
+ `got id=${JSON.stringify(id)}, map=${hasMap}). sessionHasLivePushStream() can no longer ` +
146
+ "detect a live push stream, so the HTTP-MCP live wake (pushToChannel) is " +
147
+ "BROKEN. Pin @modelcontextprotocol/sdk back, or update mcp-http.ts to the new internals.",
148
+ );
149
+ return false;
150
+ } catch (err) {
151
+ console.error(
152
+ `parachute-agent: could not verify MCP SDK stream contract (${(err as Error).message}); ` +
153
+ "HTTP-MCP delivery may be unreliable.",
154
+ );
155
+ return false;
156
+ }
157
+ }
158
+
159
+ /** Test/teardown helper — drop every registered session without touching transports. */
160
+ export function _resetSessionsForTest(): void {
161
+ sessionsByChannel.clear();
162
+ sessionsById.clear();
163
+ }
164
+
165
+ /**
166
+ * Test-only: register a fake session under a channel without booting a real
167
+ * transport. The `server` only needs a `.notification` method for the push
168
+ * tests; pass scopes to model a connection's grant.
169
+ */
170
+ export function _registerSessionForTest(
171
+ channel: string,
172
+ id: string,
173
+ server: Server,
174
+ scopes: string[],
175
+ opts?: { streamless?: boolean },
176
+ ): void {
177
+ // Model the real transport's deliverability: a connected session whose GET
178
+ // stream is open carries `_streamMapping['_GET_stream']` — the same key
179
+ // sessionHasLivePushStream + the SDK's send() consult. `streamless: true` models
180
+ // a session that registered (POSTed initialize) but never opened — or has since
181
+ // dropped — its GET stream: registered but NOT deliverable.
182
+ const transport = opts?.streamless
183
+ ? (undefined as never)
184
+ : ({ _streamMapping: new Map<string, unknown>([["_GET_stream", {}]]) } as never);
185
+ const session: McpSession = { server, transport, scopes };
186
+ registerSession(channel, id, session);
187
+ }
188
+
189
+ /** Test-only: remove a registered session — exercises the empty-set cleanup path. */
190
+ export function _unregisterSessionForTest(channel: string, id: string): void {
191
+ unregisterSession(channel, id);
192
+ }
193
+
194
+ // ---------------------------------------------------------------------------
195
+ // The wake — push to a channel's MCP sessions
196
+ // ---------------------------------------------------------------------------
197
+
198
+ /**
199
+ * Push an inbound message to every MCP session on `channel` as a
200
+ * `notifications/claude/agent` — the wake that pulls an idle session in to
201
+ * answer. The daemon calls this alongside the existing SSE `routeToChannel`, so
202
+ * both stdio bridges and HTTP MCP sessions receive the same inbound traffic.
203
+ */
204
+ export function pushToChannel(
205
+ channel: string,
206
+ content: string,
207
+ meta: Record<string, string>,
208
+ ): number {
209
+ const set = sessionsByChannel.get(channel);
210
+ if (!set) return 0;
211
+ let delivered = 0;
212
+ for (const session of set) {
213
+ // Only sessions with a LIVE GET push stream are deliverable. The SDK silently
214
+ // drops a notification to a streamless session (no throw, nothing buffered), so
215
+ // counting one here would falsely advance the channel's delivery mark. A
216
+ // streamless session is simply not woken by this live push (the deaf-on-restart
217
+ // backlog replay that used to recover it was retired with the interactive backend).
218
+ if (!sessionHasLivePushStream(session)) continue;
219
+ try {
220
+ void session.server.notification({
221
+ method: "notifications/claude/agent",
222
+ params: { content, meta: { source: "parachute-agent", ...meta } },
223
+ });
224
+ delivered++;
225
+ } catch {
226
+ // A dead session throws SYNCHRONOUSLY on a closed transport (SDK `send`), so
227
+ // this catch runs and the count stays honest; the transport's onclose handler
228
+ // removes the session from the set.
229
+ }
230
+ }
231
+ return delivered;
232
+ }
233
+
234
+ /**
235
+ * Push a permission verdict to a channel's MCP sessions (mirrors the SSE
236
+ * `permission_verdict` route → bridge's `notifications/claude/agent/permission`).
237
+ */
238
+ export function pushPermissionVerdict(
239
+ channel: string,
240
+ verdict: { request_id: string; behavior: string },
241
+ ): number {
242
+ const set = sessionsByChannel.get(channel);
243
+ if (!set) return 0;
244
+ let delivered = 0;
245
+ for (const session of set) {
246
+ // Same deliverability gate as pushToChannel: a streamless session can't
247
+ // receive the verdict (the SDK would drop it), so don't claim it did.
248
+ if (!sessionHasLivePushStream(session)) continue;
249
+ try {
250
+ void session.server.notification({
251
+ method: "notifications/claude/agent/permission",
252
+ params: { request_id: verdict.request_id, behavior: verdict.behavior },
253
+ });
254
+ delivered++;
255
+ } catch {}
256
+ }
257
+ return delivered;
258
+ }
259
+
260
+ // ---------------------------------------------------------------------------
261
+ // Tool surface — ported from bridge.ts, dispatched to the channel's transport
262
+ // ---------------------------------------------------------------------------
263
+
264
+ const INSTRUCTIONS = [
265
+ "You are connected to a live chat channel over HTTP MCP. A human is messaging you through it.",
266
+ "",
267
+ "CRITICAL — HOW THE HUMAN SEES YOU: they read ONLY what you send via the `reply` tool. Your normal assistant/transcript text is INVISIBLE to them. So for EVERY message that arrives on this channel you MUST call the `reply` tool to answer — even a one-word reply. Never answer only in your transcript: if you don't call `reply`, the human sees nothing at all.",
268
+ "",
269
+ 'Inbound messages arrive as <channel source="parachute-agent" ...> with metadata attributes describing the sender. Treat each as a chat message from the human and respond by calling the reply tool — the daemon routes it back out the same channel. Pass back any addressing fields the inbound tag carried (e.g. chat_id) if present; omit them otherwise. Use reply_to (message_id) to thread a specific message; omit it for normal responses.',
270
+ "",
271
+ "If the tag has an image_path attribute, Read that file — it is an attachment the sender sent.",
272
+ "If the tag has attachment_file_id, call download_attachment with that file_id to fetch the file, then Read the returned path.",
273
+ "",
274
+ "Use react to add emoji reactions. Use edit_message for interim progress updates (edits do not push notifications — send a new reply when a long task completes so the user's device pings).",
275
+ "",
276
+ 'reply accepts file paths (files: ["/abs/path.png"]) for attachments.',
277
+ ].join("\n");
278
+
279
+ /** The tool list — identical schema to bridge.ts (transport-neutral: no chat_id required on reply). */
280
+ const TOOL_DEFS = [
281
+ {
282
+ name: "reply",
283
+ description:
284
+ "Send a message back through the channel to the sender. Supports text, file attachments, and quote-reply threading. The daemon routes it out whichever transport the channel uses.",
285
+ inputSchema: {
286
+ type: "object" as const,
287
+ properties: {
288
+ text: { type: "string", description: "Message text (optional if files provided)" },
289
+ reply_to: { type: "string", description: "Message ID to quote-reply (optional)" },
290
+ files: {
291
+ type: "array",
292
+ items: { type: "string" },
293
+ description: "Absolute file paths to attach (optional)",
294
+ },
295
+ chat_id: {
296
+ type: "string",
297
+ description:
298
+ "Addressing field some transports need (e.g. a Telegram chat ID). Include it ONLY if the inbound message tag carried one; omit it otherwise (e.g. for a web/UI channel).",
299
+ },
300
+ },
301
+ required: [] as string[],
302
+ },
303
+ },
304
+ {
305
+ name: "react",
306
+ description:
307
+ "Add an emoji reaction to a message, on transports that support reactions (e.g. Telegram's fixed emoji whitelist 👍 👎 ❤ 🔥 👀). Not all channels support this.",
308
+ inputSchema: {
309
+ type: "object" as const,
310
+ properties: {
311
+ message_id: { type: "string", description: "Message ID to react to" },
312
+ emoji: { type: "string", description: "Emoji reaction" },
313
+ chat_id: {
314
+ type: "string",
315
+ description: "Addressing field for transports that need it (e.g. Telegram chat ID); omit otherwise.",
316
+ },
317
+ },
318
+ required: ["message_id", "emoji"],
319
+ },
320
+ },
321
+ {
322
+ name: "edit_message",
323
+ description:
324
+ "Edit a message you previously sent. Useful for progress updates. On most transports edits don't push a notification.",
325
+ inputSchema: {
326
+ type: "object" as const,
327
+ properties: {
328
+ message_id: { type: "string", description: "Message ID to edit" },
329
+ text: { type: "string", description: "New text" },
330
+ chat_id: {
331
+ type: "string",
332
+ description: "Addressing field for transports that need it (e.g. Telegram chat ID); omit otherwise.",
333
+ },
334
+ },
335
+ required: ["message_id", "text"],
336
+ },
337
+ },
338
+ {
339
+ name: "download_attachment",
340
+ description: "Download a Telegram file attachment by file_id. Returns the local path.",
341
+ inputSchema: {
342
+ type: "object" as const,
343
+ properties: {
344
+ file_id: { type: "string", description: "Telegram file_id from the attachment_file_id attribute" },
345
+ },
346
+ required: ["file_id"],
347
+ },
348
+ },
349
+ ];
350
+
351
+ // Tools that send/mutate on the channel require agent:write. download_attachment
352
+ // is deliberately NOT here: fetching an attachment that was sent *to* this session is
353
+ // read-access — an agent:read session can receive and read its own messages,
354
+ // attachments included. (The legacy stdio-bridge /api/download gates it as write; the
355
+ // MCP path is the principled read. If they ever need to match, relax the bridge, not this.)
356
+ const WRITE_TOOLS = new Set(["reply", "react", "edit_message"]);
357
+
358
+ // ---------------------------------------------------------------------------
359
+ // ATTACHED-BACKEND tool surface — the MCP pull/reply protocol (design
360
+ // 2026-06-18-channel-backend.md, phase 2). When the channel has a `backend:attached`
361
+ // agent registered, the session connects + PULLs the durable queue instead of being
362
+ // pushed to: `pending` / `next-message` / `reply` / `release`, dispatched to the
363
+ // {@link AttachedQueueRegistry}. The session "is" the agent by adopting the
364
+ // systemPrompt `next-message` returns (the def body) — reinforced by INSTRUCTIONS
365
+ // below, since MCP can't force a system prompt on the caller.
366
+ // ---------------------------------------------------------------------------
367
+
368
+ const CHANNEL_INSTRUCTIONS = [
369
+ "You are connected to a Parachute CHANNEL — a durable queue of messages a human sent to an agent you are handling. Nothing is pushed to you; you PULL.",
370
+ "",
371
+ "THE LOOP, every time you're ready to handle a message:",
372
+ " 1. `pending` — see how many messages are waiting (count + a preview of each).",
373
+ " 2. `next-message` — claim the oldest waiting message. It returns { id, text, inReplyTo, systemPrompt }.",
374
+ " 3. TREAT THE RETURNED `systemPrompt` AS YOUR INSTRUCTIONS FOR THIS REPLY — it is the agent's persona/role. Adopt it: answer as that agent would.",
375
+ " 4. Do the work in this session (your full tools, your env).",
376
+ " 5. `reply` { inReplyTo: <the claimed id>, text: <your answer> } — this writes the reply back to the human and marks the message handled.",
377
+ "",
378
+ "If you claim a message with `next-message` but can't handle it, call `release` { id } to return it to the queue for another session (or your next pass). Claimed messages auto-release after a while if you go away, so the queue never gets stranded.",
379
+ "",
380
+ "The human reads ONLY what you send via `reply`. Your transcript text is invisible to them — always finish a handled message with a `reply`.",
381
+ ].join("\n");
382
+
383
+ /** The attached-backend pull/reply tool list (design 2026-06-18, phase 2). */
384
+ const CHANNEL_TOOL_DEFS = [
385
+ {
386
+ name: "pending",
387
+ description:
388
+ "How many inbound messages are waiting on this channel + a preview of each (id + a short text snippet). Use it to decide whether to pull. Read-only — claims nothing.",
389
+ inputSchema: { type: "object" as const, properties: {}, required: [] as string[] },
390
+ },
391
+ {
392
+ name: "next-message",
393
+ description:
394
+ "Claim the OLDEST waiting message and return it: { id, text, inReplyTo, systemPrompt }. Marks it in-flight so another connected session won't also handle it. Treat the returned systemPrompt as your instructions for the reply (it is the agent's persona). Returns nothing-to-do when the queue is empty. Pass the returned id back as `reply`'s inReplyTo.",
395
+ inputSchema: { type: "object" as const, properties: {}, required: [] as string[] },
396
+ },
397
+ {
398
+ name: "reply",
399
+ description:
400
+ "Send your answer back to the human AND mark the claimed message handled. inReplyTo is the id you got from next-message (threads the reply); text is your answer. Writes a durable outbound note that shows in the chat UI.",
401
+ inputSchema: {
402
+ type: "object" as const,
403
+ properties: {
404
+ inReplyTo: {
405
+ type: "string",
406
+ description: "The message id you claimed via next-message (threads the reply + marks it handled).",
407
+ },
408
+ text: { type: "string", description: "Your reply text." },
409
+ },
410
+ required: ["text"] as string[],
411
+ },
412
+ },
413
+ {
414
+ name: "release",
415
+ description:
416
+ "Un-claim a message you claimed but won't handle — returns it to the waiting queue (status pending) for another session or your next pass. id is the message id from next-message.",
417
+ inputSchema: {
418
+ type: "object" as const,
419
+ properties: {
420
+ id: { type: "string", description: "The message id to release back to pending." },
421
+ },
422
+ required: ["id"] as string[],
423
+ },
424
+ },
425
+ ];
426
+
427
+ /** Attached-backend tools that mutate the queue/write outbound require agent:write
428
+ * (`pending` + `next-message`... next-message claims, so it mutates → write; pending
429
+ * is read-only). reply + release + next-message mutate; pending is read. */
430
+ const CHANNEL_WRITE_TOOLS = new Set(["next-message", "reply", "release"]);
431
+
432
+ /**
433
+ * Dispatch one ATTACHED-backend tool call to the {@link AttachedQueueRegistry},
434
+ * enforcing write scope on the mutating tools. Returns a tool-error result (not a
435
+ * throw) when no attached-backend agent is registered for `channel` — so a session that
436
+ * connected to a non-attached channel and called these tools gets a clean "not an attached
437
+ * agent" message rather than a crash. Pure over its inputs (the daemon's per-session
438
+ * handler + the unit tests both call it).
439
+ */
440
+ export async function dispatchChannelTool(
441
+ channel: string,
442
+ attachedQueue: AttachedQueueRegistry,
443
+ scopes: string[],
444
+ name: string,
445
+ args: Record<string, unknown>,
446
+ ): Promise<ToolResult> {
447
+ // Gate cleanly for a non-attached channel — these tools are meaningful only when an
448
+ // attached-backend agent is registered. (The daemon also only serves CHANNEL_TOOL_DEFS
449
+ // when the agent is registered, so a well-behaved client never reaches here for a
450
+ // non-attached channel — but a hand-crafted call should fail gracefully, not 500.)
451
+ if (!attachedQueue.hasChannel(channel)) {
452
+ return {
453
+ content: [{ type: "text", text: `channel "${channel}" has no attached-backend agent — the pull/reply tools are not available here` }],
454
+ isError: true,
455
+ };
456
+ }
457
+ if (CHANNEL_WRITE_TOOLS.has(name) && !grantsScope(scopes, SCOPE_WRITE)) {
458
+ return {
459
+ content: [
460
+ {
461
+ type: "text",
462
+ text: `tool "${name}" requires the ${SCOPE_WRITE} scope, which this connection's token lacks`,
463
+ },
464
+ ],
465
+ isError: true,
466
+ };
467
+ }
468
+ try {
469
+ switch (name) {
470
+ case "pending": {
471
+ const view = await attachedQueue.pending(channel);
472
+ return { content: [{ type: "text", text: JSON.stringify(view) }] };
473
+ }
474
+ case "next-message": {
475
+ const claimed = await attachedQueue.claimNext(channel);
476
+ if (!claimed) {
477
+ return { content: [{ type: "text", text: JSON.stringify({ message: null, note: "no pending messages" }) }] };
478
+ }
479
+ return { content: [{ type: "text", text: JSON.stringify(claimed) }] };
480
+ }
481
+ case "reply": {
482
+ const text = typeof args.text === "string" ? args.text : "";
483
+ const inReplyTo = typeof args.inReplyTo === "string" ? args.inReplyTo : undefined;
484
+ const sent = await attachedQueue.reply(channel, { text, ...(inReplyTo ? { inReplyTo } : {}) });
485
+ const ids = sent.sent;
486
+ return {
487
+ content: [{ type: "text", text: `replied + marked handled (outbound id: ${ids.join(", ")})` }],
488
+ };
489
+ }
490
+ case "release": {
491
+ const id = typeof args.id === "string" ? args.id : "";
492
+ if (!id) {
493
+ return { content: [{ type: "text", text: "release requires an id" }], isError: true };
494
+ }
495
+ await attachedQueue.release(channel, id);
496
+ return { content: [{ type: "text", text: `released ${id} back to pending` }] };
497
+ }
498
+ default:
499
+ return { content: [{ type: "text", text: `unknown channel tool: ${name}` }], isError: true };
500
+ }
501
+ } catch (err) {
502
+ return {
503
+ content: [{ type: "text", text: `${name} failed: ${err instanceof Error ? err.message : String(err)}` }],
504
+ isError: true,
505
+ };
506
+ }
507
+ }
508
+
509
+ /** Fold a top-level chat_id into meta, mirroring the daemon's mergeMeta. */
510
+ function mergeMeta(args: Record<string, unknown>): Record<string, string> {
511
+ const meta: Record<string, string> = {};
512
+ if (typeof args.chat_id === "string") meta.chat_id = args.chat_id;
513
+ return meta;
514
+ }
515
+
516
+ /**
517
+ * Build the per-session MCP Server. Tool handlers dispatch to `transport` — the
518
+ * SAME transport methods the daemon's `/api/*` handlers call — and check write
519
+ * scope against `session.scopes` (mutated by the caller after construction so
520
+ * the closure reads the live value). `channel` is threaded so outbound args
521
+ * carry the right channel context.
522
+ */
523
+ function buildServer(
524
+ channel: string,
525
+ transport: Transport,
526
+ session: McpSession,
527
+ attachedQueue?: AttachedQueueRegistry,
528
+ ): Server {
529
+ // ── ATTACHED-BACKEND FORK (design 2026-06-18). When a `backend:attached` agent is
530
+ // registered for this channel, serve the PULL/REPLY surface (pending / next-message
531
+ // / reply / release) + its reinforcing INSTRUCTIONS, dispatched to the
532
+ // AttachedQueueRegistry. Otherwise serve the existing push surface (reply / react /
533
+ // edit / download), dispatched to the transport. Resolved at connect time — a
534
+ // channel doesn't switch backends under a live session.
535
+ const isAttachedBackend = !!attachedQueue?.hasChannel(channel);
536
+ const server = new Server(
537
+ // Per-channel name (`agent-<name>`) so it reads clearly in `/mcp` and lines
538
+ // up with the `--dangerously-load-development-channels=server:agent-<name>`
539
+ // flag + the `claude mcp add agent-<name>` name the setup UI/launcher use.
540
+ { name: `agent-${channel}`, version: "0.1.0" },
541
+ {
542
+ capabilities: {
543
+ experimental: {
544
+ "claude/agent": {},
545
+ "claude/agent/permission": {},
546
+ },
547
+ tools: {},
548
+ },
549
+ instructions: isAttachedBackend ? CHANNEL_INSTRUCTIONS : INSTRUCTIONS,
550
+ },
551
+ );
552
+
553
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({
554
+ tools: isAttachedBackend ? CHANNEL_TOOL_DEFS : TOOL_DEFS,
555
+ }));
556
+
557
+ // Dispatch reads `session.scopes` live (the daemon refreshes it per request),
558
+ // so a re-auth with a narrower token takes effect on the next tool call.
559
+ server.setRequestHandler(CallToolRequestSchema, async (req) => {
560
+ const args = (req.params.arguments ?? {}) as Record<string, unknown>;
561
+ const result =
562
+ isAttachedBackend && attachedQueue
563
+ ? await dispatchChannelTool(channel, attachedQueue, session.scopes, req.params.name, args)
564
+ : await dispatchTool(channel, transport, session.scopes, req.params.name, args);
565
+ // Our ToolResult is the content/isError subset of the SDK's CallToolResult
566
+ // (which also has a task-variant we never emit); return it as that shape.
567
+ return result as { content: Array<{ type: "text"; text: string }>; isError?: boolean };
568
+ });
569
+
570
+ return server;
571
+ }
572
+
573
+ /** A tool-call result in the MCP content shape. */
574
+ export interface ToolResult {
575
+ content: Array<{ type: "text"; text: string }>;
576
+ isError?: boolean;
577
+ }
578
+
579
+ /**
580
+ * Dispatch one tool call to the channel's transport, enforcing write scope on
581
+ * the mutating tools (reply/react/edit) from the connection's `scopes`. Pure
582
+ * over its inputs — the daemon's per-session handler and the unit tests both
583
+ * call it, so tool behavior is asserted without standing up an MCP transport.
584
+ */
585
+ export async function dispatchTool(
586
+ channel: string,
587
+ transport: Transport,
588
+ scopes: string[],
589
+ name: string,
590
+ args: Record<string, unknown>,
591
+ ): Promise<ToolResult> {
592
+ // Read-only tokens connect + receive the wake, but cannot send. Enforce
593
+ // agent:write on the mutating tools using the connection's own scopes.
594
+ // Dual-accept (channel→agent rename): a pre-rename token carrying the legacy
595
+ // `channel:write` scope also authorizes — `grantsScope` matches agent:write OR
596
+ // its channel:write alias — so HTTP-MCP sends keep working until tokens re-mint.
597
+ if (WRITE_TOOLS.has(name) && !grantsScope(scopes, SCOPE_WRITE)) {
598
+ return {
599
+ content: [
600
+ {
601
+ type: "text",
602
+ text: `tool "${name}" requires the ${SCOPE_WRITE} scope, which this connection's token lacks`,
603
+ },
604
+ ],
605
+ isError: true,
606
+ };
607
+ }
608
+
609
+ try {
610
+ switch (name) {
611
+ case "reply": {
612
+ const replyArgs: ReplyArgs = {
613
+ channel,
614
+ text: typeof args.text === "string" ? args.text : undefined,
615
+ files: Array.isArray(args.files) ? (args.files as string[]) : undefined,
616
+ reply_to: typeof args.reply_to === "string" ? args.reply_to : undefined,
617
+ meta: mergeMeta(args),
618
+ };
619
+ const result = await transport.reply(replyArgs);
620
+ const ids = result.sent;
621
+ const parts = ids.length === 1 ? `(id: ${ids[0]})` : `(ids: ${ids.join(", ")})`;
622
+ return {
623
+ content: [{ type: "text", text: `sent ${ids.length} part${ids.length === 1 ? "" : "s"} ${parts}` }],
624
+ };
625
+ }
626
+
627
+ case "react": {
628
+ if (!transport.react) return methodMissing(channel, transport, "react");
629
+ const reactArgs: ReactArgs = {
630
+ channel,
631
+ message_id: String(args.message_id ?? ""),
632
+ emoji: String(args.emoji ?? ""),
633
+ meta: mergeMeta(args),
634
+ };
635
+ await transport.react(reactArgs);
636
+ return { content: [{ type: "text", text: "reacted" }] };
637
+ }
638
+
639
+ case "edit_message": {
640
+ if (!transport.edit) return methodMissing(channel, transport, "edit");
641
+ const editArgs: EditArgs = {
642
+ channel,
643
+ message_id: String(args.message_id ?? ""),
644
+ text: String(args.text ?? ""),
645
+ meta: mergeMeta(args),
646
+ };
647
+ await transport.edit(editArgs);
648
+ return { content: [{ type: "text", text: "edited" }] };
649
+ }
650
+
651
+ case "download_attachment": {
652
+ if (!transport.download) return methodMissing(channel, transport, "download");
653
+ const dlArgs: DownloadArgs = { channel, file_id: String(args.file_id ?? "") };
654
+ const result = await transport.download(dlArgs);
655
+ return { content: [{ type: "text", text: result.path }] };
656
+ }
657
+
658
+ default:
659
+ return { content: [{ type: "text", text: `unknown tool: ${name}` }], isError: true };
660
+ }
661
+ } catch (err) {
662
+ return {
663
+ content: [{ type: "text", text: `${name} failed: ${err instanceof Error ? err.message : String(err)}` }],
664
+ isError: true,
665
+ };
666
+ }
667
+ }
668
+
669
+ /** Thin alias the reply-dispatch tests read against. */
670
+ export function callReplyTool(
671
+ channel: string,
672
+ transport: Transport,
673
+ scopes: string[],
674
+ args: Record<string, unknown>,
675
+ ): Promise<ToolResult> {
676
+ return dispatchTool(channel, transport, scopes, "reply", args);
677
+ }
678
+
679
+ function methodMissing(channel: string, transport: Transport, method: string): ToolResult {
680
+ return {
681
+ content: [
682
+ {
683
+ type: "text",
684
+ text: `transport "${transport.kind}" for channel "${channel}" does not support ${method}`,
685
+ },
686
+ ],
687
+ isError: true,
688
+ };
689
+ }
690
+
691
+ // ---------------------------------------------------------------------------
692
+ // HTTP entry — the daemon routes POST/GET/DELETE /mcp/<channel> here
693
+ // ---------------------------------------------------------------------------
694
+
695
+ /**
696
+ * Handle a stateful Streamable HTTP MCP request for `channel`, dispatching tool
697
+ * calls to `transport`. The daemon has ALREADY validated `agent:read` and
698
+ * passes the caller's scopes in (threaded onto the session for write-tool
699
+ * checks). Returns the transport's Response (JSON or an SSE stream).
700
+ *
701
+ * Session resolution mirrors the probe:
702
+ * - existing mcp-session-id → reuse its transport.
703
+ * - POST with no session id → a NEW session (initialize handshake).
704
+ * - anything else with no/unknown session id → 400 (no session to attach to).
705
+ */
706
+ export async function handleMcp(
707
+ req: Request,
708
+ channel: string,
709
+ transport: Transport,
710
+ scopes: string[],
711
+ attachedQueue?: AttachedQueueRegistry,
712
+ ): Promise<Response> {
713
+ const sid = req.headers.get("mcp-session-id");
714
+
715
+ if (sid && sessionsById.has(sid)) {
716
+ const existing = sessionsById.get(sid)!;
717
+ // Refresh the connection's scopes from the presented token each request, so
718
+ // a re-auth with a narrower/wider token takes effect (the daemon re-validates
719
+ // agent:read on every call before reaching here).
720
+ existing.scopes = scopes;
721
+ // A GET opens (or reopens) the standalone SSE push stream for the live wake
722
+ // (`pushToChannel`). The deaf-on-restart BACKLOG REPLAY that used to fire here
723
+ // was retired with the interactive backend (design
724
+ // 2026-06-19-retire-interactive-backend.md): the programmatic path runs
725
+ // synchronously and the channel path uses the durable note-claim queue, so
726
+ // there's no missed-while-idle backlog to replay onto a reconnecting session.
727
+ return existing.transport.handleRequest(req);
728
+ }
729
+
730
+ if (req.method === "DELETE") {
731
+ // No session to delete — treat as a no-op success.
732
+ return new Response(null, { status: 204 });
733
+ }
734
+
735
+ if (req.method !== "POST") {
736
+ return new Response(
737
+ JSON.stringify({ jsonrpc: "2.0", error: { code: -32000, message: "No valid session" }, id: null }),
738
+ { status: 400, headers: { "content-type": "application/json" } },
739
+ );
740
+ }
741
+
742
+ // New session — build the transport + server, register on init.
743
+ const session: McpSession = {
744
+ // Placeholders replaced immediately below; the object identity is what the
745
+ // registry + tool closures capture.
746
+ server: undefined as unknown as Server,
747
+ transport: undefined as unknown as WebStandardStreamableHTTPServerTransport,
748
+ scopes,
749
+ };
750
+
751
+ const httpTransport = new WebStandardStreamableHTTPServerTransport({
752
+ sessionIdGenerator: () => crypto.randomUUID(),
753
+ enableJsonResponse: false,
754
+ onsessioninitialized: (id: string) => {
755
+ registerSession(channel, id, session);
756
+ },
757
+ });
758
+
759
+ const server = buildServer(channel, transport, session, attachedQueue);
760
+ session.server = server;
761
+ session.transport = httpTransport;
762
+
763
+ // Clean up on transport close (client disconnect / DELETE / stream end).
764
+ httpTransport.onclose = () => {
765
+ const id = httpTransport.sessionId;
766
+ if (id) unregisterSession(channel, id);
767
+ };
768
+
769
+ await server.connect(httpTransport);
770
+ return httpTransport.handleRequest(req);
771
+ }