@openparachute/agent 0.1.2 → 0.2.0

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