@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/cron.ts ADDED
@@ -0,0 +1,380 @@
1
+ /**
2
+ * A tiny, dependency-free cron evaluator for the runner (design
3
+ * `2026-06-17-runner-scheduled-agent-turns.md`).
4
+ *
5
+ * Scope (v1, deliberately narrow):
6
+ * - FIVE fields only: `minute hour day-of-month month day-of-week`.
7
+ * - Each field supports `*`, `*​/n` (step), `a-b` (range), `a-b/n` (range+step),
8
+ * and `a,b,c` (comma list, each element any of the above).
9
+ * - NO seconds field, NO macros (`@daily`), NO names (`MON`, `JAN`) — numeric only.
10
+ * - day-of-week: 0-6, Sunday = 0. (7 is NOT accepted as Sunday in v1 — keep the
11
+ * accepted set tight and unambiguous.)
12
+ *
13
+ * Day-of-month / day-of-week semantics follow standard cron: if BOTH `dom` and
14
+ * `dow` are restricted (neither is `*`), a day matches when EITHER matches (the
15
+ * union). If only one is restricted, only that one constrains. This is the Vixie
16
+ * cron rule and the one operators expect from "0 9 * * 1-5".
17
+ *
18
+ * Timezone correctness — the #1 risk. `nextRunAfter` evaluates the cron fields
19
+ * against the WALL CLOCK in a given IANA timezone (default: the daemon's local
20
+ * tz). It does this by walking forward minute-by-minute from `from`, projecting
21
+ * each candidate instant into the target tz via `Intl.DateTimeFormat` (Bun ships
22
+ * full ICU), and testing the projected wall-clock fields against the cron sets.
23
+ * This is O(minutes-until-next-match); bounded by a hard cap (~366 days of
24
+ * minutes) so a pathological/never-matching spec returns null instead of looping
25
+ * forever. The minute-walk is simple and DST-honest: because we read the wall
26
+ * clock the OS/ICU reports for each real instant, a spring-forward gap simply has
27
+ * no matching instant (the wall time never occurs → that fire is skipped that
28
+ * day), and a fall-back repeat can match twice (both 01:30s exist as distinct
29
+ * instants → both fire). v1 accepts that behavior rather than inventing a policy;
30
+ * see the comment at `nextRunAfter`.
31
+ */
32
+
33
+ /** A parsed cron expression: the allowed-value Sets per field, plus restriction flags. */
34
+ export interface ParsedCron {
35
+ minute: Set<number>;
36
+ hour: Set<number>;
37
+ /** day-of-month (1-31). */
38
+ dom: Set<number>;
39
+ /** month (1-12). */
40
+ month: Set<number>;
41
+ /** day-of-week (0-6, Sun=0). */
42
+ dow: Set<number>;
43
+ /** Whether `dom` was `*` (unrestricted) — drives the dom/dow union rule. */
44
+ domStar: boolean;
45
+ /** Whether `dow` was `*` (unrestricted) — drives the dom/dow union rule. */
46
+ dowStar: boolean;
47
+ }
48
+
49
+ /** Inclusive numeric bounds for each of the five fields (v1: numeric only). */
50
+ const FIELD_BOUNDS: ReadonlyArray<{ min: number; max: number; name: string }> = [
51
+ { min: 0, max: 59, name: "minute" },
52
+ { min: 0, max: 23, name: "hour" },
53
+ { min: 1, max: 31, name: "day-of-month" },
54
+ { min: 1, max: 12, name: "month" },
55
+ { min: 0, max: 6, name: "day-of-week" },
56
+ ];
57
+
58
+ /** Thrown by `parseCron` on a malformed expression — callers map it to a 400. */
59
+ export class CronParseError extends Error {}
60
+
61
+ /**
62
+ * Parse ONE field token (between the spaces) into the set of allowed integers in
63
+ * `[min, max]`. Supports `*`, `*​/n`, `a`, `a-b`, `a-b/n`, and comma lists of any
64
+ * of those. Throws `CronParseError` on anything out of range or malformed.
65
+ */
66
+ function parseField(token: string, min: number, max: number, fieldName: string): { set: Set<number>; star: boolean } {
67
+ const trimmed = token.trim();
68
+ if (trimmed.length === 0) {
69
+ throw new CronParseError(`cron ${fieldName}: empty field`);
70
+ }
71
+ const set = new Set<number>();
72
+ // A field is `*` only if EVERY comma-element is `*` / `*​/n` over the full range.
73
+ // We track whether the literal token was a bare `*` (no list, no step) for the
74
+ // dom/dow union rule — a stepped `*​/2` is a restriction, not "unrestricted".
75
+ const star = trimmed === "*";
76
+
77
+ for (const part of trimmed.split(",")) {
78
+ const elem = part.trim();
79
+ if (elem.length === 0) {
80
+ throw new CronParseError(`cron ${fieldName}: empty list element in "${token}"`);
81
+ }
82
+
83
+ // Split off an optional step (`/n`).
84
+ let rangePart = elem;
85
+ let step = 1;
86
+ const slash = elem.indexOf("/");
87
+ if (slash >= 0) {
88
+ rangePart = elem.slice(0, slash);
89
+ const stepStr = elem.slice(slash + 1);
90
+ if (!/^\d+$/.test(stepStr)) {
91
+ throw new CronParseError(`cron ${fieldName}: bad step "/${stepStr}" in "${token}"`);
92
+ }
93
+ step = parseInt(stepStr, 10);
94
+ if (step <= 0) {
95
+ throw new CronParseError(`cron ${fieldName}: step must be >= 1 in "${token}"`);
96
+ }
97
+ }
98
+
99
+ // Resolve the range the step applies over.
100
+ let lo: number;
101
+ let hi: number;
102
+ if (rangePart === "*") {
103
+ lo = min;
104
+ hi = max;
105
+ } else if (rangePart.includes("-")) {
106
+ const [aStr, bStr, ...rest] = rangePart.split("-");
107
+ if (rest.length > 0 || aStr === undefined || bStr === undefined) {
108
+ throw new CronParseError(`cron ${fieldName}: bad range "${rangePart}" in "${token}"`);
109
+ }
110
+ if (!/^\d+$/.test(aStr) || !/^\d+$/.test(bStr)) {
111
+ throw new CronParseError(`cron ${fieldName}: non-numeric range "${rangePart}" in "${token}"`);
112
+ }
113
+ lo = parseInt(aStr, 10);
114
+ hi = parseInt(bStr, 10);
115
+ if (lo > hi) {
116
+ throw new CronParseError(`cron ${fieldName}: descending range "${rangePart}" in "${token}"`);
117
+ }
118
+ } else {
119
+ // A single number, optionally with a step (`5/2` means 5,7,9,… to max — the
120
+ // step extends a bare number to the field max, matching common cron impls).
121
+ if (!/^\d+$/.test(rangePart)) {
122
+ throw new CronParseError(`cron ${fieldName}: non-numeric value "${rangePart}" in "${token}"`);
123
+ }
124
+ lo = parseInt(rangePart, 10);
125
+ hi = slash >= 0 ? max : lo;
126
+ }
127
+
128
+ if (lo < min || hi > max) {
129
+ throw new CronParseError(
130
+ `cron ${fieldName}: value out of range (${lo}-${hi}); allowed ${min}-${max}`,
131
+ );
132
+ }
133
+ for (let v = lo; v <= hi; v += step) set.add(v);
134
+ }
135
+
136
+ if (set.size === 0) {
137
+ throw new CronParseError(`cron ${fieldName}: no values matched in "${token}"`);
138
+ }
139
+ return { set, star };
140
+ }
141
+
142
+ /**
143
+ * Parse a 5-field cron expression into a {@link ParsedCron}. Throws
144
+ * `CronParseError` (message names the offending field) on anything malformed.
145
+ * Whitespace between fields is any run of spaces/tabs.
146
+ */
147
+ export function parseCron(expr: string): ParsedCron {
148
+ if (typeof expr !== "string") {
149
+ throw new CronParseError("cron expression must be a string");
150
+ }
151
+ const fields = expr.trim().split(/\s+/);
152
+ if (fields.length !== 5) {
153
+ throw new CronParseError(
154
+ `cron expression must have exactly 5 fields (min hour dom mon dow); got ${fields.length}: "${expr}"`,
155
+ );
156
+ }
157
+ const minute = parseField(fields[0]!, FIELD_BOUNDS[0]!.min, FIELD_BOUNDS[0]!.max, "minute");
158
+ const hour = parseField(fields[1]!, FIELD_BOUNDS[1]!.min, FIELD_BOUNDS[1]!.max, "hour");
159
+ const dom = parseField(fields[2]!, FIELD_BOUNDS[2]!.min, FIELD_BOUNDS[2]!.max, "day-of-month");
160
+ const month = parseField(fields[3]!, FIELD_BOUNDS[3]!.min, FIELD_BOUNDS[3]!.max, "month");
161
+ const dow = parseField(fields[4]!, FIELD_BOUNDS[4]!.min, FIELD_BOUNDS[4]!.max, "day-of-week");
162
+ return {
163
+ minute: minute.set,
164
+ hour: hour.set,
165
+ dom: dom.set,
166
+ month: month.set,
167
+ dow: dow.set,
168
+ domStar: dom.star,
169
+ dowStar: dow.star,
170
+ };
171
+ }
172
+
173
+ /** The wall-clock fields of an instant projected into a given IANA timezone. */
174
+ interface WallClock {
175
+ minute: number;
176
+ hour: number;
177
+ /** day-of-month (1-31). */
178
+ dom: number;
179
+ /** month (1-12). */
180
+ month: number;
181
+ /** day-of-week (0-6, Sun=0). */
182
+ dow: number;
183
+ }
184
+
185
+ const WEEKDAY_INDEX: Record<string, number> = {
186
+ Sun: 0,
187
+ Mon: 1,
188
+ Tue: 2,
189
+ Wed: 3,
190
+ Thu: 4,
191
+ Fri: 5,
192
+ Sat: 6,
193
+ };
194
+
195
+ /**
196
+ * Project a real instant (`Date`) into its wall-clock fields IN `tz` via
197
+ * `Intl.DateTimeFormat`. Throws if `tz` is not a valid IANA zone (the formatter
198
+ * throws a RangeError — we let it propagate so `nextRunAfter`'s caller surfaces
199
+ * "bad timezone" rather than silently using UTC).
200
+ */
201
+ function wallClockInTz(date: Date, tz: string): WallClock {
202
+ // `en-US` with explicit numeric parts + `weekday: short` gives stable,
203
+ // locale-independent token shapes we can parse back to integers.
204
+ const parts = new Intl.DateTimeFormat("en-US", {
205
+ timeZone: tz,
206
+ hour12: false,
207
+ weekday: "short",
208
+ year: "numeric",
209
+ month: "numeric",
210
+ day: "numeric",
211
+ hour: "numeric",
212
+ minute: "numeric",
213
+ }).formatToParts(date);
214
+
215
+ const get = (type: string): string => {
216
+ const p = parts.find((x) => x.type === type);
217
+ return p ? p.value : "";
218
+ };
219
+
220
+ // `hour12: false` can emit "24" for midnight in some ICU versions; normalize.
221
+ let hour = parseInt(get("hour"), 10);
222
+ if (hour === 24) hour = 0;
223
+
224
+ return {
225
+ minute: parseInt(get("minute"), 10),
226
+ hour,
227
+ dom: parseInt(get("day"), 10),
228
+ month: parseInt(get("month"), 10),
229
+ dow: WEEKDAY_INDEX[get("weekday")] ?? 0,
230
+ };
231
+ }
232
+
233
+ /** Does a wall clock satisfy the parsed cron? (applies the dom/dow union rule). */
234
+ function matches(parsed: ParsedCron, wc: WallClock): boolean {
235
+ if (!parsed.minute.has(wc.minute)) return false;
236
+ if (!parsed.hour.has(wc.hour)) return false;
237
+ if (!parsed.month.has(wc.month)) return false;
238
+
239
+ // Standard cron dom/dow rule: if BOTH are restricted, match on EITHER (union).
240
+ // If only one is restricted, only that one constrains. If both are `*`, both
241
+ // pass trivially.
242
+ const domOk = parsed.dom.has(wc.dom);
243
+ const dowOk = parsed.dow.has(wc.dow);
244
+ if (!parsed.domStar && !parsed.dowStar) {
245
+ return domOk || dowOk;
246
+ }
247
+ if (!parsed.domStar) return domOk;
248
+ if (!parsed.dowStar) return dowOk;
249
+ return true; // both `*`
250
+ }
251
+
252
+ /** Does ONLY the date part (month + dom/dow union) match? Used for coarse day-skip. */
253
+ function dateMatches(parsed: ParsedCron, wc: WallClock): boolean {
254
+ if (!parsed.month.has(wc.month)) return false;
255
+ const domOk = parsed.dom.has(wc.dom);
256
+ const dowOk = parsed.dow.has(wc.dow);
257
+ if (!parsed.domStar && !parsed.dowStar) return domOk || dowOk;
258
+ if (!parsed.domStar) return domOk;
259
+ if (!parsed.dowStar) return dowOk;
260
+ return true;
261
+ }
262
+
263
+ /**
264
+ * The hard cap for the forward search, expressed in DAYS. A sparse spec like
265
+ * "Feb 29" can legitimately be up to ~4 years out; cap at 5 years so it resolves
266
+ * while a truly never-matching spec (impossible with numeric-only fields, but
267
+ * cheap insurance) returns null instead of looping. Day-coarse skipping keeps the
268
+ * search O(days-until-match) + O(minutes-within-the-day), not O(total minutes).
269
+ */
270
+ const MAX_LOOKAHEAD_DAYS = 5 * 366;
271
+
272
+ /**
273
+ * Return the next instant STRICTLY AFTER `from` whose wall-clock-in-`tz` matches
274
+ * `expr`, or `null` if no match within ~5 years (effectively a never-firing spec).
275
+ *
276
+ * STRICTLY AFTER is load-bearing: the runner persists `nextRunAt` and re-derives
277
+ * forward from a fired instant, so returning `from` itself would double-fire on
278
+ * the same minute. We advance to the start of the NEXT minute first, then search
279
+ * forward at cron's one-minute resolution.
280
+ *
281
+ * Search strategy (so a sparse spec like Feb 29 doesn't take a million minute
282
+ * steps): a single FORWARD-ONLY minute cursor with a DAY-SKIP fast path. On each
283
+ * step we read the cursor's wall clock; if the cursor's wall DATE (month + dom/dow
284
+ * union) can't match, we skip the cursor forward to the next wall-midnight in one
285
+ * jump instead of crawling minute-by-minute through the dead day. On a date that
286
+ * DOES qualify, we walk its minutes testing the full predicate. So the cost is
287
+ * O(days-to-match) date-checks + O(minutes-in-the-few-matching-days) minute-checks.
288
+ *
289
+ * Forward-only is what keeps strictly-after honest: the cursor starts at the first
290
+ * minute AFTER `from` and never rewinds, so the first match it reaches is the
291
+ * earliest instant > `from`. (We deliberately do NOT rewind to wall-midnight on
292
+ * the matching day — that could surface a match earlier than `from`.)
293
+ *
294
+ * Timezone + DST: each candidate is a real instant; we read the wall clock the
295
+ * target tz reports for it. So a spring-forward gap (e.g. 02:00→03:00) has no
296
+ * matching instant for a 02:30 spec that day — it's simply skipped (the next
297
+ * day fires). A fall-back repeat (01:30 occurring twice) yields two distinct
298
+ * matching instants — both fire. v1 documents and accepts this rather than
299
+ * inventing skip/dedup policy; jobs are coarse ("daily 8am"), and the
300
+ * fire-once-on-miss catch-up in the runner bounds any practical surprise.
301
+ *
302
+ * The day-skip advances to the next WALL-midnight (00:00 of the next wall-day IN
303
+ * `zone`), computed from the wall clock we already read — NOT to UTC-midnight. This
304
+ * is load-bearing in a negative-offset zone: UTC-midnight there is ~17:00 of the
305
+ * wall-day, so jumping to it would strand the forward-only cursor in the *evening*
306
+ * and the crawl could never reach that wall-day's MORNING (a `0 9 * * *` would be
307
+ * missed; a sparse-dom morning job would never fire). Instead we step the cursor by
308
+ * the minutes from this wall-instant to the next wall-midnight
309
+ * (`(23-hour)*60 + (60-minute)`), which lands at/just-before the next wall-day's
310
+ * 00:00. A DST transition can make the landing 23:00 or 01:00 of the wall-day — the
311
+ * main loop self-corrects with a few minute steps, and it never lands in the
312
+ * evening. The minute-walk remains the source of truth, so no offset arithmetic can
313
+ * desync it. (Each skip advances ≥1 minute, so the search always terminates.)
314
+ *
315
+ * `from` defaults to now; `tz` defaults to the daemon's local timezone (resolved
316
+ * from `Intl.DateTimeFormat().resolvedOptions().timeZone`).
317
+ */
318
+ export function nextRunAfter(expr: string | ParsedCron, tz?: string, from: Date = new Date()): Date | null {
319
+ const parsed = typeof expr === "string" ? parseCron(expr) : expr;
320
+ const zone = tz ?? Intl.DateTimeFormat().resolvedOptions().timeZone;
321
+
322
+ // Validate the zone once (and fail loudly) by projecting `from`. An invalid IANA
323
+ // zone throws RangeError here, which propagates to the caller as a clear error.
324
+ wallClockInTz(from, zone);
325
+
326
+ // Start at the top of the NEXT minute after `from` (strictly-after + minute
327
+ // resolution: zero the seconds/ms and step one minute past `from`'s minute).
328
+ const cursor = new Date(from.getTime());
329
+ cursor.setUTCSeconds(0, 0);
330
+ cursor.setUTCMinutes(cursor.getUTCMinutes() + 1);
331
+
332
+ // Bound the search by DATE-CHECKS (one per distinct wall-day we touch), so a
333
+ // sparse "Feb 29" still resolves while a never-matching spec terminates. We
334
+ // count days touched, not minutes, since the day-skip collapses dead days.
335
+ let daysTouched = 0;
336
+ let lastSkipKey = "";
337
+
338
+ // Cap the total minute steps generously: matching days are few, and each gets a
339
+ // bounded (~25h) minute scan; a hard ceiling is belt-and-suspenders against an
340
+ // infinite loop. 5y of days × ~1500 min is the theoretical worst case, but the
341
+ // day-skip means we never get near it for real specs.
342
+ const MAX_MINUTE_STEPS = MAX_LOOKAHEAD_DAYS * 24 * 60;
343
+
344
+ for (let i = 0; i < MAX_MINUTE_STEPS; i++) {
345
+ const wc = wallClockInTz(cursor, zone);
346
+ if (dateMatches(parsed, wc)) {
347
+ if (matches(parsed, wc)) return new Date(cursor.getTime());
348
+ // Date qualifies but this minute/hour doesn't — crawl one minute.
349
+ cursor.setUTCMinutes(cursor.getUTCMinutes() + 1);
350
+ continue;
351
+ }
352
+ // Date does NOT qualify — DAY-SKIP: jump to the next WALL-midnight (00:00 of
353
+ // the next wall-day IN `zone`), computed from the wall clock we already have.
354
+ // Each distinct dead wall-day counts once against the lookahead bound.
355
+ //
356
+ // CRITICAL: we must advance to the next *wall*-midnight, NOT UTC-midnight. In a
357
+ // negative-offset zone (e.g. America/Los_Angeles, UTC-7/8) UTC-midnight is
358
+ // ~17:00 of the wall-day — so zeroing the UTC clock would land the cursor in
359
+ // the *evening* of a wall-day and the forward-only crawl could never reach that
360
+ // wall-day's MORNING (a `0 9 * * *` would be missed every cycle; a sparse-dom
361
+ // morning job would exhaust the search → null). Stepping by the minutes from
362
+ // THIS wall-instant to the next wall-midnight lands the cursor AT/BEFORE the
363
+ // morning of the next wall-day, so the crawl reaches it.
364
+ //
365
+ // `mins` is the minutes remaining in this wall-day plus one (to roll into the
366
+ // next day's 00:00): (23 - hour)*60 covers the whole hours left, +(60 - minute)
367
+ // covers the rest of this minute's hour AND the +1 minute to cross midnight.
368
+ // A DST transition can make the landing 23:00 or 01:00 of the wall-day rather
369
+ // than exactly 00:00 — harmless: the main loop self-corrects with a few minute
370
+ // steps, and it never lands at the evening (17:00) the UTC bump produced.
371
+ const skipKey = `${wc.month}-${wc.dom}`;
372
+ if (skipKey !== lastSkipKey) {
373
+ lastSkipKey = skipKey;
374
+ if (++daysTouched > MAX_LOOKAHEAD_DAYS) return null;
375
+ }
376
+ const mins = (23 - wc.hour) * 60 + (60 - wc.minute);
377
+ cursor.setUTCMinutes(cursor.getUTCMinutes() + mins);
378
+ }
379
+ return null;
380
+ }
@@ -0,0 +1,166 @@
1
+ /**
2
+ * Daemon route test for the vault-native agent-def RELOAD webhook
3
+ * (POST /api/vault/agent-def, design 2026-06-17-vault-native-agents Phase 4a).
4
+ *
5
+ * Auth mirrors /api/vault/inbound: hub JWT, scope agent:send, uniform-401. The
6
+ * AgentDefRegistry is injected with a recorder for its `reload`, so we assert the
7
+ * route's dispatch (auth → parse → route to vault → reload) without a real vault.
8
+ * Uses the same sentinel-token `mock.module("./hub-jwt.ts")` harness as the other
9
+ * daemon route tests (file-scoped).
10
+ */
11
+ import { describe, test, expect, mock } from "bun:test";
12
+
13
+ const SEND_TOKEN = "test-send-token"; // agent:send
14
+ import { HubJwtError, looksLikeJwt } from "@openparachute/scope-guard";
15
+ mock.module("./hub-jwt.ts", () => ({
16
+ AGENT_AUDIENCE: "agent",
17
+ CHANNEL_AUDIENCE: "channel",
18
+ async validateHubJwt(token: string) {
19
+ const base = { sub: "test", aud: "agent", jti: undefined, clientId: undefined, vaultScope: undefined };
20
+ if (token === SEND_TOKEN) return { ...base, scopes: ["agent:read", "agent:send"] };
21
+ throw new HubJwtError("issuer", "invalid token");
22
+ },
23
+ HubJwtError,
24
+ looksLikeJwt,
25
+ resetJwksCache() {},
26
+ resetRevocationCache() {},
27
+ }));
28
+
29
+ import { createFetchHandler } from "./daemon.ts";
30
+ import { ClientRegistry } from "./routing.ts";
31
+ import { AgentDefRegistry, type InstantiateDeps } from "./agent-defs.ts";
32
+ import type { Channel } from "./registry.ts";
33
+
34
+ /** A registry whose `reload` is recorded; one bound def-vault by default. */
35
+ function recordingRegistry(opts?: { vaults?: string[] }) {
36
+ const reloads: Array<{ vault: string; noteId: string; event?: string }> = [];
37
+ const noopDeps: InstantiateDeps = {
38
+ ensureChannel: async () => {},
39
+ setupAndRegister: async () => {},
40
+ deregister: async () => true,
41
+ removeChannel: async () => true,
42
+ };
43
+ const reg = new AgentDefRegistry(noopDeps, {
44
+ bindings: (opts?.vaults ?? ["default"]).map((v) => ({ vault: v, token: "t" })),
45
+ });
46
+ // Override reload to record (avoid any vault I/O).
47
+ reg.reload = (async (vault: string, noteId: string, event?: "created" | "updated" | "deleted") => {
48
+ reloads.push({ vault, noteId, event });
49
+ return "instantiated";
50
+ }) as typeof reg.reload;
51
+ return { reg, reloads };
52
+ }
53
+
54
+ function serverWith(channels: Map<string, Channel>, agentDefs?: AgentDefRegistry) {
55
+ const registry = new ClientRegistry();
56
+ const srv = Bun.serve({
57
+ port: 0,
58
+ hostname: "127.0.0.1",
59
+ idleTimeout: 0,
60
+ fetch: createFetchHandler(channels, registry, agentDefs ? { agentDefs } : undefined),
61
+ });
62
+ return { srv, base: `http://127.0.0.1:${srv.port}` };
63
+ }
64
+
65
+ const emptyChannels = () => new Map<string, Channel>();
66
+ const auth = { authorization: `Bearer ${SEND_TOKEN}`, "content-type": "application/json" };
67
+
68
+ describe("POST /api/vault/agent-def", () => {
69
+ test("no Authorization → 401", async () => {
70
+ const { reg } = recordingRegistry();
71
+ const { srv, base } = serverWith(emptyChannels(), reg);
72
+ const res = await fetch(`${base}/api/vault/agent-def`, {
73
+ method: "POST",
74
+ headers: { "content-type": "application/json" },
75
+ body: JSON.stringify({ note: { id: "n" } }),
76
+ });
77
+ expect(res.status).toBe(401);
78
+ srv.stop();
79
+ });
80
+
81
+ test("authed reload routes to the registry with vault + noteId + event (single-vault default)", async () => {
82
+ const { reg, reloads } = recordingRegistry();
83
+ const { srv, base } = serverWith(emptyChannels(), reg);
84
+ const res = await fetch(`${base}/api/vault/agent-def`, {
85
+ method: "POST",
86
+ headers: auth,
87
+ body: JSON.stringify({ event: "updated", note: { id: "Agents/uni-dev" } }),
88
+ });
89
+ expect(res.status).toBe(200);
90
+ const body = (await res.json()) as { ok: boolean; reloaded: string };
91
+ expect(body.ok).toBe(true);
92
+ expect(body.reloaded).toBe("instantiated");
93
+ // vault defaulted to the sole bound def-vault.
94
+ expect(reloads).toEqual([{ vault: "default", noteId: "Agents/uni-dev", event: "updated" }]);
95
+ srv.stop();
96
+ });
97
+
98
+ // Connector 1: the two def-reload triggers (note.created + note.updated) the
99
+ // hub provisions send `event: "created"` / `event: "updated"` here. Both must
100
+ // route through to reload() — the webhook coercion accepts BOTH, not just one.
101
+ test("a note.created trigger payload routes to reload (created → instantiate)", async () => {
102
+ const { reg, reloads } = recordingRegistry();
103
+ const { srv, base } = serverWith(emptyChannels(), reg);
104
+ const res = await fetch(`${base}/api/vault/agent-def`, {
105
+ method: "POST",
106
+ headers: auth,
107
+ body: JSON.stringify({ event: "created", note: { id: "Agents/new-agent" } }),
108
+ });
109
+ expect(res.status).toBe(200);
110
+ const body = (await res.json()) as { ok: boolean; reloaded: string };
111
+ expect(body.ok).toBe(true);
112
+ expect(reloads).toEqual([{ vault: "default", noteId: "Agents/new-agent", event: "created" }]);
113
+ srv.stop();
114
+ });
115
+
116
+ test("explicit body.vault is honored", async () => {
117
+ const { reg, reloads } = recordingRegistry({ vaults: ["default", "research"] });
118
+ const { srv, base } = serverWith(emptyChannels(), reg);
119
+ const res = await fetch(`${base}/api/vault/agent-def`, {
120
+ method: "POST",
121
+ headers: auth,
122
+ body: JSON.stringify({ event: "deleted", vault: "research", note: { id: "Agents/r" } }),
123
+ });
124
+ expect(res.status).toBe(200);
125
+ expect(reloads).toEqual([{ vault: "research", noteId: "Agents/r", event: "deleted" }]);
126
+ srv.stop();
127
+ });
128
+
129
+ test("multiple def-vaults + no explicit vault → 400 (ambiguous)", async () => {
130
+ const { reg, reloads } = recordingRegistry({ vaults: ["default", "research"] });
131
+ const { srv, base } = serverWith(emptyChannels(), reg);
132
+ const res = await fetch(`${base}/api/vault/agent-def`, {
133
+ method: "POST",
134
+ headers: auth,
135
+ body: JSON.stringify({ note: { id: "n" } }),
136
+ });
137
+ expect(res.status).toBe(400);
138
+ expect(reloads).toHaveLength(0);
139
+ srv.stop();
140
+ });
141
+
142
+ test("missing note.id → 400", async () => {
143
+ const { reg } = recordingRegistry();
144
+ const { srv, base } = serverWith(emptyChannels(), reg);
145
+ const res = await fetch(`${base}/api/vault/agent-def`, {
146
+ method: "POST",
147
+ headers: auth,
148
+ body: JSON.stringify({ event: "created" }),
149
+ });
150
+ expect(res.status).toBe(400);
151
+ srv.stop();
152
+ });
153
+
154
+ test("no agentDefs configured → clean no-op ack (200, reloaded: skipped)", async () => {
155
+ const { srv, base } = serverWith(emptyChannels()); // no registry
156
+ const res = await fetch(`${base}/api/vault/agent-def`, {
157
+ method: "POST",
158
+ headers: auth,
159
+ body: JSON.stringify({ note: { id: "n" } }),
160
+ });
161
+ expect(res.status).toBe(200);
162
+ const body = (await res.json()) as { ok: boolean; reloaded: string };
163
+ expect(body.reloaded).toBe("skipped");
164
+ srv.stop();
165
+ });
166
+ });