@openparachute/agent 0.1.1 → 0.2.0

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 (598) hide show
  1. package/.parachute/module.json +124 -8
  2. package/LICENSE +2 -16
  3. package/README.md +118 -166
  4. package/package.json +32 -43
  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/tsconfig.json +2 -1
  103. package/.claude/scheduled_tasks.lock +0 -1
  104. package/.claude/settings.json +0 -5
  105. package/.claude/skills/add-atomic-chat-tool/SKILL.md +0 -243
  106. package/.claude/skills/add-atomic-chat-tool/atomic-chat-mcp-stdio.ts +0 -229
  107. package/.claude/skills/add-codex/SKILL.md +0 -161
  108. package/.claude/skills/add-dashboard/SKILL.md +0 -138
  109. package/.claude/skills/add-dashboard/resources/dashboard-pusher.ts +0 -495
  110. package/.claude/skills/add-emacs/SKILL.md +0 -296
  111. package/.claude/skills/add-gcal-tool/SKILL.md +0 -210
  112. package/.claude/skills/add-gchat/REMOVE.md +0 -6
  113. package/.claude/skills/add-gchat/SKILL.md +0 -92
  114. package/.claude/skills/add-gchat/VERIFY.md +0 -3
  115. package/.claude/skills/add-github/REMOVE.md +0 -6
  116. package/.claude/skills/add-github/SKILL.md +0 -148
  117. package/.claude/skills/add-github/VERIFY.md +0 -3
  118. package/.claude/skills/add-gmail-tool/SKILL.md +0 -229
  119. package/.claude/skills/add-imessage/REMOVE.md +0 -6
  120. package/.claude/skills/add-imessage/SKILL.md +0 -113
  121. package/.claude/skills/add-imessage/VERIFY.md +0 -3
  122. package/.claude/skills/add-karpathy-llm-wiki/SKILL.md +0 -110
  123. package/.claude/skills/add-karpathy-llm-wiki/llm-wiki.md +0 -75
  124. package/.claude/skills/add-linear/REMOVE.md +0 -6
  125. package/.claude/skills/add-linear/SKILL.md +0 -168
  126. package/.claude/skills/add-linear/VERIFY.md +0 -3
  127. package/.claude/skills/add-macos-statusbar/SKILL.md +0 -133
  128. package/.claude/skills/add-macos-statusbar/add/src/statusbar.swift +0 -147
  129. package/.claude/skills/add-matrix/REMOVE.md +0 -6
  130. package/.claude/skills/add-matrix/SKILL.md +0 -148
  131. package/.claude/skills/add-matrix/VERIFY.md +0 -3
  132. package/.claude/skills/add-ollama-provider/SKILL.md +0 -179
  133. package/.claude/skills/add-ollama-tool/SKILL.md +0 -193
  134. package/.claude/skills/add-opencode/SKILL.md +0 -229
  135. package/.claude/skills/add-parallel/SKILL.md +0 -290
  136. package/.claude/skills/add-resend/REMOVE.md +0 -6
  137. package/.claude/skills/add-resend/SKILL.md +0 -93
  138. package/.claude/skills/add-resend/VERIFY.md +0 -3
  139. package/.claude/skills/add-signal/REMOVE.md +0 -13
  140. package/.claude/skills/add-signal/SKILL.md +0 -318
  141. package/.claude/skills/add-signal/VERIFY.md +0 -5
  142. package/.claude/skills/add-slack/REMOVE.md +0 -6
  143. package/.claude/skills/add-slack/SKILL.md +0 -112
  144. package/.claude/skills/add-slack/VERIFY.md +0 -3
  145. package/.claude/skills/add-teams/REMOVE.md +0 -6
  146. package/.claude/skills/add-teams/SKILL.md +0 -207
  147. package/.claude/skills/add-teams/VERIFY.md +0 -3
  148. package/.claude/skills/add-vercel/SKILL.md +0 -147
  149. package/.claude/skills/add-vercel/container-skills/vercel-cli/SKILL.md +0 -103
  150. package/.claude/skills/add-webex/REMOVE.md +0 -6
  151. package/.claude/skills/add-webex/SKILL.md +0 -88
  152. package/.claude/skills/add-webex/VERIFY.md +0 -3
  153. package/.claude/skills/add-wechat/REMOVE.md +0 -49
  154. package/.claude/skills/add-wechat/SKILL.md +0 -170
  155. package/.claude/skills/add-wechat/scripts/wire-dm.ts +0 -172
  156. package/.claude/skills/add-whatsapp/SKILL.md +0 -264
  157. package/.claude/skills/add-whatsapp-cloud/REMOVE.md +0 -6
  158. package/.claude/skills/add-whatsapp-cloud/SKILL.md +0 -95
  159. package/.claude/skills/add-whatsapp-cloud/VERIFY.md +0 -3
  160. package/.claude/skills/claw/SKILL.md +0 -131
  161. package/.claude/skills/claw/scripts/claw +0 -374
  162. package/.claude/skills/convert-to-apple-container/SKILL.md +0 -212
  163. package/.claude/skills/customize/SKILL.md +0 -110
  164. package/.claude/skills/debug/SKILL.md +0 -349
  165. package/.claude/skills/get-qodo-rules/SKILL.md +0 -122
  166. package/.claude/skills/get-qodo-rules/references/output-format.md +0 -41
  167. package/.claude/skills/get-qodo-rules/references/pagination.md +0 -33
  168. package/.claude/skills/get-qodo-rules/references/repository-scope.md +0 -26
  169. package/.claude/skills/init-first-agent/SKILL.md +0 -120
  170. package/.claude/skills/init-onecli/SKILL.md +0 -270
  171. package/.claude/skills/manage-channels/SKILL.md +0 -87
  172. package/.claude/skills/manage-mounts/SKILL.md +0 -47
  173. package/.claude/skills/migrate-from-openclaw/MIGRATE_CRONS.md +0 -100
  174. package/.claude/skills/migrate-from-openclaw/SKILL.md +0 -447
  175. package/.claude/skills/migrate-from-openclaw/scripts/discover-openclaw.ts +0 -734
  176. package/.claude/skills/migrate-from-openclaw/scripts/extract-channel-credentials.ts +0 -476
  177. package/.claude/skills/migrate-nanoclaw/SKILL.md +0 -484
  178. package/.claude/skills/migrate-nanoclaw/diagnostics.md +0 -51
  179. package/.claude/skills/qodo-pr-resolver/SKILL.md +0 -326
  180. package/.claude/skills/qodo-pr-resolver/resources/providers.md +0 -329
  181. package/.claude/skills/update-nanoclaw/SKILL.md +0 -243
  182. package/.claude/skills/update-nanoclaw/diagnostics.md +0 -48
  183. package/.claude/skills/update-skills/SKILL.md +0 -130
  184. package/.claude/skills/use-native-credential-proxy/SKILL.md +0 -167
  185. package/.claude/skills/x-integration/SKILL.md +0 -417
  186. package/.claude/skills/x-integration/agent.ts +0 -243
  187. package/.claude/skills/x-integration/host.ts +0 -155
  188. package/.claude/skills/x-integration/lib/browser.ts +0 -148
  189. package/.claude/skills/x-integration/lib/config.ts +0 -62
  190. package/.claude/skills/x-integration/scripts/like.ts +0 -56
  191. package/.claude/skills/x-integration/scripts/post.ts +0 -66
  192. package/.claude/skills/x-integration/scripts/quote.ts +0 -80
  193. package/.claude/skills/x-integration/scripts/reply.ts +0 -74
  194. package/.claude/skills/x-integration/scripts/retweet.ts +0 -62
  195. package/.claude/skills/x-integration/scripts/setup.ts +0 -87
  196. package/.github/CODEOWNERS +0 -10
  197. package/.github/PULL_REQUEST_TEMPLATE.md +0 -18
  198. package/.github/workflows/bump-version.yml +0 -35
  199. package/.github/workflows/ci.yml +0 -39
  200. package/.github/workflows/label-pr.yml +0 -40
  201. package/.github/workflows/update-tokens.yml +0 -43
  202. package/.husky/pre-commit +0 -1
  203. package/.mcp.json +0 -3
  204. package/.nvmrc +0 -1
  205. package/.prettierrc +0 -4
  206. package/CHANGELOG.md +0 -221
  207. package/CLAUDE.md +0 -307
  208. package/CODE_OF_CONDUCT.md +0 -128
  209. package/CONTRIBUTING.md +0 -159
  210. package/CONTRIBUTORS.md +0 -26
  211. package/LICENSE-NANOCLAW-MIT +0 -21
  212. package/README_ja.md +0 -194
  213. package/README_zh.md +0 -194
  214. package/assets/nanoclaw-favicon.png +0 -0
  215. package/assets/nanoclaw-icon.png +0 -0
  216. package/assets/nanoclaw-logo-dark.png +0 -0
  217. package/assets/nanoclaw-logo.png +0 -0
  218. package/assets/nanoclaw-profile.jpeg +0 -0
  219. package/assets/nanoclaw-sales.png +0 -0
  220. package/assets/social-preview.jpg +0 -0
  221. package/config-examples/mount-allowlist.json +0 -25
  222. package/container/.dockerignore +0 -2
  223. package/container/CLAUDE.md +0 -21
  224. package/container/Dockerfile +0 -121
  225. package/container/agent-runner/bun.lock +0 -243
  226. package/container/agent-runner/package.json +0 -22
  227. package/container/agent-runner/scripts/sdk-signal-probe.ts +0 -169
  228. package/container/agent-runner/src/config.ts +0 -55
  229. package/container/agent-runner/src/db/connection.ts +0 -267
  230. package/container/agent-runner/src/db/index.ts +0 -20
  231. package/container/agent-runner/src/db/messages-in.ts +0 -138
  232. package/container/agent-runner/src/db/messages-out.ts +0 -143
  233. package/container/agent-runner/src/db/session-routing.ts +0 -30
  234. package/container/agent-runner/src/db/session-state.test.ts +0 -100
  235. package/container/agent-runner/src/db/session-state.ts +0 -79
  236. package/container/agent-runner/src/destinations.ts +0 -135
  237. package/container/agent-runner/src/formatter.test.ts +0 -167
  238. package/container/agent-runner/src/formatter.ts +0 -260
  239. package/container/agent-runner/src/index.ts +0 -110
  240. package/container/agent-runner/src/integration.test.ts +0 -121
  241. package/container/agent-runner/src/mcp-tools/agents.instructions.md +0 -26
  242. package/container/agent-runner/src/mcp-tools/agents.ts +0 -66
  243. package/container/agent-runner/src/mcp-tools/core.instructions.md +0 -27
  244. package/container/agent-runner/src/mcp-tools/core.ts +0 -262
  245. package/container/agent-runner/src/mcp-tools/index.ts +0 -22
  246. package/container/agent-runner/src/mcp-tools/interactive.instructions.md +0 -22
  247. package/container/agent-runner/src/mcp-tools/interactive.ts +0 -169
  248. package/container/agent-runner/src/mcp-tools/scheduling.instructions.md +0 -40
  249. package/container/agent-runner/src/mcp-tools/scheduling.ts +0 -299
  250. package/container/agent-runner/src/mcp-tools/self-mod.instructions.md +0 -25
  251. package/container/agent-runner/src/mcp-tools/self-mod.ts +0 -120
  252. package/container/agent-runner/src/mcp-tools/server.ts +0 -54
  253. package/container/agent-runner/src/mcp-tools/types.ts +0 -6
  254. package/container/agent-runner/src/poll-loop.test.ts +0 -248
  255. package/container/agent-runner/src/poll-loop.ts +0 -437
  256. package/container/agent-runner/src/providers/claude.ts +0 -379
  257. package/container/agent-runner/src/providers/factory.test.ts +0 -19
  258. package/container/agent-runner/src/providers/factory.ts +0 -13
  259. package/container/agent-runner/src/providers/index.ts +0 -6
  260. package/container/agent-runner/src/providers/mock.ts +0 -77
  261. package/container/agent-runner/src/providers/provider-registry.ts +0 -33
  262. package/container/agent-runner/src/providers/types.ts +0 -82
  263. package/container/agent-runner/src/scheduling/task-script.ts +0 -121
  264. package/container/agent-runner/src/timezone.test.ts +0 -93
  265. package/container/agent-runner/src/timezone.ts +0 -107
  266. package/container/agent-runner/tsconfig.json +0 -14
  267. package/container/build.sh +0 -48
  268. package/container/entrypoint.sh +0 -16
  269. package/container/skills/agent-browser/SKILL.md +0 -159
  270. package/container/skills/frontend-engineer/SKILL.md +0 -157
  271. package/container/skills/self-customize/SKILL.md +0 -87
  272. package/container/skills/slack-formatting/SKILL.md +0 -94
  273. package/container/skills/vercel-cli/SKILL.md +0 -111
  274. package/container/skills/welcome/SKILL.md +0 -85
  275. package/docs/APPLE-CONTAINER-NETWORKING.md +0 -90
  276. package/docs/BRANCH-FORK-MAINTENANCE.md +0 -81
  277. package/docs/README.md +0 -25
  278. package/docs/SDK_DEEP_DIVE.md +0 -643
  279. package/docs/SECURITY.md +0 -162
  280. package/docs/agent-runner-details.md +0 -749
  281. package/docs/api-details.md +0 -365
  282. package/docs/architecture-diagram.html +0 -422
  283. package/docs/architecture-diagram.md +0 -215
  284. package/docs/architecture.md +0 -751
  285. package/docs/audit/2026-04-30-channel-endpoint-audit.md +0 -36
  286. package/docs/build-and-runtime.md +0 -80
  287. package/docs/cross-mount-stress/README.md +0 -112
  288. package/docs/cross-mount-stress/container-writer-retry.mjs +0 -55
  289. package/docs/cross-mount-stress/container-writer-slow.mjs +0 -42
  290. package/docs/cross-mount-stress/container-writer.mjs +0 -47
  291. package/docs/cross-mount-stress/host-writer-retry.mjs +0 -55
  292. package/docs/cross-mount-stress/host-writer-slow.mjs +0 -43
  293. package/docs/cross-mount-stress/host-writer.mjs +0 -47
  294. package/docs/db-central.md +0 -316
  295. package/docs/db-session.md +0 -183
  296. package/docs/db.md +0 -119
  297. package/docs/design/2026-04-29-vault-management-ui.md +0 -231
  298. package/docs/design/2026-04-30-channel-wiring-rework.md +0 -234
  299. package/docs/design/2026-05-01-channel-wiring-approvals-deep-dive.md +0 -272
  300. package/docs/design/2026-05-02-channel-policy-and-approval-routing.md +0 -250
  301. package/docs/docker-sandboxes.md +0 -359
  302. package/docs/isolation-model.md +0 -88
  303. package/docs/ollama.md +0 -79
  304. package/docs/parachute-integration.md +0 -109
  305. package/docs/post-night-rebirth-reflections.md +0 -151
  306. package/eslint.config.js +0 -32
  307. package/pnpm-workspace.yaml +0 -8
  308. package/repo-tokens/README.md +0 -113
  309. package/repo-tokens/action.yml +0 -186
  310. package/repo-tokens/badge.svg +0 -23
  311. package/repo-tokens/examples/green.svg +0 -14
  312. package/repo-tokens/examples/red.svg +0 -14
  313. package/repo-tokens/examples/yellow-green.svg +0 -14
  314. package/repo-tokens/examples/yellow.svg +0 -14
  315. package/scripts/chat.ts +0 -101
  316. package/scripts/cleanup-sessions.sh +0 -150
  317. package/scripts/init-cli-agent.ts +0 -171
  318. package/scripts/init-first-agent.ts +0 -377
  319. package/scripts/parachute.ts +0 -158
  320. package/scripts/run-migrations.ts +0 -105
  321. package/scripts/sanity-live-poll.ts +0 -95
  322. package/scripts/seed-discord.ts +0 -79
  323. package/scripts/test-v2-agent.ts +0 -106
  324. package/scripts/test-v2-channel-e2e.ts +0 -265
  325. package/scripts/test-v2-host.ts +0 -184
  326. package/src/channels/adapter.ts +0 -214
  327. package/src/channels/ask-question.ts +0 -46
  328. package/src/channels/channel-registry.test.ts +0 -421
  329. package/src/channels/channel-registry.ts +0 -313
  330. package/src/channels/chat-sdk-bridge.test.ts +0 -84
  331. package/src/channels/chat-sdk-bridge.ts +0 -652
  332. package/src/channels/cli.ts +0 -276
  333. package/src/channels/discord.ts +0 -90
  334. package/src/channels/index.ts +0 -17
  335. package/src/channels/telegram-markdown-sanitize.test.ts +0 -78
  336. package/src/channels/telegram-markdown-sanitize.ts +0 -55
  337. package/src/channels/telegram-pairing.test.ts +0 -254
  338. package/src/channels/telegram-pairing.ts +0 -339
  339. package/src/channels/telegram.ts +0 -279
  340. package/src/channels/trust-hint.test.ts +0 -48
  341. package/src/channels/trust-hint.ts +0 -75
  342. package/src/claude-md-compose.migrate.test.ts +0 -64
  343. package/src/claude-md-compose.ts +0 -205
  344. package/src/command-gate.ts +0 -63
  345. package/src/config.test.ts +0 -93
  346. package/src/config.ts +0 -108
  347. package/src/container-config.ts +0 -167
  348. package/src/container-runner.test.ts +0 -32
  349. package/src/container-runner.ts +0 -576
  350. package/src/container-runtime.test.ts +0 -169
  351. package/src/container-runtime.ts +0 -92
  352. package/src/db/_bun-sqlite-shim.ts +0 -88
  353. package/src/db/agent-activity.test.ts +0 -155
  354. package/src/db/agent-activity.ts +0 -121
  355. package/src/db/agent-groups.ts +0 -77
  356. package/src/db/connection.migrate.test.ts +0 -143
  357. package/src/db/connection.ts +0 -224
  358. package/src/db/db-v2.test.ts +0 -440
  359. package/src/db/dropped-messages.ts +0 -44
  360. package/src/db/index.ts +0 -40
  361. package/src/db/messaging-groups.ts +0 -252
  362. package/src/db/migrations/001-initial.ts +0 -112
  363. package/src/db/migrations/002-chat-sdk-state.ts +0 -36
  364. package/src/db/migrations/008-dropped-messages.ts +0 -27
  365. package/src/db/migrations/009-drop-pending-credentials.ts +0 -13
  366. package/src/db/migrations/010-engage-modes.ts +0 -103
  367. package/src/db/migrations/011-pending-sender-approvals.ts +0 -40
  368. package/src/db/migrations/012-channel-registration.ts +0 -48
  369. package/src/db/migrations/013-approval-render-metadata.ts +0 -27
  370. package/src/db/migrations/014-secrets.ts +0 -44
  371. package/src/db/migrations/015-secrets-drop-host-pattern.ts +0 -18
  372. package/src/db/migrations/016-secret-assignments.ts +0 -30
  373. package/src/db/migrations/017-agent-activity.ts +0 -40
  374. package/src/db/migrations/018-oauth-app-configs.ts +0 -34
  375. package/src/db/migrations/019-oauth-app-connections.ts +0 -48
  376. package/src/db/migrations/020-agent-app-connections.ts +0 -28
  377. package/src/db/migrations/021-pending-oauth-states.ts +0 -35
  378. package/src/db/migrations/022-app-connections-provider.ts +0 -25
  379. package/src/db/migrations/023-agent-group-secret-mode.test.ts +0 -124
  380. package/src/db/migrations/023-agent-group-secret-mode.ts +0 -65
  381. package/src/db/migrations/024-collapse-approvals.test.ts +0 -249
  382. package/src/db/migrations/024-collapse-approvals.ts +0 -182
  383. package/src/db/migrations/025-secret-mode-check.test.ts +0 -155
  384. package/src/db/migrations/025-secret-mode-check.ts +0 -49
  385. package/src/db/migrations/026-user-dms-bot-id.test.ts +0 -116
  386. package/src/db/migrations/026-user-dms-bot-id.ts +0 -54
  387. package/src/db/migrations/027-provider-credentials.ts +0 -41
  388. package/src/db/migrations/_test-helpers.ts +0 -41
  389. package/src/db/migrations/index.ts +0 -127
  390. package/src/db/migrations/module-agent-to-agent-destinations.ts +0 -84
  391. package/src/db/migrations/module-approvals-pending-approvals.ts +0 -42
  392. package/src/db/migrations/module-approvals-title-options.ts +0 -40
  393. package/src/db/schema.ts +0 -258
  394. package/src/db/session-db.test.ts +0 -93
  395. package/src/db/session-db.ts +0 -325
  396. package/src/db/sessions.ts +0 -241
  397. package/src/delivery.test.ts +0 -148
  398. package/src/delivery.ts +0 -445
  399. package/src/env.ts +0 -74
  400. package/src/group-folder.test.ts +0 -35
  401. package/src/group-folder.ts +0 -44
  402. package/src/group-init.ts +0 -92
  403. package/src/host-core.test.ts +0 -456
  404. package/src/host-sweep.test.ts +0 -146
  405. package/src/host-sweep.ts +0 -287
  406. package/src/index.ts +0 -227
  407. package/src/install-slug.ts +0 -33
  408. package/src/log.test.ts +0 -81
  409. package/src/log.ts +0 -117
  410. package/src/mcp/http.ts +0 -72
  411. package/src/mcp/server.ts +0 -92
  412. package/src/mcp/stdio.ts +0 -51
  413. package/src/mcp/tools/activity.ts +0 -88
  414. package/src/mcp/tools/agent-groups.ts +0 -183
  415. package/src/mcp/tools/approvals.ts +0 -122
  416. package/src/mcp/tools/channels.ts +0 -199
  417. package/src/mcp/tools/index.ts +0 -27
  418. package/src/mcp/tools/oauth.ts +0 -48
  419. package/src/mcp/tools/secrets.ts +0 -169
  420. package/src/mcp/tools/sessions.ts +0 -135
  421. package/src/mcp/types.ts +0 -51
  422. package/src/modules/agent-to-agent/agent-route.test.ts +0 -46
  423. package/src/modules/agent-to-agent/agent-route.ts +0 -223
  424. package/src/modules/agent-to-agent/create-agent.ts +0 -127
  425. package/src/modules/agent-to-agent/db/agent-destinations.ts +0 -135
  426. package/src/modules/agent-to-agent/index.ts +0 -22
  427. package/src/modules/agent-to-agent/write-destinations.ts +0 -59
  428. package/src/modules/approvals/agent.md +0 -45
  429. package/src/modules/approvals/index.ts +0 -21
  430. package/src/modules/approvals/picks.test.ts +0 -291
  431. package/src/modules/approvals/primitive.ts +0 -279
  432. package/src/modules/approvals/project.md +0 -27
  433. package/src/modules/approvals/response-handler.ts +0 -87
  434. package/src/modules/index.ts +0 -24
  435. package/src/modules/interactive/agent.md +0 -21
  436. package/src/modules/interactive/index.ts +0 -69
  437. package/src/modules/interactive/project.md +0 -12
  438. package/src/modules/mount-security/index.ts +0 -448
  439. package/src/modules/mount-security/migrate.test.ts +0 -91
  440. package/src/modules/permissions/access.ts +0 -28
  441. package/src/modules/permissions/channel-approval.test.ts +0 -389
  442. package/src/modules/permissions/channel-approval.ts +0 -188
  443. package/src/modules/permissions/db/agent-group-members.ts +0 -44
  444. package/src/modules/permissions/db/pending-channel-approvals.test.ts +0 -86
  445. package/src/modules/permissions/db/pending-channel-approvals.ts +0 -66
  446. package/src/modules/permissions/db/pending-sender-approvals.ts +0 -60
  447. package/src/modules/permissions/db/user-dms.ts +0 -58
  448. package/src/modules/permissions/db/user-roles.ts +0 -85
  449. package/src/modules/permissions/db/users.ts +0 -38
  450. package/src/modules/permissions/index.ts +0 -421
  451. package/src/modules/permissions/permissions.test.ts +0 -358
  452. package/src/modules/permissions/sender-approval.test.ts +0 -470
  453. package/src/modules/permissions/sender-approval.ts +0 -165
  454. package/src/modules/permissions/user-dm.ts +0 -200
  455. package/src/modules/provider-credentials/db.ts +0 -121
  456. package/src/modules/provider-credentials/index.ts +0 -12
  457. package/src/modules/provider-credentials/spawn.test.ts +0 -206
  458. package/src/modules/provider-credentials/spawn.ts +0 -114
  459. package/src/modules/scheduling/actions.ts +0 -113
  460. package/src/modules/scheduling/db.test.ts +0 -282
  461. package/src/modules/scheduling/db.ts +0 -148
  462. package/src/modules/scheduling/index.ts +0 -34
  463. package/src/modules/scheduling/recurrence.test.ts +0 -98
  464. package/src/modules/scheduling/recurrence.ts +0 -54
  465. package/src/modules/self-mod/agent.md +0 -30
  466. package/src/modules/self-mod/apply.ts +0 -85
  467. package/src/modules/self-mod/index.ts +0 -30
  468. package/src/modules/self-mod/project.md +0 -39
  469. package/src/modules/self-mod/request.ts +0 -91
  470. package/src/modules/typing/index.ts +0 -165
  471. package/src/oauth/agent-app-connections.ts +0 -103
  472. package/src/oauth/app-configs.test.ts +0 -64
  473. package/src/oauth/app-configs.ts +0 -114
  474. package/src/oauth/app-connections.test.ts +0 -109
  475. package/src/oauth/app-connections.ts +0 -178
  476. package/src/oauth/crypto.ts +0 -56
  477. package/src/oauth/flow.ts +0 -104
  478. package/src/oauth/providers/google.test.ts +0 -38
  479. package/src/oauth/providers/google.ts +0 -46
  480. package/src/oauth/providers/index.ts +0 -48
  481. package/src/oauth/state-store.test.ts +0 -54
  482. package/src/oauth/state-store.ts +0 -93
  483. package/src/parachute/README.md +0 -27
  484. package/src/parachute/create-agent.test.ts +0 -83
  485. package/src/parachute/create-agent.ts +0 -122
  486. package/src/parachute/group-status.test.ts +0 -165
  487. package/src/parachute/group-status.ts +0 -136
  488. package/src/parachute/types.ts +0 -41
  489. package/src/parachute/vault-mcp.test.ts +0 -251
  490. package/src/parachute/vault-mcp.ts +0 -232
  491. package/src/platform-id.test.ts +0 -104
  492. package/src/platform-id.ts +0 -109
  493. package/src/providers/index.ts +0 -6
  494. package/src/providers/provider-container-registry.ts +0 -58
  495. package/src/response-registry.ts +0 -45
  496. package/src/router.ts +0 -530
  497. package/src/secrets/crypto.test.ts +0 -45
  498. package/src/secrets/crypto.ts +0 -55
  499. package/src/secrets/index.ts +0 -355
  500. package/src/secrets/master-key.ts +0 -70
  501. package/src/secrets/secrets.test.ts +0 -354
  502. package/src/session-manager.migrate.test.ts +0 -59
  503. package/src/session-manager.ts +0 -433
  504. package/src/startup-bootstrap.test.ts +0 -226
  505. package/src/startup-bootstrap.ts +0 -207
  506. package/src/state-sqlite.ts +0 -182
  507. package/src/timezone.test.ts +0 -64
  508. package/src/timezone.ts +0 -37
  509. package/src/types.ts +0 -230
  510. package/src/web/auth.test.ts +0 -335
  511. package/src/web/auth.ts +0 -214
  512. package/src/web/discord-validate.test.ts +0 -77
  513. package/src/web/discord-validate.ts +0 -88
  514. package/src/web/hub-discovery.test.ts +0 -98
  515. package/src/web/hub-discovery.ts +0 -69
  516. package/src/web/routes/activity.ts +0 -106
  517. package/src/web/routes/agent-provider.test.ts +0 -282
  518. package/src/web/routes/agent-provider.ts +0 -309
  519. package/src/web/routes/approvals.ts +0 -185
  520. package/src/web/routes/apps.ts +0 -434
  521. package/src/web/routes/channels-mg-detail.test.ts +0 -324
  522. package/src/web/routes/channels-mga-detail.test.ts +0 -425
  523. package/src/web/routes/channels.ts +0 -489
  524. package/src/web/routes/oauth-providers.ts +0 -42
  525. package/src/web/routes/secrets.test.ts +0 -175
  526. package/src/web/routes/secrets.ts +0 -282
  527. package/src/web/routes/sessions.ts +0 -123
  528. package/src/web/routes/settings.test.ts +0 -106
  529. package/src/web/routes/settings.ts +0 -247
  530. package/src/web/routes/setup-status.ts +0 -205
  531. package/src/web/routes/vaults.test.ts +0 -389
  532. package/src/web/routes/vaults.ts +0 -225
  533. package/src/web/server-version.test.ts +0 -16
  534. package/src/web/server.ts +0 -1003
  535. package/src/web/services-manifest.test.ts +0 -120
  536. package/src/web/services-manifest.ts +0 -61
  537. package/src/web/static-serve.test.ts +0 -255
  538. package/src/web/static-serve.ts +0 -104
  539. package/src/web/telegram-validate.test.ts +0 -116
  540. package/src/web/telegram-validate.ts +0 -107
  541. package/src/web/vault-proxy.test.ts +0 -214
  542. package/src/web/vault-proxy.ts +0 -120
  543. package/src/web/wire-channel.ts +0 -181
  544. package/src/webhook-server.ts +0 -134
  545. package/vitest.config.ts +0 -18
  546. package/web/README.md +0 -63
  547. package/web/ui/index.html +0 -13
  548. package/web/ui/package.json +0 -35
  549. package/web/ui/pnpm-lock.yaml +0 -2164
  550. package/web/ui/scripts/verify-base.mjs +0 -31
  551. package/web/ui/src/App.tsx +0 -88
  552. package/web/ui/src/components/ActivityFeed.tsx +0 -444
  553. package/web/ui/src/components/AgentGroupPicker.tsx +0 -263
  554. package/web/ui/src/components/AgentProviderCards.tsx +0 -220
  555. package/web/ui/src/components/CredentialForm.tsx +0 -214
  556. package/web/ui/src/components/ScopeGrants.tsx +0 -74
  557. package/web/ui/src/components/StatusDot.tsx +0 -43
  558. package/web/ui/src/components/VaultPicker.tsx +0 -127
  559. package/web/ui/src/components/setup/AdapterInstallStep.tsx +0 -178
  560. package/web/ui/src/components/setup/AgentGroupStep.tsx +0 -43
  561. package/web/ui/src/components/setup/ChannelPickStep.tsx +0 -74
  562. package/web/ui/src/components/setup/DoneStep.tsx +0 -49
  563. package/web/ui/src/components/setup/PrereqStep.tsx +0 -129
  564. package/web/ui/src/components/setup/TestConnectionStep.tsx +0 -108
  565. package/web/ui/src/components/setup/TestMessageStep.tsx +0 -104
  566. package/web/ui/src/components/setup/WireChannelStep.tsx +0 -166
  567. package/web/ui/src/components/setup/types.ts +0 -105
  568. package/web/ui/src/lib/api.test.ts +0 -410
  569. package/web/ui/src/lib/api.ts +0 -1210
  570. package/web/ui/src/lib/auth.test.ts +0 -139
  571. package/web/ui/src/lib/auth.ts +0 -348
  572. package/web/ui/src/lib/channel-adapters.ts +0 -136
  573. package/web/ui/src/main.tsx +0 -19
  574. package/web/ui/src/routes/ApprovalsList.tsx +0 -294
  575. package/web/ui/src/routes/Apps.tsx +0 -613
  576. package/web/ui/src/routes/ChannelWireDetail.test.tsx +0 -233
  577. package/web/ui/src/routes/ChannelWireDetail.tsx +0 -403
  578. package/web/ui/src/routes/ChannelsList.tsx +0 -158
  579. package/web/ui/src/routes/GroupDetail.tsx +0 -755
  580. package/web/ui/src/routes/GroupList.tsx +0 -187
  581. package/web/ui/src/routes/MessagingGroupDetail.test.tsx +0 -233
  582. package/web/ui/src/routes/MessagingGroupDetail.tsx +0 -306
  583. package/web/ui/src/routes/NewGroupWizard.tsx +0 -390
  584. package/web/ui/src/routes/OAuthCallback.tsx +0 -56
  585. package/web/ui/src/routes/SecretsList.tsx +0 -921
  586. package/web/ui/src/routes/SessionsList.tsx +0 -220
  587. package/web/ui/src/routes/SettingsAgentProvider.tsx +0 -109
  588. package/web/ui/src/routes/SettingsApprovals.tsx +0 -234
  589. package/web/ui/src/routes/SetupWizard.tsx +0 -219
  590. package/web/ui/src/routes/VaultDetail.test.tsx +0 -361
  591. package/web/ui/src/routes/VaultDetail.tsx +0 -960
  592. package/web/ui/src/routes/VaultsList.tsx +0 -295
  593. package/web/ui/src/routes/WireChannelPage.tsx +0 -413
  594. package/web/ui/src/styles.css +0 -608
  595. package/web/ui/src/test/setup.ts +0 -23
  596. package/web/ui/src/vite-env.d.ts +0 -10
  597. package/web/ui/vite.config.ts +0 -34
  598. 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
+ }