@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/runner.ts ADDED
@@ -0,0 +1,255 @@
1
+ /**
2
+ * The runner — a scheduler that fires scheduled jobs (design
3
+ * `2026-06-17-runner-scheduled-agent-turns.md`).
4
+ *
5
+ * It does NOT execute anything. On each tick it loads the current jobs (from the
6
+ * VAULT-NATIVE store), asks each enabled job "are you due?" and, if so, FIRES it —
7
+ * where "fire" means "inject an inbound note onto the job's vault channel." The
8
+ * existing vault trigger → agent-turn → outbound flow does all the work. The
9
+ * runner is a clock that authors messages.
10
+ *
11
+ * Determinism is the design's hard requirement: the testable core takes an
12
+ * INJECTABLE clock (`now`), INJECTABLE load + fire + persist fns, and an
13
+ * INJECTABLE tick driver. The daemon's boot supplies the real ones (`Date`, the
14
+ * vault job store, the vault-inject fire, a `setInterval` tick); tests supply
15
+ * fakes and step time by hand. No real `setInterval`/`Date.now()` appears in
16
+ * `tick()` itself.
17
+ *
18
+ * Storage-agnostic: the runner never touches the vault directly. It calls:
19
+ * - `loadJobs()` → the current jobs (the store queries the vault),
20
+ * - `fire(job)` → inject the inbound note (the transport writes it),
21
+ * - `persistFire(job)` → write back lastRunAt/lastStatus (the store PATCHes).
22
+ *
23
+ * `nextRunAt` is COMPUTED IN MEMORY and NEVER persisted. The runner keeps a small
24
+ * per-job horizon map (keyed by id) across ticks so a job fires once per slot; a
25
+ * job seen for the first time gets a horizon computed from now (and won't fire
26
+ * until that horizon passes — so a freshly-created job never back-fires on its
27
+ * first tick).
28
+ *
29
+ * Catch-up policy = FIRE-ONCE-ON-MISS: if a job's horizon is already in the past
30
+ * on a tick (the daemon was down across one or more slots), the job fires ONCE and
31
+ * the horizon is recomputed forward from now — never replaying every missed slot.
32
+ *
33
+ * Idempotency under overlap: a job that's mid-fire (its async fire hasn't resolved)
34
+ * is SKIPPED on subsequent ticks, so a slow vault write can't be double-fired.
35
+ */
36
+
37
+ import { nextRunAfter } from "./cron.ts";
38
+ import type { Job } from "./jobs.ts";
39
+
40
+ /** Load the current jobs (the vault-native store queries the vault). Async. */
41
+ export type LoadJobsFn = () => Promise<Job[]>;
42
+
43
+ /** Fire a job: inject its message as an inbound note onto its channel. Async. */
44
+ export type FireFn = (job: Job) => Promise<void>;
45
+
46
+ /** Persist a job's bookkeeping (lastRunAt/lastStatus) after a fire. Async. */
47
+ export type PersistFireFn = (job: Job) => Promise<void>;
48
+
49
+ /** A scheduler driver: schedule `fn` to run every `ms`, return a cancel handle. */
50
+ export interface TickDriver {
51
+ schedule(fn: () => void, ms: number): { cancel: () => void };
52
+ }
53
+
54
+ export interface RunnerOptions {
55
+ /** Load the current jobs (queries the vault each tick). */
56
+ loadJobs: LoadJobsFn;
57
+ /** Fire a due job (inject the inbound note). */
58
+ fire: FireFn;
59
+ /** Persist a job's bookkeeping after a fire. */
60
+ persistFire: PersistFireFn;
61
+ /** Clock — injected for determinism. Default `() => new Date()`. */
62
+ now?: () => Date;
63
+ /** Tick driver — injected for determinism. Default a real-setInterval driver. */
64
+ driver?: TickDriver;
65
+ /** Tick interval (ms). Default 30s. */
66
+ intervalMs?: number;
67
+ /** Log sink (errors per job/tick never throw out). Default `console`. */
68
+ log?: { warn: (msg: string) => void; error: (msg: string) => void };
69
+ }
70
+
71
+ /** A real `setInterval`-backed tick driver (the daemon uses this; tests don't). */
72
+ export function realTickDriver(): TickDriver {
73
+ return {
74
+ schedule(fn, ms) {
75
+ const t = setInterval(fn, ms);
76
+ // Don't keep the process alive solely for the runner tick.
77
+ if (typeof t === "object" && t && "unref" in t) (t as { unref: () => void }).unref();
78
+ return { cancel: () => clearInterval(t) };
79
+ },
80
+ };
81
+ }
82
+
83
+ const DEFAULT_INTERVAL_MS = 30_000;
84
+
85
+ export class Runner {
86
+ private readonly loadJobs: LoadJobsFn;
87
+ private readonly fire: FireFn;
88
+ private readonly persistFire: PersistFireFn;
89
+ private readonly now: () => Date;
90
+ private readonly driver: TickDriver;
91
+ private readonly intervalMs: number;
92
+ private readonly log: { warn: (msg: string) => void; error: (msg: string) => void };
93
+
94
+ /**
95
+ * Per-job next-fire horizon (ISO), COMPUTED IN MEMORY and carried across ticks.
96
+ * Keyed by job id. Seeded the first time a job is seen; recomputed forward after
97
+ * each fire. Not persisted — the vault store doesn't carry nextRunAt.
98
+ */
99
+ private readonly horizons = new Map<string, string>();
100
+ /** Job ids currently mid-fire — skipped by an interleaving tick (overlap guard). */
101
+ private readonly inFlight = new Set<string>();
102
+ private handle: { cancel: () => void } | undefined;
103
+
104
+ constructor(opts: RunnerOptions) {
105
+ this.loadJobs = opts.loadJobs;
106
+ this.fire = opts.fire;
107
+ this.persistFire = opts.persistFire;
108
+ this.now = opts.now ?? (() => new Date());
109
+ this.driver = opts.driver ?? realTickDriver();
110
+ this.intervalMs = opts.intervalMs ?? DEFAULT_INTERVAL_MS;
111
+ this.log = opts.log ?? {
112
+ warn: (m) => console.warn(m),
113
+ error: (m) => console.error(m),
114
+ };
115
+ }
116
+
117
+ /**
118
+ * The stable per-job key for horizon + in-flight tracking. Prefer the vault
119
+ * `noteId` (globally unique across channels/vaults) so two jobs that share a
120
+ * SLUG in different channels don't collide; fall back to the slug `id` for an
121
+ * in-memory job that hasn't been persisted yet (tests).
122
+ */
123
+ private keyOf(job: Job): string {
124
+ return job.noteId ?? job.id;
125
+ }
126
+
127
+ /** Compute the next fire instant for a job after `from`, or null on a bad cron. */
128
+ private computeNext(job: Job, from: Date): Date | null {
129
+ try {
130
+ return nextRunAfter(job.schedule.cron, job.schedule.tz, from);
131
+ } catch (err) {
132
+ this.log.warn(`runner: job "${job.id}" has an unschedulable cron: ${(err as Error).message}`);
133
+ return null;
134
+ }
135
+ }
136
+
137
+ /** Start the periodic tick. Idempotent (a second start is a no-op). */
138
+ start(): void {
139
+ if (this.handle) return;
140
+ this.handle = this.driver.schedule(() => {
141
+ // A tick must never throw out (it'd kill the interval). `tick()` already
142
+ // guards; this is belt-and-suspenders for an unexpected throw.
143
+ void this.tick().catch((err) => this.log.error(`runner: tick failed: ${err}`));
144
+ }, this.intervalMs);
145
+ }
146
+
147
+ /** Stop the tick. Safe to call when not started. */
148
+ stop(): void {
149
+ this.handle?.cancel();
150
+ this.handle = undefined;
151
+ }
152
+
153
+ /**
154
+ * One scheduling pass. Loads the current jobs; for each enabled, not-in-flight
155
+ * job whose horizon is due (≤ now), fires it once and advances the horizon
156
+ * forward from now. A job seen for the first time gets a horizon computed (it
157
+ * won't fire this tick — it's in the future). Per-job failures are caught and
158
+ * recorded; one bad job never aborts the pass. A load failure is logged and the
159
+ * tick is a no-op (it retries next interval). Awaitable for deterministic tests.
160
+ */
161
+ async tick(): Promise<void> {
162
+ const at = this.now();
163
+ let jobs: Job[];
164
+ try {
165
+ jobs = await this.loadJobs();
166
+ } catch (err) {
167
+ this.log.error(`runner: loadJobs failed (skipping this tick): ${(err as Error).message}`);
168
+ return;
169
+ }
170
+
171
+ // Prune horizons for jobs that no longer exist (deleted), so the map can't grow.
172
+ const liveKeys = new Set(jobs.map((j) => this.keyOf(j)));
173
+ for (const key of [...this.horizons.keys()]) {
174
+ if (!liveKeys.has(key)) this.horizons.delete(key);
175
+ }
176
+
177
+ const fires: Array<Promise<void>> = [];
178
+ for (const job of jobs) {
179
+ if (!job.enabled) continue;
180
+ const key = this.keyOf(job);
181
+ if (this.inFlight.has(key)) continue; // overlap guard — already firing.
182
+
183
+ let horizon = this.horizons.get(key);
184
+ if (!horizon) {
185
+ // First time we've seen this job — seed a horizon from now. Future → not
186
+ // due this tick (a freshly-created job never back-fires on first sight).
187
+ const next = this.computeNext(job, at);
188
+ if (!next) continue; // unschedulable cron — skip (logged in computeNext).
189
+ horizon = next.toISOString();
190
+ this.horizons.set(key, horizon);
191
+ job.nextRunAt = horizon;
192
+ continue;
193
+ }
194
+
195
+ job.nextRunAt = horizon;
196
+ if (new Date(horizon).getTime() <= at.getTime()) {
197
+ fires.push(this.fireOne(job, at));
198
+ }
199
+ }
200
+ await Promise.allSettled(fires);
201
+ }
202
+
203
+ /**
204
+ * Fire one due job: mark in-flight, inject, record bookkeeping, recompute the
205
+ * horizon FORWARD FROM NOW (fire-once-on-miss — never replay missed slots),
206
+ * persist the bookkeeping. Never throws — a fire failure is recorded as
207
+ * `lastStatus: "error: …"` and still advances the horizon (so it retries the
208
+ * next slot rather than getting stuck).
209
+ */
210
+ private async fireOne(job: Job, at: Date): Promise<void> {
211
+ const key = this.keyOf(job);
212
+ this.inFlight.add(key);
213
+ try {
214
+ await this.fire(job);
215
+ job.lastStatus = "ok";
216
+ } catch (err) {
217
+ job.lastStatus = `error: ${(err as Error).message}`;
218
+ this.log.error(`runner: job "${job.id}" fire failed: ${(err as Error).message}`);
219
+ } finally {
220
+ job.lastRunAt = at.toISOString();
221
+ // Recompute the horizon from NOW (not the missed slot) — fire-once-on-miss.
222
+ const next = this.computeNext(job, at);
223
+ if (next) {
224
+ this.horizons.set(key, next.toISOString());
225
+ job.nextRunAt = next.toISOString();
226
+ } else {
227
+ this.horizons.delete(key);
228
+ job.nextRunAt = undefined;
229
+ }
230
+ this.inFlight.delete(key);
231
+ // Persist the bookkeeping (lastRunAt/lastStatus) — best-effort.
232
+ try {
233
+ await this.persistFire(job);
234
+ } catch (err) {
235
+ this.log.warn(`runner: persist bookkeeping for "${job.id}" failed (continuing): ${(err as Error).message}`);
236
+ }
237
+ }
238
+ }
239
+
240
+ /**
241
+ * Fire a single job immediately, on demand (the "Run now" API). Loads jobs to
242
+ * find it, bypasses the schedule + due check but honors the overlap guard and
243
+ * records bookkeeping exactly like a scheduled fire. Returns the resulting
244
+ * `lastStatus`. Throws only if the job is unknown; a fire failure is recorded
245
+ * and returned, not thrown.
246
+ */
247
+ async runNow(id: string): Promise<string> {
248
+ const jobs = await this.loadJobs();
249
+ const job = jobs.find((j) => j.id === id);
250
+ if (!job) throw new Error(`runner: no job with id "${id}"`);
251
+ if (this.inFlight.has(this.keyOf(job))) return job.lastStatus ?? "already running";
252
+ await this.fireOne(job, this.now());
253
+ return job.lastStatus ?? "ok";
254
+ }
255
+ }
@@ -0,0 +1,150 @@
1
+ import { describe, test, expect } from "bun:test";
2
+ import { buildSandboxConfig } from "./config.ts";
3
+ import type { AgentSpec, BaseBinds } from "./types.ts";
4
+ import type { EgressBaseInput } from "./egress.ts";
5
+
6
+ const BASE_BINDS: BaseBinds = {
7
+ workspace: "/state/sessions/arm",
8
+ runtimeReadOnly: ["/home/op/.claude"],
9
+ };
10
+ const EGRESS_BASE: EgressBaseInput = { hubOrigin: "https://hub.example.com" };
11
+
12
+ // Most cases exercise the egress floor, which needs network "restricted". Scoped
13
+ // reads are the DEFAULT (filesystem "workspace"), so the helper only sets the
14
+ // network and leaves filesystem at its default. A spread `p` overrides (e.g.
15
+ // `filesystem: "full"` to test broad reads).
16
+ function specOf(p: Partial<AgentSpec> = {}): AgentSpec {
17
+ return { name: "arm", channels: ["ch"], network: "restricted", ...p };
18
+ }
19
+
20
+ describe("buildSandboxConfig — defaults (scoped reads + open network)", () => {
21
+ test("DEFAULT: scoped reads (home tree denied) + open network (no allowedDomains), writes confined", () => {
22
+ const cfg = buildSandboxConfig({
23
+ spec: { name: "arm", channels: ["ch"] }, // no filesystem/network → both defaults
24
+ baseBinds: BASE_BINDS,
25
+ egressBase: EGRESS_BASE,
26
+ platform: "darwin",
27
+ });
28
+ // Scoped reads by default: the home tree is DENIED — this is what keeps the
29
+ // operator's secrets (~/.parachute/operator.token, SSH keys) unreadable.
30
+ expect(cfg.filesystem.denyRead).toContain("/Users");
31
+ // Open network by default: allowedDomains omitted entirely (runtime = no restriction).
32
+ expect((cfg.network as { allowedDomains?: string[] }).allowedDomains).toBeUndefined();
33
+ // Writes confined to the workspace.
34
+ expect(cfg.filesystem.allowWrite).toContain("/state/sessions/arm");
35
+ });
36
+
37
+ test("filesystem 'full': broad reads (no home-tree deny), writes still confined", () => {
38
+ const cfg = buildSandboxConfig({
39
+ spec: { name: "arm", channels: ["ch"], filesystem: "full" },
40
+ baseBinds: BASE_BINDS,
41
+ egressBase: EGRESS_BASE,
42
+ platform: "darwin",
43
+ });
44
+ expect(cfg.filesystem.denyRead).toEqual([]);
45
+ expect(cfg.filesystem.allowWrite).toContain("/state/sessions/arm");
46
+ });
47
+ });
48
+
49
+ describe("buildSandboxConfig — spec → SandboxRuntimeConfig", () => {
50
+ test("network: deny-by-default + base floor present, deniedDomains empty", () => {
51
+ const cfg = buildSandboxConfig({
52
+ spec: specOf({ egress: [] }),
53
+ baseBinds: BASE_BINDS,
54
+ egressBase: EGRESS_BASE,
55
+ platform: "darwin",
56
+ });
57
+ expect(cfg.network.allowedDomains).toContain("api.anthropic.com");
58
+ expect(cfg.network.allowedDomains).toContain("hub.example.com");
59
+ expect(cfg.network.deniedDomains).toEqual([]);
60
+ });
61
+
62
+ test("SECURITY: a spec with foreign egress still carries the base floor", () => {
63
+ const cfg = buildSandboxConfig({
64
+ spec: specOf({ egress: ["registry.npmjs.org"] }),
65
+ baseBinds: BASE_BINDS,
66
+ egressBase: EGRESS_BASE,
67
+ platform: "darwin",
68
+ });
69
+ expect(cfg.network.allowedDomains).toContain("api.anthropic.com");
70
+ expect(cfg.network.allowedDomains).toContain("hub.example.com");
71
+ expect(cfg.network.allowedDomains).toContain("registry.npmjs.org");
72
+ });
73
+
74
+ test("filesystem: scoped reads (deny home tree, re-allow binds) + write confinement", () => {
75
+ const cfg = buildSandboxConfig({
76
+ spec: specOf({ mounts: [{ hostPath: "/proj", mountPath: "/work", mode: "rw" }] }),
77
+ baseBinds: BASE_BINDS,
78
+ egressBase: EGRESS_BASE,
79
+ platform: "darwin",
80
+ });
81
+ expect(cfg.filesystem.denyRead).toContain("/Users");
82
+ expect(cfg.filesystem.allowRead).toContain("/state/sessions/arm");
83
+ expect(cfg.filesystem.allowRead).toContain("/home/op/.claude");
84
+ expect(cfg.filesystem.allowRead).toContain("/proj");
85
+ expect(cfg.filesystem.allowWrite).toContain("/state/sessions/arm");
86
+ expect(cfg.filesystem.allowWrite).toContain("/proj");
87
+ });
88
+
89
+ test("Linux platform denies /home instead of /Users", () => {
90
+ const cfg = buildSandboxConfig({
91
+ spec: specOf(),
92
+ baseBinds: BASE_BINDS,
93
+ egressBase: EGRESS_BASE,
94
+ platform: "linux",
95
+ });
96
+ expect(cfg.filesystem.denyRead).toContain("/home");
97
+ expect(cfg.filesystem.denyRead).not.toContain("/Users");
98
+ });
99
+
100
+ test("allowPty defaults true (interactive claude needs a pty)", () => {
101
+ const cfg = buildSandboxConfig({
102
+ spec: specOf(),
103
+ baseBinds: BASE_BINDS,
104
+ egressBase: EGRESS_BASE,
105
+ platform: "darwin",
106
+ });
107
+ expect(cfg.allowPty).toBe(true);
108
+ });
109
+
110
+ test("ripgrep override threads through when provided", () => {
111
+ const cfg = buildSandboxConfig({
112
+ spec: specOf(),
113
+ baseBinds: BASE_BINDS,
114
+ egressBase: EGRESS_BASE,
115
+ platform: "darwin",
116
+ ripgrep: { command: "/abs/rg" },
117
+ });
118
+ expect(cfg.ripgrep).toEqual({ command: "/abs/rg" });
119
+ });
120
+
121
+ test("a restricted-network config carries the full runtime shape (allowedDomains present)", () => {
122
+ const cfg = buildSandboxConfig({
123
+ spec: specOf(), // network "restricted" → allowedDomains present
124
+ baseBinds: BASE_BINDS,
125
+ egressBase: EGRESS_BASE,
126
+ platform: "darwin",
127
+ });
128
+ expect(cfg.network).toHaveProperty("allowedDomains");
129
+ expect(cfg.network).toHaveProperty("deniedDomains");
130
+ expect(cfg.filesystem).toHaveProperty("denyRead");
131
+ expect(cfg.filesystem).toHaveProperty("allowRead");
132
+ expect(cfg.filesystem).toHaveProperty("allowWrite");
133
+ expect(cfg.filesystem).toHaveProperty("denyWrite");
134
+ });
135
+
136
+ test("an open-network config OMITS allowedDomains but keeps the rest of the shape", () => {
137
+ const cfg = buildSandboxConfig({
138
+ spec: { name: "arm", channels: ["ch"] }, // default → network open
139
+ baseBinds: BASE_BINDS,
140
+ egressBase: EGRESS_BASE,
141
+ platform: "darwin",
142
+ });
143
+ // allowedDomains is deliberately ABSENT on open (the runtime's allow-all shape);
144
+ // this is NOT a protocol guarantee that allowedDomains is always present.
145
+ expect(cfg.network).not.toHaveProperty("allowedDomains");
146
+ expect(cfg.network).toHaveProperty("deniedDomains");
147
+ expect(cfg.filesystem).toHaveProperty("denyRead");
148
+ expect(cfg.filesystem).toHaveProperty("allowWrite");
149
+ });
150
+ });
@@ -0,0 +1,102 @@
1
+ /**
2
+ * Map an agent-spec → a `SandboxRuntimeConfig` for `@anthropic-ai/sandbox-runtime`
3
+ * (design §3 — the isolation envelope).
4
+ *
5
+ * The runtime config shape (verified against the package's own types,
6
+ * `@anthropic-ai/sandbox-runtime` 0.0.54 `SandboxRuntimeConfig`):
7
+ *
8
+ * {
9
+ * network: { allowedDomains: string[], deniedDomains: string[] },
10
+ * filesystem: { denyRead: string[], allowRead?: string[],
11
+ * allowWrite: string[], denyWrite: string[] },
12
+ * …optional knobs (allowPty, bwrapPath, ripgrep, …)
13
+ * }
14
+ *
15
+ * This module is the single place spec → runtime-config happens, so the egress
16
+ * floor (§4.4) and scoped-read policy (§4.5) are guaranteed on every launch.
17
+ */
18
+
19
+ import type { SandboxRuntimeConfig } from "@anthropic-ai/sandbox-runtime";
20
+ import type { AgentSpec, BaseBinds, SandboxPlatform } from "./types.ts";
21
+ import { composeEgressAllowlist, type EgressBaseInput } from "./egress.ts";
22
+ import { composeFilesystemView } from "./mounts.ts";
23
+
24
+ export interface BuildSandboxConfigInput {
25
+ spec: AgentSpec;
26
+ /** Workspace + runtime/config binds the contract always grants. */
27
+ baseBinds: BaseBinds;
28
+ /** Origins for the non-removable egress base. */
29
+ egressBase: EgressBaseInput;
30
+ /** Target platform. Defaults to the running platform. */
31
+ platform?: SandboxPlatform;
32
+ /**
33
+ * Allow the session to allocate a pty (a tmux/interactive `claude` needs one).
34
+ * Defaults true. Surfaced so a non-interactive arm can drop it.
35
+ */
36
+ allowPty?: boolean;
37
+ /**
38
+ * Optional ripgrep override the runtime uses for deny-path scanning. On macOS
39
+ * the runtime needs a ripgrep binary; pass `{ command: <abs path> }` when the
40
+ * host has no `rg` on PATH. Omitted = the runtime's own resolution.
41
+ */
42
+ ripgrep?: { command: string; args?: string[] };
43
+ }
44
+
45
+ /** Resolve the running platform to the two we support. */
46
+ export function currentSandboxPlatform(): SandboxPlatform {
47
+ return process.platform === "darwin" ? "darwin" : "linux";
48
+ }
49
+
50
+ /**
51
+ * Build the `SandboxRuntimeConfig` for an agent spec. The egress allowlist is the
52
+ * non-removable base unioned with the spec's additions; the filesystem view is
53
+ * scoped-read (home-tree denied, binds re-allowed) with writes confined to the
54
+ * workspace + rw mounts.
55
+ */
56
+ export function buildSandboxConfig(input: BuildSandboxConfigInput): SandboxRuntimeConfig {
57
+ const platform = input.platform ?? currentSandboxPlatform();
58
+
59
+ // The two containment boundaries are INDEPENDENT (Anthropic's model). Each has a
60
+ // security-first default: reads scoped to the workspace, network open.
61
+ const scopedReads = input.spec.filesystem !== "full"; // default "workspace" = scoped
62
+ const restrictedNet = input.spec.network === "restricted"; // default "open"
63
+
64
+ // Filesystem: scoped reads (deny home tree, re-allow private home + claude
65
+ // runtime + the working dir + mounts) unless explicitly "full". Writes are
66
+ // confined to the private home + the working dir + rw mounts in BOTH cases. The
67
+ // scoped default is what keeps the operator's secrets (e.g.
68
+ // ~/.parachute/operator.token) unreadable even with the network open. The spec's
69
+ // `workspace` (the shared real dir the agent works from) is threaded in as an rw
70
+ // working-root — decoupled from the private home, so `.mcp.json`'s secrets stay
71
+ // per-agent even when the working dir is shared (design
72
+ // 2026-06-16-agent-filesystem-and-sharing.md).
73
+ const fs = composeFilesystemView(
74
+ input.baseBinds,
75
+ input.spec.mounts,
76
+ platform,
77
+ scopedReads,
78
+ input.spec.workspace,
79
+ );
80
+
81
+ // Network: "open" (default) OMITS `allowedDomains` — the runtime treats an absent
82
+ // allowedDomains as "no restriction" (verified: present-but-empty = block all,
83
+ // absent = allow all). "restricted" = the non-removable base UNIONed with the
84
+ // spec's additions. The cast is because the runtime's TS type marks allowedDomains
85
+ // required while its runtime honors the absent case as the documented allow-all.
86
+ const network: SandboxRuntimeConfig["network"] = restrictedNet
87
+ ? { allowedDomains: composeEgressAllowlist(input.egressBase, input.spec.egress), deniedDomains: [] }
88
+ : ({ deniedDomains: [] } as unknown as SandboxRuntimeConfig["network"]);
89
+
90
+ const config: SandboxRuntimeConfig = {
91
+ network,
92
+ filesystem: {
93
+ denyRead: fs.denyRead,
94
+ allowRead: fs.allowRead,
95
+ allowWrite: fs.allowWrite,
96
+ denyWrite: fs.denyWrite,
97
+ },
98
+ allowPty: input.allowPty ?? true,
99
+ };
100
+ if (input.ripgrep) config.ripgrep = input.ripgrep;
101
+ return config;
102
+ }
@@ -0,0 +1,113 @@
1
+ import { describe, test, expect } from "bun:test";
2
+ import {
3
+ ANTHROPIC_EGRESS_HOSTS,
4
+ baseEgressAllowlist,
5
+ composeEgressAllowlist,
6
+ hostFromOrigin,
7
+ type EgressBaseInput,
8
+ } from "./egress.ts";
9
+
10
+ const BASE: EgressBaseInput = { hubOrigin: "https://hub.example.com" };
11
+
12
+ describe("hostFromOrigin", () => {
13
+ test("reduces an origin to its hostname (strips scheme + port + path)", () => {
14
+ expect(hostFromOrigin("https://hub.example.com:1939/admin")).toBe("hub.example.com");
15
+ });
16
+ test("passes a bare host through", () => {
17
+ expect(hostFromOrigin("registry.npmjs.org")).toBe("registry.npmjs.org");
18
+ });
19
+ test("strips a :port from a bare host:port", () => {
20
+ expect(hostFromOrigin("127.0.0.1:1939")).toBe("127.0.0.1");
21
+ });
22
+ test("preserves loopback (a co-located dev hub is loopback)", () => {
23
+ expect(hostFromOrigin("http://127.0.0.1:1939")).toBe("127.0.0.1");
24
+ });
25
+ test("returns null for empty / nullish input", () => {
26
+ expect(hostFromOrigin("")).toBeNull();
27
+ expect(hostFromOrigin(undefined)).toBeNull();
28
+ expect(hostFromOrigin(" ")).toBeNull();
29
+ });
30
+ });
31
+
32
+ describe("baseEgressAllowlist — the non-removable base", () => {
33
+ test("always includes the Anthropic hosts + the hub host", () => {
34
+ const base = baseEgressAllowlist(BASE);
35
+ for (const h of ANTHROPIC_EGRESS_HOSTS) expect(base).toContain(h);
36
+ expect(base).toContain("hub.example.com");
37
+ });
38
+
39
+ test("includes a distinct vault host when given", () => {
40
+ const base = baseEgressAllowlist({ ...BASE, vaultOrigin: "https://vault.example.com" });
41
+ expect(base).toContain("vault.example.com");
42
+ });
43
+
44
+ test("dedupes a vault origin equal to the hub origin", () => {
45
+ const base = baseEgressAllowlist({
46
+ hubOrigin: "https://h.example.com",
47
+ vaultOrigin: "https://h.example.com",
48
+ });
49
+ expect(base.filter((h) => h === "h.example.com")).toHaveLength(1);
50
+ });
51
+ });
52
+
53
+ describe("composeEgressAllowlist — base floor is non-removable, spec is additive", () => {
54
+ test("an empty spec egress still gets the full base (weaver-style arm)", () => {
55
+ const allow = composeEgressAllowlist(BASE, []);
56
+ for (const h of ANTHROPIC_EGRESS_HOSTS) expect(allow).toContain(h);
57
+ expect(allow).toContain("hub.example.com");
58
+ });
59
+
60
+ test("an undefined spec egress still gets the full base", () => {
61
+ const allow = composeEgressAllowlist(BASE, undefined);
62
+ expect(allow).toContain("api.anthropic.com");
63
+ expect(allow).toContain("hub.example.com");
64
+ });
65
+
66
+ test("spec hosts are ADDED on top of the base", () => {
67
+ const allow = composeEgressAllowlist(BASE, ["registry.npmjs.org", "pypi.org"]);
68
+ // base present...
69
+ expect(allow).toContain("api.anthropic.com");
70
+ expect(allow).toContain("hub.example.com");
71
+ // ...plus the additions
72
+ expect(allow).toContain("registry.npmjs.org");
73
+ expect(allow).toContain("pypi.org");
74
+ });
75
+
76
+ test("SECURITY: a spec that lists ONLY a foreign host CANNOT drop the base — the base is still present", () => {
77
+ // A spec authored to omit the base entirely (the malicious-omit case).
78
+ const allow = composeEgressAllowlist(BASE, ["evil.example.com"]);
79
+ // The base floor survives regardless of what the spec listed.
80
+ expect(allow).toContain("api.anthropic.com");
81
+ expect(allow).toContain("hub.example.com");
82
+ // The spec's own host is added (additive), not a replacement.
83
+ expect(allow).toContain("evil.example.com");
84
+ });
85
+
86
+ test("SECURITY: a spec cannot REPLACE the Anthropic host with a look-alike — both end up present, base is not dropped", () => {
87
+ // A spec that tries to "override" the Anthropic host by re-declaring a near-miss.
88
+ const allow = composeEgressAllowlist(BASE, ["api.anthropic.com.evil.example.com"]);
89
+ // The real Anthropic apex is still on the list (the base recomputed from code).
90
+ expect(allow).toContain("api.anthropic.com");
91
+ // The look-alike is just an additional (separate) host, not a substitution.
92
+ expect(allow).toContain("api.anthropic.com.evil.example.com");
93
+ // And the look-alike did not evict the real host.
94
+ expect(allow.indexOf("api.anthropic.com")).toBeGreaterThanOrEqual(0);
95
+ });
96
+
97
+ test("the base always sorts FIRST (recomputed from code, prepended)", () => {
98
+ const allow = composeEgressAllowlist(BASE, ["z-late.example.com"]);
99
+ expect(allow[0]).toBe("api.anthropic.com");
100
+ expect(allow[allow.length - 1]).toBe("z-late.example.com");
101
+ });
102
+
103
+ test("spec origins are normalized to hosts (full URL and bare host land the same)", () => {
104
+ const a = composeEgressAllowlist(BASE, ["https://registry.npmjs.org"]);
105
+ const b = composeEgressAllowlist(BASE, ["registry.npmjs.org"]);
106
+ expect(a).toEqual(b);
107
+ });
108
+
109
+ test("dedupes a spec host that duplicates a base host", () => {
110
+ const allow = composeEgressAllowlist(BASE, ["hub.example.com"]);
111
+ expect(allow.filter((h) => h === "hub.example.com")).toHaveLength(1);
112
+ });
113
+ });