@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,455 @@
1
+ /**
2
+ * Tier 1 unit + integration tests for the http-ui transport.
3
+ *
4
+ * These exercise the transport (and a daemon-shaped Bun.serve harness) WITHOUT a
5
+ * live Claude session. They cover:
6
+ * - inbound routing: a UI `send` reaches the bridge subscribed to that channel;
7
+ * - outbound to UI: `transport.reply()` pushes a `reply` event to a connected
8
+ * /ui/events SSE client;
9
+ * - round-trip through the daemon HTTP server (UI send → bridge, bridge reply
10
+ * → UI) with no Claude;
11
+ * - channel isolation (a send on A never reaches a UI client on B);
12
+ * - registry: an http-ui channel instantiates without a token;
13
+ * - reply() with no connected UI client does not throw.
14
+ */
15
+
16
+ import { describe, test, expect, mock } from "bun:test";
17
+
18
+ // Layer 2 gates the http-ui send + SSE routes on `requireScope`, which validates
19
+ // a hub JWT against the hub's JWKS. The no-token path short-circuits to 401
20
+ // before any JWKS fetch (asserted below). To exercise the *delivery* paths
21
+ // (routing, SSE fan-out) without a live hub, stub the JWT validator so a single
22
+ // sentinel token validates with the agent scopes. A request with no token (or
23
+ // any other token) still hits the real no-token / shape-first reject. This keeps
24
+ // the round-trip coverage genuine while staying hub-free.
25
+ const VALID_TOKEN = "test-valid-token";
26
+ // A token carrying ONLY agent:write (a session/bridge token) — must be
27
+ // REJECTED on the UI send endpoint, which requires agent:send. Locks the
28
+ // privilege separation (a session token can't post as a human).
29
+ const WRITE_ONLY_TOKEN = "test-write-only-token";
30
+ mock.module("../hub-jwt.ts", () => ({
31
+ // New tokens carry aud "agent" (channel→agent rename); CHANNEL_AUDIENCE stays a
32
+ // deprecated alias. ACCEPTED_AUDIENCES is the dual-accept set the real adapter
33
+ // checks — included so the mock matches the renamed module surface.
34
+ AGENT_AUDIENCE: "agent",
35
+ CHANNEL_AUDIENCE: "channel",
36
+ ACCEPTED_AUDIENCES: ["agent", "channel"],
37
+ async validateHubJwt(token: string) {
38
+ if (token === VALID_TOKEN) {
39
+ return {
40
+ sub: "test",
41
+ scopes: ["agent:read", "agent:send", "agent:write"],
42
+ aud: "agent",
43
+ jti: undefined,
44
+ clientId: undefined,
45
+ vaultScope: undefined,
46
+ };
47
+ }
48
+ if (token === WRITE_ONLY_TOKEN) {
49
+ return {
50
+ sub: "test",
51
+ scopes: ["agent:write"],
52
+ aud: "agent",
53
+ jti: undefined,
54
+ clientId: undefined,
55
+ vaultScope: undefined,
56
+ };
57
+ }
58
+ throw new HubJwtError("invalid token");
59
+ },
60
+ HubJwtError: class HubJwtError extends Error {},
61
+ looksLikeJwt: (t: string) => t.split(".").length === 3,
62
+ resetJwksCache() {},
63
+ resetRevocationCache() {},
64
+ }));
65
+ class HubJwtError extends Error {}
66
+
67
+ import { HttpUiTransport } from "./http-ui.ts";
68
+ import type { TransportContext, InboundMessage } from "../transport.ts";
69
+ import { ClientRegistry } from "../routing.ts";
70
+ import { instantiateTransport } from "../registry.ts";
71
+
72
+ /** Authorization header carrying the sentinel valid token. */
73
+ const AUTH = { authorization: "Bearer " + VALID_TOKEN } as const;
74
+ /** Append the sentinel token as a `?token=` query param (the SSE auth path). */
75
+ function withToken(path: string): string {
76
+ return path + (path.includes("?") ? "&" : "?") + "token=" + encodeURIComponent(VALID_TOKEN);
77
+ }
78
+
79
+ /** A test context that records emitted inbound messages + permission verdicts. */
80
+ function fakeCtx(channel: string): TransportContext & {
81
+ emitted: InboundMessage[];
82
+ verdicts: { request_id: string; behavior: string }[];
83
+ } {
84
+ const emitted: InboundMessage[] = [];
85
+ const verdicts: { request_id: string; behavior: string }[] = [];
86
+ return {
87
+ channel,
88
+ emitted,
89
+ verdicts,
90
+ emit(msg) {
91
+ emitted.push(msg);
92
+ },
93
+ emitPermissionVerdict(v) {
94
+ verdicts.push(v);
95
+ },
96
+ };
97
+ }
98
+
99
+ /**
100
+ * Read the next non-comment SSE frame from a reader. Handles both the bytes a
101
+ * `fetch` response body yields and the raw string chunks a directly-read
102
+ * in-process ReadableStream<string> yields (the transport enqueues strings).
103
+ */
104
+ async function readFrame(
105
+ reader: ReadableStreamDefaultReader<Uint8Array | string>,
106
+ decoder = new TextDecoder(),
107
+ ): Promise<string> {
108
+ while (true) {
109
+ const { value, done } = await reader.read();
110
+ if (done) return "";
111
+ const chunk = typeof value === "string" ? value : decoder.decode(value, { stream: true });
112
+ if (chunk.includes("event:")) return chunk;
113
+ }
114
+ }
115
+
116
+ describe("HttpUiTransport — direct", () => {
117
+ test("ingestHttp send (authed) → ctx.emit on its own channel", async () => {
118
+ const t = new HttpUiTransport();
119
+ const ctx = fakeCtx("dev");
120
+ await t.start(ctx);
121
+
122
+ const req = new Request("http://x/api/channels/dev/send", {
123
+ method: "POST",
124
+ headers: { "content-type": "application/json", ...AUTH },
125
+ body: JSON.stringify({ text: "hello session" }),
126
+ });
127
+ const res = await t.ingestHttp(req, new URL(req.url));
128
+ expect(res).not.toBeNull();
129
+ expect(res!.status).toBe(200);
130
+ expect(await res!.json()).toEqual({ ok: true });
131
+
132
+ expect(ctx.emitted).toHaveLength(1);
133
+ expect(ctx.emitted[0]!.content).toBe("hello session");
134
+ expect(ctx.emitted[0]!.channel).toBe("dev");
135
+ expect(ctx.emitted[0]!.source).toBe("http-ui");
136
+ });
137
+
138
+ test("ingestHttp send WITHOUT a token → 401, no emit (Layer 2)", async () => {
139
+ const t = new HttpUiTransport();
140
+ const ctx = fakeCtx("dev");
141
+ await t.start(ctx);
142
+ const req = new Request("http://x/api/channels/dev/send", {
143
+ method: "POST",
144
+ headers: { "content-type": "application/json" },
145
+ body: JSON.stringify({ text: "no token" }),
146
+ });
147
+ const res = await t.ingestHttp(req, new URL(req.url));
148
+ expect(res).not.toBeNull();
149
+ expect(res!.status).toBe(401);
150
+ expect(ctx.emitted).toHaveLength(0);
151
+ });
152
+
153
+ test("ingestHttp send with an agent:write-only (session) token → 403, no emit", async () => {
154
+ // Privilege separation: a session/bridge token (agent:write) must NOT be
155
+ // usable to post a human message through the UI send endpoint (agent:send).
156
+ const t = new HttpUiTransport();
157
+ const ctx = fakeCtx("dev");
158
+ await t.start(ctx);
159
+ const req = new Request("http://x/api/channels/dev/send", {
160
+ method: "POST",
161
+ headers: { "content-type": "application/json", authorization: "Bearer " + WRITE_ONLY_TOKEN },
162
+ body: JSON.stringify({ text: "trying to send as a session" }),
163
+ });
164
+ const res = await t.ingestHttp(req, new URL(req.url));
165
+ expect(res).not.toBeNull();
166
+ expect(res!.status).toBe(403);
167
+ expect(ctx.emitted).toHaveLength(0);
168
+ });
169
+
170
+ test("ingestHttp SSE WITHOUT a ?token= → 401 (Layer 2)", async () => {
171
+ const t = new HttpUiTransport();
172
+ await t.start(fakeCtx("dev"));
173
+ const req = new Request("http://x/ui/events?channel=dev");
174
+ const res = await t.ingestHttp(req, new URL(req.url));
175
+ expect(res).not.toBeNull();
176
+ expect(res!.status).toBe(401);
177
+ expect(res!.headers.get("content-type")).toContain("application/json");
178
+ });
179
+
180
+ test("ingestHttp ignores a send for a DIFFERENT channel's path", async () => {
181
+ const t = new HttpUiTransport();
182
+ await t.start(fakeCtx("dev"));
183
+ const req = new Request("http://x/api/channels/other/send", {
184
+ method: "POST",
185
+ body: JSON.stringify({ text: "nope" }),
186
+ });
187
+ const res = await t.ingestHttp(req, new URL(req.url));
188
+ expect(res).toBeNull();
189
+ });
190
+
191
+ test("send (authed) with empty/missing text → 400, no emit", async () => {
192
+ const t = new HttpUiTransport();
193
+ const ctx = fakeCtx("dev");
194
+ await t.start(ctx);
195
+ const req = new Request("http://x/api/channels/dev/send", {
196
+ method: "POST",
197
+ headers: { ...AUTH },
198
+ body: JSON.stringify({ text: "" }),
199
+ });
200
+ const res = await t.ingestHttp(req, new URL(req.url));
201
+ expect(res!.status).toBe(400);
202
+ expect(ctx.emitted).toHaveLength(0);
203
+ });
204
+
205
+ test("reply() with no connected UI client does not throw and returns sent:[]", async () => {
206
+ const t = new HttpUiTransport();
207
+ await t.start(fakeCtx("dev"));
208
+ const result = await t.reply({ channel: "dev", text: "ping" });
209
+ expect(result.sent).toEqual([]);
210
+ });
211
+
212
+ test("reply() pushes a `reply` event to a connected /ui/events SSE client", async () => {
213
+ const t = new HttpUiTransport();
214
+ await t.start(fakeCtx("dev"));
215
+
216
+ // Open the UI SSE stream via ingestHttp (authed via ?token=).
217
+ const sseReq = new Request("http://x" + withToken("/ui/events?channel=dev"));
218
+ const sseRes = await t.ingestHttp(sseReq, new URL(sseReq.url));
219
+ expect(sseRes).not.toBeNull();
220
+ const reader = sseRes!.body!.getReader();
221
+
222
+ // Drain the ": connected" comment, then reply.
223
+ const result = await t.reply({ channel: "dev", text: "from session", files: ["/tmp/a.png"] });
224
+ expect(result.sent).toHaveLength(1);
225
+
226
+ const frame = await readFrame(reader);
227
+ expect(frame).toContain("event: reply");
228
+ expect(frame).toContain("from session");
229
+ expect(frame).toContain("/tmp/a.png");
230
+ reader.cancel().catch(() => {});
231
+ });
232
+
233
+ test("stop() clears UI clients", async () => {
234
+ const t = new HttpUiTransport();
235
+ await t.start(fakeCtx("dev"));
236
+ const sseReq = new Request("http://x" + withToken("/ui/events?channel=dev"));
237
+ const sseRes = await t.ingestHttp(sseReq, new URL(sseReq.url));
238
+ sseRes!.body!.getReader();
239
+ await t.stop();
240
+ // After stop, a reply reaches nobody.
241
+ const result = await t.reply({ channel: "dev", text: "x" });
242
+ expect(result.sent).toEqual([]);
243
+ });
244
+ });
245
+
246
+ describe("registry — http-ui", () => {
247
+ test("an http-ui channel instantiates without a token", () => {
248
+ const transport = instantiateTransport({ name: "dev", transport: "http-ui" });
249
+ expect(transport.kind).toBe("http-ui");
250
+ });
251
+ });
252
+
253
+ // ---------------------------------------------------------------------------
254
+ // Daemon-shaped integration: a Bun.serve harness wiring routing + ingestHttp,
255
+ // mirroring daemon.ts. No Claude.
256
+ // ---------------------------------------------------------------------------
257
+
258
+ describe("HttpUiTransport — through a daemon-shaped server", () => {
259
+ /** Build a minimal daemon-shaped server over the given channels. */
260
+ function buildServer(channelDefs: { name: string }[]) {
261
+ const registry = new ClientRegistry();
262
+ const channels = new Map<string, { name: string; transport: HttpUiTransport }>();
263
+ for (const def of channelDefs) {
264
+ const transport = new HttpUiTransport();
265
+ channels.set(def.name, { name: def.name, transport });
266
+ }
267
+
268
+ // Start each transport with a ctx that routes into the bridge registry,
269
+ // exactly like daemon.ts's contextFor.
270
+ for (const ch of channels.values()) {
271
+ const name = ch.name;
272
+ const ctx: TransportContext = {
273
+ channel: name,
274
+ emit(msg) {
275
+ registry.routeToChannel(name, "message", {
276
+ content: msg.content,
277
+ meta: msg.meta,
278
+ source: msg.source,
279
+ });
280
+ },
281
+ emitPermissionVerdict(v) {
282
+ registry.routeToChannel(name, "permission_verdict", v);
283
+ },
284
+ };
285
+ ch.transport.start(ctx);
286
+ }
287
+
288
+ const server = Bun.serve({
289
+ port: 0,
290
+ hostname: "127.0.0.1",
291
+ idleTimeout: 0,
292
+ async fetch(req) {
293
+ const url = new URL(req.url);
294
+
295
+ // Bridge SSE subscription (mirrors daemon /events).
296
+ if (req.method === "GET" && url.pathname === "/events") {
297
+ const channel = url.searchParams.get("channel") ?? "default";
298
+ const id = crypto.randomUUID();
299
+ const stream = new ReadableStream<string>({
300
+ start(controller) {
301
+ registry.add(id, { channel, enqueue: (p) => controller.enqueue(p) });
302
+ controller.enqueue(": connected\n\n");
303
+ },
304
+ cancel() {
305
+ registry.remove(id);
306
+ },
307
+ });
308
+ return new Response(stream, { headers: { "content-type": "text/event-stream" } });
309
+ }
310
+
311
+ // Bridge reply (mirrors daemon /api/reply dispatch).
312
+ if (req.method === "POST" && url.pathname === "/api/reply") {
313
+ const body = (await req.json()) as { channel: string; text?: string };
314
+ const ch = channels.get(body.channel);
315
+ if (!ch) return new Response(JSON.stringify({ error: "unknown channel" }), { status: 400 });
316
+ const r = await ch.transport.reply({ channel: body.channel, text: body.text });
317
+ return new Response(JSON.stringify({ sent: r.sent }), {
318
+ headers: { "content-type": "application/json" },
319
+ });
320
+ }
321
+
322
+ // Transport-owned routes (send + /ui/events).
323
+ for (const ch of channels.values()) {
324
+ const res = await ch.transport.ingestHttp(req, url);
325
+ if (res) return res;
326
+ }
327
+ return new Response(JSON.stringify({ error: "not found" }), { status: 404 });
328
+ },
329
+ });
330
+
331
+ return { server, base: `http://127.0.0.1:${server.port}`, registry };
332
+ }
333
+
334
+ /** Open an SSE stream and return a reader + helpers. The http-ui `/ui/events`
335
+ * route is Layer-2-gated, so append the sentinel `?token=` for those; the
336
+ * bridge `/events` route in this harness is ungated (pass through as-is). */
337
+ async function openSse(base: string, path: string) {
338
+ const url = path.startsWith("/ui/events") ? withToken(path) : path;
339
+ const res = await fetch(`${base}${url}`);
340
+ const reader = res.body!.getReader();
341
+ return {
342
+ read: () => readFrame(reader),
343
+ cancel: () => reader.cancel().catch(() => {}),
344
+ };
345
+ }
346
+
347
+ test("inbound routing: UI send reaches the subscribed bridge", async () => {
348
+ const { server, base, registry } = buildServer([{ name: "dev" }]);
349
+ try {
350
+ // A bridge subscribes to channel "dev".
351
+ const bridge = await openSse(base, "/events?channel=dev");
352
+ // Wait for registration.
353
+ const start = Date.now();
354
+ while (registry.size < 1 && Date.now() - start < 1000) {
355
+ await new Promise((r) => setTimeout(r, 5));
356
+ }
357
+ expect(registry.size).toBe(1);
358
+
359
+ // UI POSTs a send (authed — Layer 2).
360
+ const res = await fetch(`${base}/api/channels/dev/send`, {
361
+ method: "POST",
362
+ headers: { "content-type": "application/json", ...AUTH },
363
+ body: JSON.stringify({ text: "hi from UI" }),
364
+ });
365
+ expect(res.status).toBe(200);
366
+ expect(await res.json()).toEqual({ ok: true });
367
+
368
+ const frame = await bridge.read();
369
+ expect(frame).toContain("event: message");
370
+ expect(frame).toContain("hi from UI");
371
+ bridge.cancel();
372
+ } finally {
373
+ server.stop(true);
374
+ }
375
+ });
376
+
377
+ test("round-trip: UI send → bridge AND bridge /api/reply → UI SSE, end to end", async () => {
378
+ const { server, base, registry } = buildServer([{ name: "dev" }]);
379
+ try {
380
+ const bridge = await openSse(base, "/events?channel=dev");
381
+ const ui = await openSse(base, "/ui/events?channel=dev");
382
+ const start = Date.now();
383
+ while (registry.size < 1 && Date.now() - start < 1000) {
384
+ await new Promise((r) => setTimeout(r, 5));
385
+ }
386
+
387
+ // UI → bridge (authed — Layer 2).
388
+ await fetch(`${base}/api/channels/dev/send`, {
389
+ method: "POST",
390
+ headers: { "content-type": "application/json", ...AUTH },
391
+ body: JSON.stringify({ text: "wake up" }),
392
+ });
393
+ const bridgeFrame = await bridge.read();
394
+ expect(bridgeFrame).toContain("wake up");
395
+
396
+ // bridge → UI (the session replied via the reply tool).
397
+ const replyRes = await fetch(`${base}/api/reply`, {
398
+ method: "POST",
399
+ headers: { "content-type": "application/json" },
400
+ body: JSON.stringify({ channel: "dev", text: "I am awake" }),
401
+ });
402
+ expect((await replyRes.json()).sent).toHaveLength(1);
403
+
404
+ const uiFrame = await ui.read();
405
+ expect(uiFrame).toContain("event: reply");
406
+ expect(uiFrame).toContain("I am awake");
407
+
408
+ bridge.cancel();
409
+ ui.cancel();
410
+ } finally {
411
+ server.stop(true);
412
+ }
413
+ });
414
+
415
+ test("channel isolation: a send on A reaches A's bridge but NOT a UI client on B", async () => {
416
+ const { server, base } = buildServer([{ name: "A" }, { name: "B" }]);
417
+ try {
418
+ const bridgeA = await openSse(base, "/events?channel=A");
419
+ const uiB = await openSse(base, "/ui/events?channel=B");
420
+
421
+ // Send on channel A, then reply on A (authed — Layer 2).
422
+ await fetch(`${base}/api/channels/A/send`, {
423
+ method: "POST",
424
+ headers: { "content-type": "application/json", ...AUTH },
425
+ body: JSON.stringify({ text: "for-A" }),
426
+ });
427
+ // Close the loop: A's bridge MUST receive the inbound (not just "B didn't").
428
+ const bridgeFrame = await bridgeA.read();
429
+ expect(bridgeFrame).toContain("for-A");
430
+
431
+ await fetch(`${base}/api/reply`, {
432
+ method: "POST",
433
+ headers: { "content-type": "application/json" },
434
+ body: JSON.stringify({ channel: "A", text: "reply-to-A" }),
435
+ });
436
+
437
+ // Now reply on B so B's stream definitely has a frame, and assert it's B's
438
+ // only — none of A's traffic leaked across.
439
+ await fetch(`${base}/api/reply`, {
440
+ method: "POST",
441
+ headers: { "content-type": "application/json" },
442
+ body: JSON.stringify({ channel: "B", text: "reply-to-B" }),
443
+ });
444
+
445
+ const frame = await uiB.read();
446
+ expect(frame).toContain("reply-to-B");
447
+ expect(frame).not.toContain("reply-to-A");
448
+ expect(frame).not.toContain("for-A");
449
+ bridgeA.cancel();
450
+ uiB.cancel();
451
+ } finally {
452
+ server.stop(true);
453
+ }
454
+ });
455
+ });
@@ -0,0 +1,201 @@
1
+ /**
2
+ * http-ui transport for parachute-agent.
3
+ *
4
+ * The freestanding "make sure message sending works" surface: a human talks to
5
+ * a Claude Code session through a browser, with NO Telegram and NO vault.
6
+ *
7
+ * How it differs from telegram — the "external party" is a browser:
8
+ * - Inbound (human → session): the UI POSTs to
9
+ * `/api/channels/<name>/send {text}`; this transport calls `ctx.emit(...)`,
10
+ * which routes to the bridge subscribed to that channel and wakes the
11
+ * session.
12
+ * - Outbound (session → human): when the session calls the `reply` tool, the
13
+ * bridge POSTs `/api/reply {channel,...}`; the daemon dispatches to this
14
+ * transport's `reply()`. Since the browser can't be POSTed to, this transport
15
+ * holds its own set of **UI SSE clients** per channel and `reply()` enqueues
16
+ * to them (mirroring the daemon's `/events` SSE pattern for bridges).
17
+ *
18
+ * It owns two HTTP surfaces via `ingestHttp` (scoped to ITS OWN channel name):
19
+ * 1. POST /api/channels/<name>/send — body {text} → ctx.emit(...) → {ok:true}
20
+ * 2. GET /ui/events?channel=<name> — SSE stream the browser subscribes to
21
+ * The static `/ui` chat page itself is global and served by the daemon, since
22
+ * it's a channel picker across all http-ui channels.
23
+ */
24
+
25
+ import type {
26
+ Transport,
27
+ TransportContext,
28
+ ReplyArgs,
29
+ EditArgs,
30
+ PermissionArgs,
31
+ } from "../transport.ts";
32
+ import { sseFrame } from "../routing.ts";
33
+ import { requireScope, json, SCOPE_SEND, SCOPE_READ } from "../auth.ts";
34
+
35
+ /** A connected browser SSE client (one per open chat page on this channel). */
36
+ interface UiClient {
37
+ enqueue(payload: string): void;
38
+ }
39
+
40
+ /** Config for an http-ui transport instance (no secret needed — just a name). */
41
+ export interface HttpUiTransportConfig {
42
+ /** Optional override for the channel name; normally taken from ctx.channel. */
43
+ channel?: string;
44
+ }
45
+
46
+ export class HttpUiTransport implements Transport {
47
+ readonly kind = "http-ui";
48
+
49
+ /** Captured in start() — the channel this instance is bound to. */
50
+ private ctx: TransportContext | undefined;
51
+ /** Connected browser SSE clients for this channel, keyed by client id. */
52
+ private uiClients = new Map<string, UiClient>();
53
+
54
+ constructor(_config: HttpUiTransportConfig = {}) {
55
+ // http-ui needs no secret. Config is accepted for forward-compat.
56
+ }
57
+
58
+ async start(ctx: TransportContext): Promise<void> {
59
+ this.ctx = ctx;
60
+ }
61
+
62
+ async stop(): Promise<void> {
63
+ // Close all browser SSE streams. Enqueue can throw on an already-closed
64
+ // stream; swallow per-client so one bad client doesn't block the rest.
65
+ for (const client of this.uiClients.values()) {
66
+ try {
67
+ client.enqueue(sseFrame("close", {}));
68
+ } catch {}
69
+ }
70
+ this.uiClients.clear();
71
+ }
72
+
73
+ /** The channel name this transport is bound to (after start). */
74
+ private get channel(): string {
75
+ if (!this.ctx) throw new Error("http-ui transport: not started");
76
+ return this.ctx.channel;
77
+ }
78
+
79
+ /** Push an SSE frame to every connected browser client. Returns delivery count. */
80
+ private pushToUi(event: string, data: unknown): number {
81
+ const payload = sseFrame(event, data);
82
+ let delivered = 0;
83
+ for (const [id, client] of this.uiClients) {
84
+ try {
85
+ client.enqueue(payload);
86
+ delivered++;
87
+ } catch {
88
+ this.uiClients.delete(id);
89
+ }
90
+ }
91
+ return delivered;
92
+ }
93
+
94
+ // -------------------------------------------------------------------------
95
+ // Outbound — the session → browser direction
96
+ // -------------------------------------------------------------------------
97
+
98
+ async reply(args: ReplyArgs): Promise<{ sent: string[] }> {
99
+ // The browser can't be POSTed to, so we push the reply over the UI SSE
100
+ // stream(s). A message with no connected UI client still succeeds — it just
101
+ // has no listener (return an empty sent list).
102
+ const id = crypto.randomUUID();
103
+ const delivered = this.pushToUi("reply", {
104
+ id,
105
+ text: args.text ?? "",
106
+ files: args.files ?? [],
107
+ });
108
+ return { sent: delivered > 0 ? [id] : [] };
109
+ }
110
+
111
+ async edit(args: EditArgs): Promise<void> {
112
+ this.pushToUi("edit", { id: args.message_id, text: args.text });
113
+ }
114
+
115
+ async sendPermission(args: PermissionArgs): Promise<{ sent: string[] }> {
116
+ // Surface the permission prompt in the chat so the human sees it. There's no
117
+ // verdict affordance wired in the minimal UI yet (Stage 1), but the prompt
118
+ // is visible — better than the daemon's methodMissing 400.
119
+ const delivered = this.pushToUi("permission", {
120
+ request_id: args.request_id,
121
+ tool_name: args.tool_name,
122
+ description: args.description,
123
+ input_preview: args.input_preview,
124
+ });
125
+ return { sent: delivered > 0 ? [args.request_id] : [] };
126
+ }
127
+
128
+ // -------------------------------------------------------------------------
129
+ // HTTP routes this transport owns (only for ITS OWN channel name)
130
+ // -------------------------------------------------------------------------
131
+
132
+ async ingestHttp(req: Request, url: URL): Promise<Response | null> {
133
+ const channel = this.channel; // getter throws a clear error if not started
134
+ const ctx = this.ctx!; // safe: the getter above guarantees ctx is set
135
+
136
+ // 1. Inbound: POST /api/channels/<channel>/send body {text}
137
+ if (
138
+ req.method === "POST" &&
139
+ url.pathname === `/api/channels/${channel}/send`
140
+ ) {
141
+ // Layer 2 (human→UI): a hub-issued token with `agent:send`. The UI
142
+ // fetches it from the hub's /admin/agent-token (portal-cookie-gated) and
143
+ // attaches it as a Bearer header. No-token → 401 (short-circuits pre-JWKS).
144
+ const denied = await requireScope(req, url, SCOPE_SEND);
145
+ if (denied) return denied;
146
+ let text: string;
147
+ try {
148
+ const body = (await req.json()) as { text?: unknown };
149
+ if (typeof body.text !== "string" || body.text.length === 0) {
150
+ return json({ error: "body must be { text: <non-empty string> }" }, 400);
151
+ }
152
+ text = body.text;
153
+ } catch {
154
+ return json({ error: "invalid JSON body" }, 400);
155
+ }
156
+ ctx.emit({
157
+ channel,
158
+ content: text,
159
+ meta: { source: "http-ui", ts: new Date().toISOString() },
160
+ source: "http-ui",
161
+ });
162
+ return json({ ok: true });
163
+ }
164
+
165
+ // 2. Outbound stream: GET /ui/events?channel=<channel>
166
+ if (
167
+ req.method === "GET" &&
168
+ url.pathname === "/ui/events" &&
169
+ url.searchParams.get("channel") === channel
170
+ ) {
171
+ // Layer 2: gate on `agent:read`. EventSource can't set headers, so the
172
+ // token rides in as a `?token=` query param — the ONLY endpoint that opts
173
+ // into the query-param fallback (allowQueryParam: true). No-token → 401
174
+ // before the stream opens.
175
+ const denied = await requireScope(req, url, SCOPE_READ, true);
176
+ if (denied) return denied;
177
+ const clientId = crypto.randomUUID();
178
+ const clients = this.uiClients;
179
+ const stream = new ReadableStream<string>({
180
+ start(controller) {
181
+ clients.set(clientId, {
182
+ enqueue: (payload) => controller.enqueue(payload),
183
+ });
184
+ controller.enqueue(": connected\n\n");
185
+ },
186
+ cancel() {
187
+ clients.delete(clientId);
188
+ },
189
+ });
190
+ return new Response(stream, {
191
+ headers: {
192
+ "content-type": "text/event-stream",
193
+ "cache-control": "no-cache",
194
+ connection: "keep-alive",
195
+ },
196
+ });
197
+ }
198
+
199
+ return null;
200
+ }
201
+ }