@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
package/src/jobs.ts ADDED
@@ -0,0 +1,266 @@
1
+ /**
2
+ * Scheduled-job model + VAULT-NATIVE store for the runner (design
3
+ * `2026-06-17-runner-scheduled-agent-turns.md`).
4
+ *
5
+ * A scheduled job is "an automated human": send message M to agent (channel) A on
6
+ * schedule S. The runner does NOT execute anything — it authors an inbound
7
+ * `#agent/message/inbound` note on a schedule, and the existing vault trigger →
8
+ * agent-turn → outbound flow does the rest.
9
+ *
10
+ * STORAGE IS VAULT-NATIVE (Aaron's call, 2026-06-17): a job IS a `#agent/job`
11
+ * note in the TARGET channel's vault — durable, queryable, and renderable by any
12
+ * surface, converging with the blueprint's "vault as the spine" and the future
13
+ * `tag:job` idea. There is NO jobs.json. The vault note I/O lives on
14
+ * `VaultTransport` (it owns the vault URL + token + encoding); this module is the
15
+ * thin, storage-agnostic FACADE that keeps the same read-all / upsert / remove
16
+ * interface the runner + API call. Token handling is never duplicated here.
17
+ *
18
+ * `metadata` on the note is all string-typed (the vault stores metadata as
19
+ * strings): `{ channel, cron, tz?, enabled, createdAt, lastRunAt?, lastStatus? }`.
20
+ * `nextRunAt` is NOT persisted — the runner computes it in memory each tick.
21
+ *
22
+ * `validateJob` is the pure gate the API runs before writing: slug-shaped id, a
23
+ * known channel that's a VAULT transport, and a parseable cron.
24
+ */
25
+
26
+ import { parseCron, CronParseError } from "./cron.ts";
27
+ import { VaultTransport } from "./transports/vault.ts";
28
+ import type { Channel } from "./registry.ts";
29
+
30
+ /** A job's schedule: a 5-field cron expr + optional IANA tz (default daemon-local). */
31
+ export interface JobSchedule {
32
+ /** 5-field cron: `min hour dom mon dow`. */
33
+ cron: string;
34
+ /** IANA timezone (e.g. "America/Los_Angeles"). Optional — default daemon-local. */
35
+ tz?: string;
36
+ }
37
+
38
+ /**
39
+ * One scheduled job. The in-memory shape the runner + API operate on; persisted as
40
+ * a `#agent/job` vault note (see the store below). `id` is the slug (also the
41
+ * `runner:<id>` sender provenance + the note path segment).
42
+ */
43
+ export interface Job {
44
+ /** The operator-facing slug (typed on create; addresses the job in `/api/jobs/:id`). */
45
+ id: string;
46
+ /**
47
+ * The vault note id/path that addresses the persisted note for PATCH/DELETE.
48
+ * Absent on a freshly-created in-memory `Job` (set after `upsert` / on `listAll`).
49
+ * The runner + UI key off `id` (the slug); the store uses `noteId` for I/O.
50
+ */
51
+ noteId?: string;
52
+ /** The channel to inject into — MUST be a vault channel. */
53
+ channel: string;
54
+ /** The message text written as the inbound note content (= the job note's content). */
55
+ message: string;
56
+ /** When to fire. */
57
+ schedule: JobSchedule;
58
+ /** Whether the runner considers this job (default true on create). */
59
+ enabled: boolean;
60
+ /** ISO timestamp the job was created. */
61
+ createdAt: string;
62
+ /** ISO timestamp of the most recent fire (set by the runner; persisted via PATCH). */
63
+ lastRunAt?: string;
64
+ /** "ok" or "error: <detail>" from the most recent fire (persisted via PATCH). */
65
+ lastStatus?: string;
66
+ /** ISO timestamp of the next scheduled fire — COMPUTED IN MEMORY, never persisted. */
67
+ nextRunAt?: string;
68
+ }
69
+
70
+ /** A slug: alphanumeric, dash, underscore (same shape as channel names). */
71
+ const SLUG_RE = /^[a-zA-Z0-9_-]+$/;
72
+
73
+ /** The result of validating a candidate job. `ok:false` carries an operator-facing reason. */
74
+ export type JobValidation = { ok: true } | { ok: false; error: string };
75
+
76
+ /**
77
+ * Validate a candidate job before it's persisted. The API runs this and maps an
78
+ * `ok:false` to a 400. Pure (no vault I/O) — `isVaultChannel(name)` is injected:
79
+ * - returns `true` → known + vault,
80
+ * - returns `false` → known but NOT vault,
81
+ * - returns `null` → unknown channel.
82
+ *
83
+ * Checks, in order: id slug → non-empty message → parseable cron → valid tz (if
84
+ * present) → channel known AND vault (the inject path is "write an inbound note,"
85
+ * which only a vault transport supports).
86
+ */
87
+ export function validateJob(
88
+ candidate: {
89
+ id?: unknown;
90
+ channel?: unknown;
91
+ message?: unknown;
92
+ schedule?: unknown;
93
+ enabled?: unknown;
94
+ },
95
+ isVaultChannel: (name: string) => boolean | null,
96
+ ): JobValidation {
97
+ if (typeof candidate.id !== "string" || !SLUG_RE.test(candidate.id)) {
98
+ return { ok: false, error: "id must be a slug (alphanumeric, dash, underscore)" };
99
+ }
100
+ if (typeof candidate.message !== "string" || candidate.message.trim().length === 0) {
101
+ return { ok: false, error: "message must be a non-empty string" };
102
+ }
103
+ const sched = candidate.schedule as { cron?: unknown; tz?: unknown } | undefined;
104
+ if (!sched || typeof sched.cron !== "string" || sched.cron.trim().length === 0) {
105
+ return { ok: false, error: "schedule.cron must be a non-empty cron expression" };
106
+ }
107
+ try {
108
+ parseCron(sched.cron);
109
+ } catch (err) {
110
+ const msg = err instanceof CronParseError ? err.message : String(err);
111
+ return { ok: false, error: `invalid schedule.cron: ${msg}` };
112
+ }
113
+ if (sched.tz !== undefined) {
114
+ if (typeof sched.tz !== "string" || sched.tz.length === 0) {
115
+ return { ok: false, error: "schedule.tz must be a non-empty IANA timezone string" };
116
+ }
117
+ try {
118
+ // Construct a formatter to validate the zone — throws RangeError if invalid.
119
+ new Intl.DateTimeFormat("en-US", { timeZone: sched.tz });
120
+ } catch {
121
+ return { ok: false, error: `invalid schedule.tz: "${sched.tz}" is not a known IANA timezone` };
122
+ }
123
+ }
124
+ if (typeof candidate.channel !== "string" || candidate.channel.length === 0) {
125
+ return { ok: false, error: "channel must be a non-empty string" };
126
+ }
127
+ const vault = isVaultChannel(candidate.channel);
128
+ if (vault === null) {
129
+ return { ok: false, error: `unknown channel "${candidate.channel}"` };
130
+ }
131
+ if (vault === false) {
132
+ return {
133
+ ok: false,
134
+ error: `channel "${candidate.channel}" is not a vault channel — scheduled jobs require a vault-backed agent (the runner injects an inbound note)`,
135
+ };
136
+ }
137
+ return { ok: true };
138
+ }
139
+
140
+ /**
141
+ * Resolve a channel name to its live `VaultTransport`, or null if the channel is
142
+ * unknown or not vault-backed. Shared by the store + the runner's discovery so the
143
+ * "is this a vault channel?" check is one implementation.
144
+ */
145
+ export function vaultTransportFor(
146
+ channels: Map<string, Channel>,
147
+ name: string,
148
+ ): VaultTransport | null {
149
+ const t = channels.get(name)?.transport;
150
+ return t instanceof VaultTransport ? t : null;
151
+ }
152
+
153
+ /**
154
+ * The vault-native job store. Same read-all / upsert / remove interface the file
155
+ * store had, now backed by `#agent/job` vault notes via the channel's
156
+ * `VaultTransport`. Each method resolves the target channel's vault transport (the
157
+ * channel carries the vault binding + write token) and delegates the I/O to it.
158
+ *
159
+ * `listAll` queries every UNIQUE vault among the live vault-channels once and maps
160
+ * each job note to a `Job`, routing by `metadata.channel`. A job note whose
161
+ * `channel` no longer names a live vault channel is still RETURNED (so the API/UI
162
+ * can show + let the operator delete a stale job), but the runner's discovery
163
+ * (which composes on top) skips firing it.
164
+ */
165
+ export class VaultJobStore {
166
+ constructor(private readonly channels: Map<string, Channel>) {}
167
+
168
+ /**
169
+ * List all scheduled jobs across every vault the live channels point at. We
170
+ * query each DISTINCT vault transport once (dedup by vault URL + name), then map
171
+ * job notes to `Job`s. A note read from one vault may target ANY channel on that
172
+ * vault (routing is by `metadata.channel`), so we keep every well-formed job
173
+ * note. De-dup by job id across vaults isn't needed — ids are namespaced by the
174
+ * channel's vault in practice — but if two notes share an id we keep both (the
175
+ * runner routes each by its own `channel`).
176
+ */
177
+ async listAll(): Promise<Job[]> {
178
+ // Dedup the vaults we query by vault IDENTITY (origin + name), NOT transport
179
+ // instance: many channels each construct their OWN VaultTransport pointing at the
180
+ // SAME vault, so instance-identity dedup misses and the same job notes come back
181
+ // once per channel sharing that vault. Key on `vaultKey()`. (Caught live
182
+ // 2026-06-18: one job listed 3x with three channels on the default vault.)
183
+ const seen = new Set<string>();
184
+ const transports: VaultTransport[] = [];
185
+ for (const ch of this.channels.values()) {
186
+ if (ch.transport instanceof VaultTransport && !seen.has(ch.transport.vaultKey())) {
187
+ seen.add(ch.transport.vaultKey());
188
+ transports.push(ch.transport);
189
+ }
190
+ }
191
+ const jobs: Job[] = [];
192
+ for (const t of transports) {
193
+ const notes = await t.listJobNotes();
194
+ for (const n of notes) {
195
+ jobs.push({
196
+ id: n.id, // the operator-facing slug (metadata.jobId)
197
+ noteId: n.noteId, // the vault note id/path — for PATCH/DELETE
198
+ channel: n.channel,
199
+ message: n.message,
200
+ schedule: { cron: n.cron, ...(n.tz ? { tz: n.tz } : {}) },
201
+ enabled: n.enabled,
202
+ createdAt: n.createdAt ?? "",
203
+ ...(n.lastRunAt ? { lastRunAt: n.lastRunAt } : {}),
204
+ ...(n.lastStatus ? { lastStatus: n.lastStatus } : {}),
205
+ });
206
+ }
207
+ }
208
+ return jobs;
209
+ }
210
+
211
+ /**
212
+ * Create or replace a job (by slug id) as a `#agent/job` note in its target
213
+ * channel's vault. Throws if the target channel isn't a live vault channel
214
+ * (the API validates this first, so it's a guard, not the primary check).
215
+ * Returns the persisted job with its `noteId` filled in (the `id` stays the slug).
216
+ */
217
+ async upsert(job: Job): Promise<Job> {
218
+ const t = vaultTransportFor(this.channels, job.channel);
219
+ if (!t) {
220
+ throw new Error(`cannot store job "${job.id}": channel "${job.channel}" is not a live vault channel`);
221
+ }
222
+ const { id: noteId } = await t.upsertJobNote({
223
+ id: job.id,
224
+ message: job.message,
225
+ channel: job.channel,
226
+ cron: job.schedule.cron,
227
+ ...(job.schedule.tz ? { tz: job.schedule.tz } : {}),
228
+ enabled: job.enabled,
229
+ createdAt: job.createdAt,
230
+ ...(job.lastRunAt ? { lastRunAt: job.lastRunAt } : {}),
231
+ ...(job.lastStatus ? { lastStatus: job.lastStatus } : {}),
232
+ });
233
+ return { ...job, noteId }; // id stays the slug; noteId addresses the persisted note.
234
+ }
235
+
236
+ /**
237
+ * Delete a job by its vault note id/path. The job lives in ITS channel's vault,
238
+ * so we need that channel to resolve the right transport. The API passes the
239
+ * job's `noteId` + channel (it has the job in hand from a prior list). Throws on
240
+ * a non-ok vault response.
241
+ */
242
+ async remove(noteId: string, channel: string): Promise<void> {
243
+ const t = vaultTransportFor(this.channels, channel);
244
+ if (!t) {
245
+ throw new Error(`cannot delete job in channel "${channel}": not a live vault channel`);
246
+ }
247
+ await t.deleteJobNote(noteId);
248
+ }
249
+
250
+ /**
251
+ * PATCH a job's bookkeeping (lastRunAt / lastStatus / enabled) onto its vault
252
+ * note. Used by the runner after a fire. Throws on a non-ok vault response (the
253
+ * runner swallows it — status persistence is best-effort).
254
+ */
255
+ async patch(
256
+ noteId: string,
257
+ channel: string,
258
+ fields: { lastRunAt?: string; lastStatus?: string; enabled?: boolean },
259
+ ): Promise<void> {
260
+ const t = vaultTransportFor(this.channels, channel);
261
+ if (!t) {
262
+ throw new Error(`cannot patch job in channel "${channel}": not a live vault channel`);
263
+ }
264
+ await t.patchJobNote(noteId, fields);
265
+ }
266
+ }
@@ -0,0 +1,265 @@
1
+ /**
2
+ * HTTP MCP tests — the per-channel session registry, the wake push, write-scope
3
+ * enforcement on tool dispatch, and the daemon's /mcp/<channel> auth gate.
4
+ *
5
+ * Like daemon.test.ts these need NO live hub: the no-token path in requireScope
6
+ * short-circuits before any JWKS fetch, and the registry/push/tool tests drive
7
+ * the in-memory session set directly with fake servers + a fake transport.
8
+ */
9
+ import { describe, test, expect, afterEach, beforeAll, afterAll } from "bun:test";
10
+ import {
11
+ pushToChannel,
12
+ pushPermissionVerdict,
13
+ mcpSessionCount,
14
+ assertMcpSdkStreamContract,
15
+ _resetSessionsForTest,
16
+ } from "./mcp-http.ts";
17
+ import { createFetchHandler } from "./daemon.ts";
18
+ import { ClientRegistry } from "./routing.ts";
19
+ import { HttpUiTransport } from "./transports/http-ui.ts";
20
+ import type { Channel } from "./registry.ts";
21
+ import type { Transport, ReplyArgs } from "./transport.ts";
22
+
23
+ // ---------------------------------------------------------------------------
24
+ // Registry + wake — drive the session set directly via a registration shim.
25
+ //
26
+ // pushToChannel iterates the channel's session set and calls
27
+ // server.notification. We register fake sessions by reaching the same internal
28
+ // maps the way handleMcp's onsessioninitialized would. Since those maps are
29
+ // module-private, we exercise them through the public surface: register via a
30
+ // tiny test-only re-entry that mirrors registerSession. To keep this hermetic
31
+ // without exporting internals, we register by spinning handleMcp is overkill;
32
+ // instead we assert push reaches a registered session through a captured
33
+ // server.notification. We use the exported _registerForTest below.
34
+ // ---------------------------------------------------------------------------
35
+
36
+ import { _registerSessionForTest, _unregisterSessionForTest } from "./mcp-http.ts";
37
+
38
+ interface FakeServer {
39
+ notes: Array<{ method: string; params: unknown }>;
40
+ }
41
+
42
+ function fakeSession(): { server: { notification: (n: unknown) => void }; captured: FakeServer } {
43
+ const captured: FakeServer = { notes: [] };
44
+ const server = {
45
+ notification(n: unknown) {
46
+ captured.notes.push(n as { method: string; params: unknown });
47
+ },
48
+ };
49
+ return { server, captured };
50
+ }
51
+
52
+ afterEach(() => {
53
+ _resetSessionsForTest();
54
+ });
55
+
56
+ describe("per-channel MCP session registry + wake push", () => {
57
+ test("pushToChannel reaches a session on A and NOT one on B", () => {
58
+ const a = fakeSession();
59
+ const b = fakeSession();
60
+ _registerSessionForTest("A", "sid-a", a.server as never, ["agent:read", "agent:write"]);
61
+ _registerSessionForTest("B", "sid-b", b.server as never, ["agent:read"]);
62
+
63
+ const delivered = pushToChannel("A", "hello A", { foo: "bar" });
64
+ expect(delivered).toBe(1);
65
+ expect(a.captured.notes).toHaveLength(1);
66
+ expect(a.captured.notes[0]!.method).toBe("notifications/claude/agent");
67
+ expect((a.captured.notes[0]!.params as { content: string }).content).toBe("hello A");
68
+ // Source is stamped + caller meta merged.
69
+ expect((a.captured.notes[0]!.params as { meta: Record<string, string> }).meta).toMatchObject({
70
+ source: "parachute-agent",
71
+ foo: "bar",
72
+ });
73
+ // B got nothing.
74
+ expect(b.captured.notes).toHaveLength(0);
75
+ });
76
+
77
+ test("two sessions on the same channel both get the wake", () => {
78
+ const a1 = fakeSession();
79
+ const a2 = fakeSession();
80
+ _registerSessionForTest("A", "sid-a1", a1.server as never, ["agent:read"]);
81
+ _registerSessionForTest("A", "sid-a2", a2.server as never, ["agent:read"]);
82
+ expect(mcpSessionCount("A")).toBe(2);
83
+ const delivered = pushToChannel("A", "broadcast", {});
84
+ expect(delivered).toBe(2);
85
+ expect(a1.captured.notes).toHaveLength(1);
86
+ expect(a2.captured.notes).toHaveLength(1);
87
+ });
88
+
89
+ test("pushPermissionVerdict pushes the permission method", () => {
90
+ const a = fakeSession();
91
+ _registerSessionForTest("A", "sid-a", a.server as never, ["agent:read"]);
92
+ const delivered = pushPermissionVerdict("A", { request_id: "r1", behavior: "allow" });
93
+ expect(delivered).toBe(1);
94
+ expect(a.captured.notes[0]!.method).toBe("notifications/claude/agent/permission");
95
+ expect(a.captured.notes[0]!.params).toMatchObject({ request_id: "r1", behavior: "allow" });
96
+ });
97
+
98
+ test("push to an unknown channel delivers to nobody (0)", () => {
99
+ expect(pushToChannel("nope", "x", {})).toBe(0);
100
+ expect(pushPermissionVerdict("nope", { request_id: "r", behavior: "deny" })).toBe(0);
101
+ });
102
+
103
+ test("a streamless session (registered, no live GET stream) is NOT counted as delivered", () => {
104
+ // The bug this guards: a session that POSTed `initialize` but hasn't opened (or
105
+ // has dropped) its standalone GET stream is registered, but the SDK silently
106
+ // drops any notification to it. If pushToChannel counted it, the daemon would
107
+ // advance the channel's delivery mark and the message would be lost.
108
+ const a = fakeSession();
109
+ _registerSessionForTest("A", "sid-streamless", a.server as never, ["agent:read"], {
110
+ streamless: true,
111
+ });
112
+ expect(mcpSessionCount("A")).toBe(1); // it IS registered…
113
+ expect(pushToChannel("A", "into the void", {})).toBe(0); // …but NOT deliverable
114
+ expect(a.captured.notes).toHaveLength(0); // not even attempted
115
+ // Permission verdicts honor the same gate.
116
+ expect(pushPermissionVerdict("A", { request_id: "r", behavior: "allow" })).toBe(0);
117
+ });
118
+
119
+ test("pushToChannel counts only the live-stream sessions in a mixed set", () => {
120
+ const live = fakeSession();
121
+ const dead = fakeSession();
122
+ _registerSessionForTest("A", "sid-live", live.server as never, ["agent:read"]);
123
+ _registerSessionForTest("A", "sid-streamless", dead.server as never, ["agent:read"], {
124
+ streamless: true,
125
+ });
126
+ expect(mcpSessionCount("A")).toBe(2); // both registered
127
+ expect(pushToChannel("A", "hi", {})).toBe(1); // only the one with a live stream
128
+ expect(live.captured.notes).toHaveLength(1);
129
+ expect(dead.captured.notes).toHaveLength(0);
130
+ });
131
+
132
+ test("the installed MCP SDK still keys the standalone GET stream as we expect (contract guard)", () => {
133
+ // If this fails, the SDK renamed the internal sessionHasLivePushStream reads —
134
+ // HTTP-MCP delivery would silently break. Catch it here, not in production.
135
+ expect(assertMcpSdkStreamContract()).toBe(true);
136
+ });
137
+
138
+ test("mcpSessionCount tracks registration + reset", () => {
139
+ expect(mcpSessionCount("A")).toBe(0);
140
+ const a = fakeSession();
141
+ _registerSessionForTest("A", "sid-a", a.server as never, ["agent:read"]);
142
+ expect(mcpSessionCount("A")).toBe(1);
143
+ _resetSessionsForTest();
144
+ expect(mcpSessionCount("A")).toBe(0);
145
+ });
146
+
147
+ test("unregister cleans up the session and drops the empty channel set (no leak)", () => {
148
+ _registerSessionForTest("A", "sid-a1", fakeSession().server as never, ["agent:read"]);
149
+ _registerSessionForTest("A", "sid-a2", fakeSession().server as never, ["agent:read"]);
150
+ expect(mcpSessionCount("A")).toBe(2);
151
+ _unregisterSessionForTest("A", "sid-a1");
152
+ expect(mcpSessionCount("A")).toBe(1); // the other session survives
153
+ _unregisterSessionForTest("A", "sid-a2");
154
+ expect(mcpSessionCount("A")).toBe(0); // empty set removed — no orphaned channel entry
155
+ // a push to the now-cleaned channel reaches nobody
156
+ expect(pushToChannel("A", "hi", {})).toBe(0);
157
+ });
158
+ });
159
+
160
+ // ---------------------------------------------------------------------------
161
+ // Tool dispatch — a reply tool call routes to the channel's transport.
162
+ // ---------------------------------------------------------------------------
163
+
164
+ describe("tool dispatch routes to the channel's transport + enforces write scope", () => {
165
+ function fakeTransport(): { transport: Transport; replies: ReplyArgs[] } {
166
+ const replies: ReplyArgs[] = [];
167
+ const transport: Transport = {
168
+ kind: "fake",
169
+ async start() {},
170
+ async stop() {},
171
+ async reply(args: ReplyArgs) {
172
+ replies.push(args);
173
+ return { sent: ["msg-1"] };
174
+ },
175
+ };
176
+ return { transport, replies };
177
+ }
178
+
179
+ test("a write-scoped reply call reaches transport.reply with the channel + args", async () => {
180
+ const { transport, replies } = fakeTransport();
181
+ const { callReplyTool } = await import("./mcp-http.ts");
182
+ const result = await callReplyTool("A", transport, ["agent:read", "agent:write"], {
183
+ text: "hi there",
184
+ chat_id: "42",
185
+ });
186
+ expect(result.isError).toBeUndefined();
187
+ expect(replies).toHaveLength(1);
188
+ expect(replies[0]).toMatchObject({ channel: "A", text: "hi there", meta: { chat_id: "42" } });
189
+ });
190
+
191
+ test("a read-only token cannot call reply (write scope enforced)", async () => {
192
+ const { transport, replies } = fakeTransport();
193
+ const { callReplyTool } = await import("./mcp-http.ts");
194
+ const result = await callReplyTool("A", transport, ["agent:read"], { text: "blocked" });
195
+ expect(result.isError).toBe(true);
196
+ expect(replies).toHaveLength(0);
197
+ expect((result.content[0] as { text: string }).text).toContain("agent:write");
198
+ });
199
+
200
+ test("DUAL-ACCEPT: a pre-rename token with the LEGACY channel:write scope can still call reply", async () => {
201
+ // The write-tool gate must dual-accept (grantsScope), not raw-includes — else
202
+ // a pre-rename token connects + is woken but silently can't send (channel#…).
203
+ const { transport, replies } = fakeTransport();
204
+ const { callReplyTool } = await import("./mcp-http.ts");
205
+ const result = await callReplyTool("A", transport, ["channel:read", "channel:write"], {
206
+ text: "legacy-send",
207
+ });
208
+ expect(result.isError).toBeUndefined();
209
+ expect(replies).toHaveLength(1);
210
+ expect(replies[0]).toMatchObject({ channel: "A", text: "legacy-send" });
211
+ });
212
+ });
213
+
214
+ // ---------------------------------------------------------------------------
215
+ // Daemon auth gate — POST /mcp/<channel> with no bearer → 401 (pre-JWKS).
216
+ // ---------------------------------------------------------------------------
217
+
218
+ describe("daemon /mcp/<channel> auth gate", () => {
219
+ let server: ReturnType<typeof Bun.serve>;
220
+ let base: string;
221
+
222
+ beforeAll(async () => {
223
+ const registry = new ClientRegistry();
224
+ const transport = new HttpUiTransport({ channel: "ui1" });
225
+ await transport.start({ channel: "ui1", emit: () => {}, emitPermissionVerdict: () => {} });
226
+ const channels = new Map<string, Channel>([
227
+ ["ui1", { name: "ui1", transport, entry: { name: "ui1", transport: "http-ui" } }],
228
+ ]);
229
+ server = Bun.serve({
230
+ port: 0,
231
+ hostname: "127.0.0.1",
232
+ idleTimeout: 0,
233
+ fetch: createFetchHandler(channels, registry),
234
+ });
235
+ base = `http://127.0.0.1:${server.port}`;
236
+ });
237
+
238
+ afterAll(() => {
239
+ server.stop(true);
240
+ });
241
+
242
+ test("POST /mcp/ui1 with an initialize body and no bearer → 401", async () => {
243
+ const res = await fetch(`${base}/mcp/ui1`, {
244
+ method: "POST",
245
+ headers: { "content-type": "application/json", accept: "application/json, text/event-stream" },
246
+ body: JSON.stringify({
247
+ jsonrpc: "2.0",
248
+ id: 1,
249
+ method: "initialize",
250
+ params: { protocolVersion: "2025-06-18", capabilities: {}, clientInfo: { name: "t", version: "0" } },
251
+ }),
252
+ });
253
+ expect(res.status).toBe(401);
254
+ expect(((await res.json()) as { error: string }).error).toBe("unauthorized");
255
+ });
256
+
257
+ test("POST /mcp/unknown-channel → 404 (channel miss, before auth body)", async () => {
258
+ const res = await fetch(`${base}/mcp/does-not-exist`, {
259
+ method: "POST",
260
+ headers: { "content-type": "application/json" },
261
+ body: JSON.stringify({ jsonrpc: "2.0", id: 1, method: "initialize", params: {} }),
262
+ });
263
+ expect(res.status).toBe(404);
264
+ });
265
+ });