@openparachute/agent 0.1.1 → 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 (598) 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 -221
  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 -171
  318. package/scripts/init-first-agent.ts +0 -377
  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 -79
  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/ask-question.ts +0 -46
  328. package/src/channels/channel-registry.test.ts +0 -421
  329. package/src/channels/channel-registry.ts +0 -313
  330. package/src/channels/chat-sdk-bridge.test.ts +0 -84
  331. package/src/channels/chat-sdk-bridge.ts +0 -652
  332. package/src/channels/cli.ts +0 -276
  333. package/src/channels/discord.ts +0 -90
  334. package/src/channels/index.ts +0 -17
  335. package/src/channels/telegram-markdown-sanitize.test.ts +0 -78
  336. package/src/channels/telegram-markdown-sanitize.ts +0 -55
  337. package/src/channels/telegram-pairing.test.ts +0 -254
  338. package/src/channels/telegram-pairing.ts +0 -339
  339. package/src/channels/telegram.ts +0 -279
  340. package/src/channels/trust-hint.test.ts +0 -48
  341. package/src/channels/trust-hint.ts +0 -75
  342. package/src/claude-md-compose.migrate.test.ts +0 -64
  343. package/src/claude-md-compose.ts +0 -205
  344. package/src/command-gate.ts +0 -63
  345. package/src/config.test.ts +0 -93
  346. package/src/config.ts +0 -108
  347. package/src/container-config.ts +0 -167
  348. package/src/container-runner.test.ts +0 -32
  349. package/src/container-runner.ts +0 -576
  350. package/src/container-runtime.test.ts +0 -169
  351. package/src/container-runtime.ts +0 -92
  352. package/src/db/_bun-sqlite-shim.ts +0 -88
  353. package/src/db/agent-activity.test.ts +0 -155
  354. package/src/db/agent-activity.ts +0 -121
  355. package/src/db/agent-groups.ts +0 -77
  356. package/src/db/connection.migrate.test.ts +0 -143
  357. package/src/db/connection.ts +0 -224
  358. package/src/db/db-v2.test.ts +0 -440
  359. package/src/db/dropped-messages.ts +0 -44
  360. package/src/db/index.ts +0 -40
  361. package/src/db/messaging-groups.ts +0 -252
  362. package/src/db/migrations/001-initial.ts +0 -112
  363. package/src/db/migrations/002-chat-sdk-state.ts +0 -36
  364. package/src/db/migrations/008-dropped-messages.ts +0 -27
  365. package/src/db/migrations/009-drop-pending-credentials.ts +0 -13
  366. package/src/db/migrations/010-engage-modes.ts +0 -103
  367. package/src/db/migrations/011-pending-sender-approvals.ts +0 -40
  368. package/src/db/migrations/012-channel-registration.ts +0 -48
  369. package/src/db/migrations/013-approval-render-metadata.ts +0 -27
  370. package/src/db/migrations/014-secrets.ts +0 -44
  371. package/src/db/migrations/015-secrets-drop-host-pattern.ts +0 -18
  372. package/src/db/migrations/016-secret-assignments.ts +0 -30
  373. package/src/db/migrations/017-agent-activity.ts +0 -40
  374. package/src/db/migrations/018-oauth-app-configs.ts +0 -34
  375. package/src/db/migrations/019-oauth-app-connections.ts +0 -48
  376. package/src/db/migrations/020-agent-app-connections.ts +0 -28
  377. package/src/db/migrations/021-pending-oauth-states.ts +0 -35
  378. package/src/db/migrations/022-app-connections-provider.ts +0 -25
  379. package/src/db/migrations/023-agent-group-secret-mode.test.ts +0 -124
  380. package/src/db/migrations/023-agent-group-secret-mode.ts +0 -65
  381. package/src/db/migrations/024-collapse-approvals.test.ts +0 -249
  382. package/src/db/migrations/024-collapse-approvals.ts +0 -182
  383. package/src/db/migrations/025-secret-mode-check.test.ts +0 -155
  384. package/src/db/migrations/025-secret-mode-check.ts +0 -49
  385. package/src/db/migrations/026-user-dms-bot-id.test.ts +0 -116
  386. package/src/db/migrations/026-user-dms-bot-id.ts +0 -54
  387. package/src/db/migrations/027-provider-credentials.ts +0 -41
  388. package/src/db/migrations/_test-helpers.ts +0 -41
  389. package/src/db/migrations/index.ts +0 -127
  390. package/src/db/migrations/module-agent-to-agent-destinations.ts +0 -84
  391. package/src/db/migrations/module-approvals-pending-approvals.ts +0 -42
  392. package/src/db/migrations/module-approvals-title-options.ts +0 -40
  393. package/src/db/schema.ts +0 -258
  394. package/src/db/session-db.test.ts +0 -93
  395. package/src/db/session-db.ts +0 -325
  396. package/src/db/sessions.ts +0 -241
  397. package/src/delivery.test.ts +0 -148
  398. package/src/delivery.ts +0 -445
  399. package/src/env.ts +0 -74
  400. package/src/group-folder.test.ts +0 -35
  401. package/src/group-folder.ts +0 -44
  402. package/src/group-init.ts +0 -92
  403. package/src/host-core.test.ts +0 -456
  404. package/src/host-sweep.test.ts +0 -146
  405. package/src/host-sweep.ts +0 -287
  406. package/src/index.ts +0 -227
  407. package/src/install-slug.ts +0 -33
  408. package/src/log.test.ts +0 -81
  409. package/src/log.ts +0 -117
  410. package/src/mcp/http.ts +0 -72
  411. package/src/mcp/server.ts +0 -92
  412. package/src/mcp/stdio.ts +0 -51
  413. package/src/mcp/tools/activity.ts +0 -88
  414. package/src/mcp/tools/agent-groups.ts +0 -183
  415. package/src/mcp/tools/approvals.ts +0 -122
  416. package/src/mcp/tools/channels.ts +0 -199
  417. package/src/mcp/tools/index.ts +0 -27
  418. package/src/mcp/tools/oauth.ts +0 -48
  419. package/src/mcp/tools/secrets.ts +0 -169
  420. package/src/mcp/tools/sessions.ts +0 -135
  421. package/src/mcp/types.ts +0 -51
  422. package/src/modules/agent-to-agent/agent-route.test.ts +0 -46
  423. package/src/modules/agent-to-agent/agent-route.ts +0 -223
  424. package/src/modules/agent-to-agent/create-agent.ts +0 -127
  425. package/src/modules/agent-to-agent/db/agent-destinations.ts +0 -135
  426. package/src/modules/agent-to-agent/index.ts +0 -22
  427. package/src/modules/agent-to-agent/write-destinations.ts +0 -59
  428. package/src/modules/approvals/agent.md +0 -45
  429. package/src/modules/approvals/index.ts +0 -21
  430. package/src/modules/approvals/picks.test.ts +0 -291
  431. package/src/modules/approvals/primitive.ts +0 -279
  432. package/src/modules/approvals/project.md +0 -27
  433. package/src/modules/approvals/response-handler.ts +0 -87
  434. package/src/modules/index.ts +0 -24
  435. package/src/modules/interactive/agent.md +0 -21
  436. package/src/modules/interactive/index.ts +0 -69
  437. package/src/modules/interactive/project.md +0 -12
  438. package/src/modules/mount-security/index.ts +0 -448
  439. package/src/modules/mount-security/migrate.test.ts +0 -91
  440. package/src/modules/permissions/access.ts +0 -28
  441. package/src/modules/permissions/channel-approval.test.ts +0 -389
  442. package/src/modules/permissions/channel-approval.ts +0 -188
  443. package/src/modules/permissions/db/agent-group-members.ts +0 -44
  444. package/src/modules/permissions/db/pending-channel-approvals.test.ts +0 -86
  445. package/src/modules/permissions/db/pending-channel-approvals.ts +0 -66
  446. package/src/modules/permissions/db/pending-sender-approvals.ts +0 -60
  447. package/src/modules/permissions/db/user-dms.ts +0 -58
  448. package/src/modules/permissions/db/user-roles.ts +0 -85
  449. package/src/modules/permissions/db/users.ts +0 -38
  450. package/src/modules/permissions/index.ts +0 -421
  451. package/src/modules/permissions/permissions.test.ts +0 -358
  452. package/src/modules/permissions/sender-approval.test.ts +0 -470
  453. package/src/modules/permissions/sender-approval.ts +0 -165
  454. package/src/modules/permissions/user-dm.ts +0 -200
  455. package/src/modules/provider-credentials/db.ts +0 -121
  456. package/src/modules/provider-credentials/index.ts +0 -12
  457. package/src/modules/provider-credentials/spawn.test.ts +0 -206
  458. package/src/modules/provider-credentials/spawn.ts +0 -114
  459. package/src/modules/scheduling/actions.ts +0 -113
  460. package/src/modules/scheduling/db.test.ts +0 -282
  461. package/src/modules/scheduling/db.ts +0 -148
  462. package/src/modules/scheduling/index.ts +0 -34
  463. package/src/modules/scheduling/recurrence.test.ts +0 -98
  464. package/src/modules/scheduling/recurrence.ts +0 -54
  465. package/src/modules/self-mod/agent.md +0 -30
  466. package/src/modules/self-mod/apply.ts +0 -85
  467. package/src/modules/self-mod/index.ts +0 -30
  468. package/src/modules/self-mod/project.md +0 -39
  469. package/src/modules/self-mod/request.ts +0 -91
  470. package/src/modules/typing/index.ts +0 -165
  471. package/src/oauth/agent-app-connections.ts +0 -103
  472. package/src/oauth/app-configs.test.ts +0 -64
  473. package/src/oauth/app-configs.ts +0 -114
  474. package/src/oauth/app-connections.test.ts +0 -109
  475. package/src/oauth/app-connections.ts +0 -178
  476. package/src/oauth/crypto.ts +0 -56
  477. package/src/oauth/flow.ts +0 -104
  478. package/src/oauth/providers/google.test.ts +0 -38
  479. package/src/oauth/providers/google.ts +0 -46
  480. package/src/oauth/providers/index.ts +0 -48
  481. package/src/oauth/state-store.test.ts +0 -54
  482. package/src/oauth/state-store.ts +0 -93
  483. package/src/parachute/README.md +0 -27
  484. package/src/parachute/create-agent.test.ts +0 -83
  485. package/src/parachute/create-agent.ts +0 -122
  486. package/src/parachute/group-status.test.ts +0 -165
  487. package/src/parachute/group-status.ts +0 -136
  488. package/src/parachute/types.ts +0 -41
  489. package/src/parachute/vault-mcp.test.ts +0 -251
  490. package/src/parachute/vault-mcp.ts +0 -232
  491. package/src/platform-id.test.ts +0 -104
  492. package/src/platform-id.ts +0 -109
  493. package/src/providers/index.ts +0 -6
  494. package/src/providers/provider-container-registry.ts +0 -58
  495. package/src/response-registry.ts +0 -45
  496. package/src/router.ts +0 -530
  497. package/src/secrets/crypto.test.ts +0 -45
  498. package/src/secrets/crypto.ts +0 -55
  499. package/src/secrets/index.ts +0 -355
  500. package/src/secrets/master-key.ts +0 -70
  501. package/src/secrets/secrets.test.ts +0 -354
  502. package/src/session-manager.migrate.test.ts +0 -59
  503. package/src/session-manager.ts +0 -433
  504. package/src/startup-bootstrap.test.ts +0 -226
  505. package/src/startup-bootstrap.ts +0 -207
  506. package/src/state-sqlite.ts +0 -182
  507. package/src/timezone.test.ts +0 -64
  508. package/src/timezone.ts +0 -37
  509. package/src/types.ts +0 -230
  510. package/src/web/auth.test.ts +0 -335
  511. package/src/web/auth.ts +0 -214
  512. package/src/web/discord-validate.test.ts +0 -77
  513. package/src/web/discord-validate.ts +0 -88
  514. package/src/web/hub-discovery.test.ts +0 -98
  515. package/src/web/hub-discovery.ts +0 -69
  516. package/src/web/routes/activity.ts +0 -106
  517. package/src/web/routes/agent-provider.test.ts +0 -282
  518. package/src/web/routes/agent-provider.ts +0 -309
  519. package/src/web/routes/approvals.ts +0 -185
  520. package/src/web/routes/apps.ts +0 -434
  521. package/src/web/routes/channels-mg-detail.test.ts +0 -324
  522. package/src/web/routes/channels-mga-detail.test.ts +0 -425
  523. package/src/web/routes/channels.ts +0 -489
  524. package/src/web/routes/oauth-providers.ts +0 -42
  525. package/src/web/routes/secrets.test.ts +0 -175
  526. package/src/web/routes/secrets.ts +0 -282
  527. package/src/web/routes/sessions.ts +0 -123
  528. package/src/web/routes/settings.test.ts +0 -106
  529. package/src/web/routes/settings.ts +0 -247
  530. package/src/web/routes/setup-status.ts +0 -205
  531. package/src/web/routes/vaults.test.ts +0 -389
  532. package/src/web/routes/vaults.ts +0 -225
  533. package/src/web/server-version.test.ts +0 -16
  534. package/src/web/server.ts +0 -1003
  535. package/src/web/services-manifest.test.ts +0 -120
  536. package/src/web/services-manifest.ts +0 -61
  537. package/src/web/static-serve.test.ts +0 -255
  538. package/src/web/static-serve.ts +0 -104
  539. package/src/web/telegram-validate.test.ts +0 -116
  540. package/src/web/telegram-validate.ts +0 -107
  541. package/src/web/vault-proxy.test.ts +0 -214
  542. package/src/web/vault-proxy.ts +0 -120
  543. package/src/web/wire-channel.ts +0 -181
  544. package/src/webhook-server.ts +0 -134
  545. package/vitest.config.ts +0 -18
  546. package/web/README.md +0 -63
  547. package/web/ui/index.html +0 -13
  548. package/web/ui/package.json +0 -35
  549. package/web/ui/pnpm-lock.yaml +0 -2164
  550. package/web/ui/scripts/verify-base.mjs +0 -31
  551. package/web/ui/src/App.tsx +0 -88
  552. package/web/ui/src/components/ActivityFeed.tsx +0 -444
  553. package/web/ui/src/components/AgentGroupPicker.tsx +0 -263
  554. package/web/ui/src/components/AgentProviderCards.tsx +0 -220
  555. package/web/ui/src/components/CredentialForm.tsx +0 -214
  556. package/web/ui/src/components/ScopeGrants.tsx +0 -74
  557. package/web/ui/src/components/StatusDot.tsx +0 -43
  558. package/web/ui/src/components/VaultPicker.tsx +0 -127
  559. package/web/ui/src/components/setup/AdapterInstallStep.tsx +0 -178
  560. package/web/ui/src/components/setup/AgentGroupStep.tsx +0 -43
  561. package/web/ui/src/components/setup/ChannelPickStep.tsx +0 -74
  562. package/web/ui/src/components/setup/DoneStep.tsx +0 -49
  563. package/web/ui/src/components/setup/PrereqStep.tsx +0 -129
  564. package/web/ui/src/components/setup/TestConnectionStep.tsx +0 -108
  565. package/web/ui/src/components/setup/TestMessageStep.tsx +0 -104
  566. package/web/ui/src/components/setup/WireChannelStep.tsx +0 -166
  567. package/web/ui/src/components/setup/types.ts +0 -105
  568. package/web/ui/src/lib/api.test.ts +0 -410
  569. package/web/ui/src/lib/api.ts +0 -1210
  570. package/web/ui/src/lib/auth.test.ts +0 -139
  571. package/web/ui/src/lib/auth.ts +0 -348
  572. package/web/ui/src/lib/channel-adapters.ts +0 -136
  573. package/web/ui/src/main.tsx +0 -19
  574. package/web/ui/src/routes/ApprovalsList.tsx +0 -294
  575. package/web/ui/src/routes/Apps.tsx +0 -613
  576. package/web/ui/src/routes/ChannelWireDetail.test.tsx +0 -233
  577. package/web/ui/src/routes/ChannelWireDetail.tsx +0 -403
  578. package/web/ui/src/routes/ChannelsList.tsx +0 -158
  579. package/web/ui/src/routes/GroupDetail.tsx +0 -755
  580. package/web/ui/src/routes/GroupList.tsx +0 -187
  581. package/web/ui/src/routes/MessagingGroupDetail.test.tsx +0 -233
  582. package/web/ui/src/routes/MessagingGroupDetail.tsx +0 -306
  583. package/web/ui/src/routes/NewGroupWizard.tsx +0 -390
  584. package/web/ui/src/routes/OAuthCallback.tsx +0 -56
  585. package/web/ui/src/routes/SecretsList.tsx +0 -921
  586. package/web/ui/src/routes/SessionsList.tsx +0 -220
  587. package/web/ui/src/routes/SettingsAgentProvider.tsx +0 -109
  588. package/web/ui/src/routes/SettingsApprovals.tsx +0 -234
  589. package/web/ui/src/routes/SetupWizard.tsx +0 -219
  590. package/web/ui/src/routes/VaultDetail.test.tsx +0 -361
  591. package/web/ui/src/routes/VaultDetail.tsx +0 -960
  592. package/web/ui/src/routes/VaultsList.tsx +0 -295
  593. package/web/ui/src/routes/WireChannelPage.tsx +0 -413
  594. package/web/ui/src/styles.css +0 -608
  595. package/web/ui/src/test/setup.ts +0 -23
  596. package/web/ui/src/vite-env.d.ts +0 -10
  597. package/web/ui/vite.config.ts +0 -34
  598. package/web/ui/vitest.config.ts +0 -25
package/src/grants.ts ADDED
@@ -0,0 +1,675 @@
1
+ /**
2
+ * Agent connectors — approval-gated cross-resource GRANTS (design
3
+ * 2026-06-17-agent-connectors-4b.md, slice 4b-1).
4
+ *
5
+ * 4a gave an agent its OWN def-vault. 4b lets a `#agent/definition` note DECLARE
6
+ * what it wants to reach BEYOND that — other local vaults, external services
7
+ * (GitHub, Cloudflare), and (parsed-but-deferred to 4b-2) remote MCP/OAuth servers.
8
+ * Every extra reach is OPERATOR-APPROVED in the hub; every secret stays in the hub's
9
+ * grant store; the agent module is the CONSUMER that fetches approved material at
10
+ * spawn + injects it into the ephemeral per-spawn `.mcp.json` + env.
11
+ *
12
+ * THE ONE INVARIANT (design §"The one invariant"): a vault note can only REQUEST, it
13
+ * can never GRANT. This module:
14
+ * - parses the note's `wants:` into structured {@link ConnectionSpec}s ({@link parseWants});
15
+ * - REGISTERS each as a PENDING grant with the hub (`PUT /admin/grants`) — that is
16
+ * the request, not a grant; worst case it sits `pending`;
17
+ * - at spawn, fetches only APPROVED grants' MATERIAL (`GET …/material`) and injects
18
+ * it. Unapproved/pending/error connections are simply ABSENT — never a failure.
19
+ *
20
+ * THE WIRE CONTRACT (parachute-hub PR #668 + #96 — consume, do not redesign):
21
+ * - PUT <hub>/admin/grants { agent, connection } → { id, agent, connection, status, reason? }
22
+ * - GET <hub>/admin/grants?agent=<> → { grants: [{ id, agent, connection, status, reason?, approvedAt? }] }
23
+ * - GET <hub>/admin/grants/<id>/material → APPROVED only:
24
+ * vault → { kind:"vault", token, mcpUrl }
25
+ * service → { kind:"service", token, inject }
26
+ * (404 unknown id / 409 not-approved)
27
+ * - POST <hub>/admin/grants/reconcile { agent, liveConnections } → { pruned, prunedIds } (#96
28
+ * grant-GC): the hub re-derives each key with ITS connectionKey and tears down +
29
+ * REMOVEs every grant for `agent` whose key is NOT among the live specs (empty
30
+ * liveConnections = the def is gone → prune ALL). Stops a removed
31
+ * want / a deleted def from orphaning a live approved grant. Pruning only ever
32
+ * REMOVES access, so it shares the host-admin Bearer (never an operator cookie).
33
+ * - Auth: all of these need a `parachute:host:admin` Bearer — we REUSE the module's
34
+ * existing host-admin-capable MANAGER BEARER (the operator token it mints vault
35
+ * tokens with; see mint-token.ts / spawn-deps.ts). NO new auth path.
36
+ * - approve/revoke are operator-only via the hub UI — the module NEVER calls those.
37
+ *
38
+ * SECURITY: grant material is SECRET (tokens). It only ever lands in the ephemeral,
39
+ * 0600 per-spawn `.mcp.json` + the child env — NEVER in a vault note. Material is
40
+ * fetched FRESH each spawn (design: revocation takes effect on the next spawn), so we
41
+ * deliberately do NOT cache it.
42
+ */
43
+
44
+ import { DENYLISTED_ENV } from "./credentials.ts";
45
+
46
+ // ---------------------------------------------------------------------------
47
+ // Connection spec — the structured form of one `wants:` entry
48
+ // ---------------------------------------------------------------------------
49
+
50
+ /**
51
+ * A declared connection beyond the def-vault. Matches the hub's `connection` spec
52
+ * shape exactly (design §"The hub grants API", connection spec):
53
+ * `{ kind, target, access?, tags?, inject? }`
54
+ * — `access`/`tags` are vault-only; `inject` is service-only (`("env"|"mcp")[]`).
55
+ */
56
+ export interface ConnectionSpec {
57
+ /** Resource kind. `vault`/`service` are wired in 4b-1; `mcp` is parsed-but-deferred. */
58
+ kind: "vault" | "service" | "mcp";
59
+ /**
60
+ * The resource target — a vault name (`research`), a service name (`github`),
61
+ * or, for `kind:"mcp"`, the remote MCP https URL.
62
+ */
63
+ target: string;
64
+ /** Vault access verb. Vault-only. */
65
+ access?: "read" | "write";
66
+ /** Vault tag-scope (one or more `#tag`). Vault-only. */
67
+ tags?: string[];
68
+ /** Injection shape(s) for a service credential. Service-only. */
69
+ inject?: ("env" | "mcp")[];
70
+ }
71
+
72
+ /** A malformed `wants:` entry — the whole def is an error (no half-instantiate). */
73
+ export class WantsParseError extends Error {
74
+ constructor(message: string) {
75
+ super(message);
76
+ this.name = "WantsParseError";
77
+ }
78
+ }
79
+
80
+ /**
81
+ * A STABLE, canonical key for a connection — the (agent, connection) grant key the
82
+ * status `pending:[…]` list reports + the hub upserts on. Derived purely from the
83
+ * spec so a re-parse of the same `wants:` yields the same key (idempotent upsert).
84
+ *
85
+ * vault → `vault:<target>:<access>[#tag…]` (tags sorted for stability)
86
+ * service → `<inject-joined>:<target>` e.g. `env+mcp:github`
87
+ * mcp → `mcp:<url>`
88
+ */
89
+ export function connectionKey(c: ConnectionSpec): string {
90
+ if (c.kind === "vault") {
91
+ const tags = c.tags && c.tags.length > 0 ? [...c.tags].sort().join("") : "";
92
+ return `vault:${c.target}:${c.access ?? "read"}${tags}`;
93
+ }
94
+ if (c.kind === "service") {
95
+ const inject = (c.inject && c.inject.length > 0 ? [...c.inject].sort() : ["env"]).join("+");
96
+ return `${inject}:${c.target}`;
97
+ }
98
+ return `mcp:${c.target}`;
99
+ }
100
+
101
+ /** A vault name slug — the `<name>` segment in `vault:<name>:<verb>`. */
102
+ const VAULT_NAME_SLUG = /^[a-zA-Z0-9_-]+$/;
103
+ /** A service name slug — `github`, `cloudflare`, … */
104
+ const SERVICE_NAME_SLUG = /^[a-zA-Z0-9_-]+$/;
105
+
106
+ /**
107
+ * Parse the `wants:` metadata field — a comma-separated list of connection specs —
108
+ * into structured {@link ConnectionSpec}s. PURE, no I/O.
109
+ *
110
+ * Spec forms (design §"The connection declaration"):
111
+ * - `vault:<name>:<read|write>` with optional `#tag` suffix(es):
112
+ * `vault:research:read` → {kind:"vault", target:"research", access:"read"}
113
+ * `vault:research:read#published#wip`→ {…, tags:["#published","#wip"]}
114
+ * (the agent's OWN def-vault is implicit — never in `wants:`; the def-vault binding
115
+ * drives `spec.vault`, not this.)
116
+ * - `env:<service>` → {kind:"service", target:"<service>", inject:["env"]}
117
+ * `mcp:<service>` → {kind:"service", target:"<service>", inject:["mcp"]}
118
+ * BOTH for the same service MERGE → inject:["env","mcp"].
119
+ * - `mcp:<https-url>`→ {kind:"mcp", target:"<url>"} (parsed; deferred to 4b-2).
120
+ *
121
+ * Disambiguation of `mcp:<x>`: an `<x>` that starts with `http://` or `https://` is
122
+ * a remote MCP (`kind:"mcp"`); otherwise it's a service MCP-injection (`mcp:github`).
123
+ *
124
+ * Accepts a real array OR the comma/space-joined string the vault stringifies arrays
125
+ * into. A MALFORMED entry throws {@link WantsParseError} — the caller stamps the def
126
+ * `status:error` rather than half-instantiating (design §1 "a malformed `wants:` →
127
+ * the def is an error").
128
+ */
129
+ export function parseWants(raw: unknown): ConnectionSpec[] {
130
+ const entries = toEntries(raw);
131
+ if (entries.length === 0) return [];
132
+
133
+ // Service connections accumulate by target so `env:github` + `mcp:github` merge to
134
+ // one connection with inject:["env","mcp"] (design §1). Keyed by service target.
135
+ const services = new Map<string, Set<"env" | "mcp">>();
136
+ // Insertion order of services (Map preserves it, but we re-emit at the end so the
137
+ // overall ordering = first-seen across all kinds).
138
+ const out: ConnectionSpec[] = [];
139
+ // Placeholder index per service so the merged service lands at its first position.
140
+ const servicePos = new Map<string, number>();
141
+
142
+ for (const entry of entries) {
143
+ const spec = parseOneWant(entry);
144
+ if (spec.kind === "service") {
145
+ const modes = services.get(spec.target);
146
+ if (modes) {
147
+ for (const m of spec.inject ?? []) modes.add(m);
148
+ continue; // already placeheld at first position
149
+ }
150
+ const set = new Set<"env" | "mcp">(spec.inject ?? []);
151
+ services.set(spec.target, set);
152
+ servicePos.set(spec.target, out.length);
153
+ out.push(spec); // placeholder; finalized below with the merged inject
154
+ continue;
155
+ }
156
+ out.push(spec);
157
+ }
158
+
159
+ // Finalize each service connection's merged inject (stable order: env before mcp).
160
+ for (const [target, modes] of services) {
161
+ const pos = servicePos.get(target)!;
162
+ const inject = (["env", "mcp"] as const).filter((m) => modes.has(m));
163
+ out[pos] = { kind: "service", target, inject };
164
+ }
165
+
166
+ return out;
167
+ }
168
+
169
+ /** Coerce a `wants:` metadata value (array or comma/space string) → clean entries. */
170
+ function toEntries(raw: unknown): string[] {
171
+ let parts: string[] = [];
172
+ if (Array.isArray(raw)) {
173
+ parts = raw.map((x) => (typeof x === "string" ? x : String(x)));
174
+ } else if (typeof raw === "string") {
175
+ parts = raw.split(/[,\s]+/);
176
+ } else if (raw === undefined || raw === null) {
177
+ return [];
178
+ } else {
179
+ parts = [String(raw)];
180
+ }
181
+ return parts.map((s) => s.trim()).filter((s) => s.length > 0);
182
+ }
183
+
184
+ /** Parse ONE `wants:` entry string. Throws {@link WantsParseError} on a malformed one. */
185
+ function parseOneWant(entry: string): ConnectionSpec {
186
+ const colon = entry.indexOf(":");
187
+ if (colon < 0) {
188
+ throw new WantsParseError(
189
+ `wants: "${entry}" is malformed — expected "<kind>:<target>…" ` +
190
+ `(e.g. "vault:research:read", "env:github", "mcp:https://…").`,
191
+ );
192
+ }
193
+ const prefix = entry.slice(0, colon);
194
+ const rest = entry.slice(colon + 1);
195
+
196
+ switch (prefix) {
197
+ case "vault":
198
+ return parseVaultWant(entry, rest);
199
+ case "env":
200
+ return parseServiceWant(entry, rest, "env");
201
+ case "mcp":
202
+ // `mcp:` is overloaded: a remote-MCP URL (kind:"mcp", 4b-2) vs a service MCP
203
+ // injection (kind:"service", inject:["mcp"], 4b-1). An http(s) target is the URL form.
204
+ if (/^https?:\/\//i.test(rest)) return parseMcpUrlWant(entry, rest);
205
+ return parseServiceWant(entry, rest, "mcp");
206
+ default:
207
+ throw new WantsParseError(
208
+ `wants: "${entry}" has unknown kind "${prefix}" — expected one of ` +
209
+ `vault | env | mcp.`,
210
+ );
211
+ }
212
+ }
213
+
214
+ /** Parse `vault:<name>:<read|write>[#tag…]`. */
215
+ function parseVaultWant(entry: string, rest: string): ConnectionSpec {
216
+ // rest = "<name>:<verb>[#tag…]". Split on the FIRST colon (name has no colon).
217
+ const colon = rest.indexOf(":");
218
+ if (colon < 0) {
219
+ throw new WantsParseError(
220
+ `wants: "${entry}" is malformed — a vault connection needs a verb: ` +
221
+ `"vault:<name>:<read|write>".`,
222
+ );
223
+ }
224
+ const name = rest.slice(0, colon);
225
+ let verbAndTags = rest.slice(colon + 1);
226
+ if (!VAULT_NAME_SLUG.test(name)) {
227
+ throw new WantsParseError(
228
+ `wants: "${entry}" — vault name "${name}" must be a slug (alphanumeric, dash, underscore).`,
229
+ );
230
+ }
231
+ // Tag suffix: everything from the first `#` onward, split into individual `#tag`s.
232
+ let tags: string[] | undefined;
233
+ const hash = verbAndTags.indexOf("#");
234
+ if (hash >= 0) {
235
+ const tagStr = verbAndTags.slice(hash);
236
+ verbAndTags = verbAndTags.slice(0, hash);
237
+ tags = tagStr
238
+ .split("#")
239
+ .map((t) => t.trim())
240
+ .filter((t) => t.length > 0)
241
+ .map((t) => `#${t}`);
242
+ if (tags.length === 0) tags = undefined;
243
+ }
244
+ const verb = verbAndTags.trim();
245
+ if (verb !== "read" && verb !== "write") {
246
+ throw new WantsParseError(
247
+ `wants: "${entry}" — vault access must be "read" or "write" (got "${verb}").`,
248
+ );
249
+ }
250
+ return { kind: "vault", target: name, access: verb, ...(tags ? { tags } : {}) };
251
+ }
252
+
253
+ /** Parse `env:<service>` / `mcp:<service>` into one service connection. */
254
+ function parseServiceWant(entry: string, service: string, mode: "env" | "mcp"): ConnectionSpec {
255
+ const target = service.trim();
256
+ if (!SERVICE_NAME_SLUG.test(target)) {
257
+ throw new WantsParseError(
258
+ `wants: "${entry}" — service name "${target}" must be a slug (alphanumeric, dash, underscore).`,
259
+ );
260
+ }
261
+ // Reject a service whose env-var would collide with the Claude-auth denylist
262
+ // (e.g. a service named `claude-code-oauth` → CLAUDE_CODE_OAUTH_TOKEN). The
263
+ // spawn-time denylist already drops it (security intact), but surface it HERE at
264
+ // define-time so the operator sees the problem rather than a silent spawn-warn.
265
+ if (DENYLISTED_ENV.has(serviceEnvVar(target))) {
266
+ throw new WantsParseError(
267
+ `wants: "${entry}" — service "${target}" maps to the protected env var ${serviceEnvVar(target)}, ` +
268
+ `which a grant can never set (it's the session's managed Claude auth).`,
269
+ );
270
+ }
271
+ return { kind: "service", target, inject: [mode] };
272
+ }
273
+
274
+ /** Parse `mcp:<https-url>` — a remote MCP (parsed; deferred to 4b-2). */
275
+ function parseMcpUrlWant(entry: string, url: string): ConnectionSpec {
276
+ let parsed: URL;
277
+ try {
278
+ parsed = new URL(url);
279
+ } catch {
280
+ throw new WantsParseError(`wants: "${entry}" — "${url}" is not a valid URL.`);
281
+ }
282
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
283
+ throw new WantsParseError(`wants: "${entry}" — remote MCP URL must be http(s) (got "${parsed.protocol}").`);
284
+ }
285
+ return { kind: "mcp", target: url };
286
+ }
287
+
288
+ // ---------------------------------------------------------------------------
289
+ // The hub grants-API client (consume parachute-hub #668)
290
+ // ---------------------------------------------------------------------------
291
+
292
+ /** A grant record as the hub returns it (PUT result / GET list element). No secrets. */
293
+ export interface GrantRecord {
294
+ /** The hub-assigned grant id (used to fetch material). */
295
+ id: string;
296
+ /** The agent name the grant belongs to. */
297
+ agent: string;
298
+ /** The connection spec (echoed back). */
299
+ connection: ConnectionSpec;
300
+ /** Lifecycle: `pending` (registered, not approved), `approved`, `revoked`, `error`. */
301
+ status: string;
302
+ /** Optional human reason (e.g. why it errored). */
303
+ reason?: string;
304
+ /** ISO timestamp the operator approved it (approved grants). */
305
+ approvedAt?: string;
306
+ }
307
+
308
+ /**
309
+ * Approved-grant material — APPROVED only. A discriminated union by `kind`.
310
+ * - `vault` → a Bearer + the granted vault's MCP URL (inject as an MCP server).
311
+ * - `service` → a Bearer + the inject shape(s) (env var and/or service MCP server).
312
+ * - `mcp` → a remote-MCP grant (4b-2): a Bearer + the remote MCP URL. The wire
313
+ * shape is byte-identical to `vault` (`{ token, mcpUrl }`) — the hub auto-refreshes
314
+ * OAuth tokens behind `/material` and projects only `{ kind, token, mcpUrl }`, so
315
+ * the consumer injects it the SAME way as a granted vault.
316
+ */
317
+ export type GrantMaterial =
318
+ | { kind: "vault"; token: string; mcpUrl: string }
319
+ | { kind: "service"; token: string; inject: ("env" | "mcp")[] }
320
+ | { kind: "mcp"; token: string; mcpUrl: string };
321
+
322
+ /** A failed grants-API call — carries the HTTP status for the caller to branch on. */
323
+ export class GrantsApiError extends Error {
324
+ constructor(
325
+ message: string,
326
+ readonly status: number,
327
+ ) {
328
+ super(message);
329
+ this.name = "GrantsApiError";
330
+ }
331
+ }
332
+
333
+ export interface GrantsClientDeps {
334
+ /** Hub public origin (the grants API lives on the hub, not the vault). */
335
+ hubOrigin: string;
336
+ /**
337
+ * The module's host-admin-capable MANAGER BEARER — the SAME operator token it
338
+ * mints vault tokens with (mint-token.ts). All three grants endpoints require a
339
+ * `parachute:host:admin` Bearer; we reuse this credential, no new auth path.
340
+ */
341
+ managerBearer: string;
342
+ /** Inject fetch for tests. Defaults to global fetch. */
343
+ fetchFn?: typeof fetch;
344
+ }
345
+
346
+ function stripTrailingSlash(url: string): string {
347
+ return url.replace(/\/$/, "");
348
+ }
349
+
350
+ /**
351
+ * Thin client for the hub's grants API (parachute-hub #668 + #96). The calls the module
352
+ * makes — register (PUT), list (GET), fetch-material (GET …/material), and reconcile
353
+ * (POST …/reconcile, the grant-GC of #96). It NEVER approves/revokes (operator-only via
354
+ * the hub UI). All requests carry the manager bearer (`parachute:host:admin`).
355
+ */
356
+ export class GrantsClient {
357
+ private readonly base: string;
358
+ private readonly managerBearer: string;
359
+ private readonly fetchFn: typeof fetch;
360
+
361
+ constructor(deps: GrantsClientDeps) {
362
+ if (!deps.hubOrigin) throw new Error("GrantsClient: hubOrigin is required");
363
+ if (!deps.managerBearer) throw new Error("GrantsClient: managerBearer is required");
364
+ this.base = stripTrailingSlash(deps.hubOrigin);
365
+ this.managerBearer = deps.managerBearer;
366
+ this.fetchFn = deps.fetchFn ?? fetch;
367
+ }
368
+
369
+ private authHeaders(extra?: Record<string, string>): Record<string, string> {
370
+ return { authorization: `Bearer ${this.managerBearer}`, ...(extra ?? {}) };
371
+ }
372
+
373
+ /**
374
+ * Register (idempotent upsert) a PENDING grant request for `(agent, connection)`.
375
+ * `PUT /admin/grants { agent, connection }` → the grant record (status, usually
376
+ * `pending` on first register; an already-approved grant returns its current
377
+ * status). Throws {@link GrantsApiError} on a non-ok response.
378
+ */
379
+ async registerGrant(agent: string, connection: ConnectionSpec): Promise<GrantRecord> {
380
+ const url = `${this.base}/admin/grants`;
381
+ const res = await this.fetchFn(url, {
382
+ method: "PUT",
383
+ headers: this.authHeaders({ "content-type": "application/json" }),
384
+ body: JSON.stringify({ agent, connection }),
385
+ });
386
+ if (!res.ok) {
387
+ const detail = await res.text().catch(() => "");
388
+ throw new GrantsApiError(`register grant failed (${res.status}) ${detail}`.trim(), res.status);
389
+ }
390
+ return (await res.json()) as GrantRecord;
391
+ }
392
+
393
+ /**
394
+ * List the grants for an agent — `GET /admin/grants?agent=<name>` → `{ grants }`.
395
+ * No secrets (status only). Throws on a non-ok response.
396
+ */
397
+ async listGrants(agent: string): Promise<GrantRecord[]> {
398
+ const url = `${this.base}/admin/grants?agent=${encodeURIComponent(agent)}`;
399
+ const res = await this.fetchFn(url, { headers: this.authHeaders() });
400
+ if (!res.ok) {
401
+ const detail = await res.text().catch(() => "");
402
+ throw new GrantsApiError(`list grants failed (${res.status}) ${detail}`.trim(), res.status);
403
+ }
404
+ const parsed = (await res.json()) as { grants?: GrantRecord[] };
405
+ return Array.isArray(parsed.grants) ? parsed.grants : [];
406
+ }
407
+
408
+ /**
409
+ * Fetch a grant's MATERIAL — `GET /admin/grants/<id>/material`. APPROVED only:
410
+ * the hub 404s an unknown id and 409s a not-yet-approved grant — both return null
411
+ * (the connection is simply absent this spawn, never a failure). Any OTHER non-ok
412
+ * throws {@link GrantsApiError} (a real fault the caller should log). The result
413
+ * is SECRET (a token) — fetched fresh each spawn, never cached.
414
+ */
415
+ async getMaterial(id: string): Promise<GrantMaterial | null> {
416
+ const url = `${this.base}/admin/grants/${encodeURIComponent(id)}/material`;
417
+ const res = await this.fetchFn(url, { headers: this.authHeaders() });
418
+ if (res.status === 404 || res.status === 409) return null;
419
+ if (!res.ok) {
420
+ const detail = await res.text().catch(() => "");
421
+ throw new GrantsApiError(`get grant material failed (${res.status}) ${detail}`.trim(), res.status);
422
+ }
423
+ return (await res.json()) as GrantMaterial;
424
+ }
425
+
426
+ /**
427
+ * GARBAGE-COLLECT an agent's now-stale grants (parachute-hub #96). `POST
428
+ * /admin/grants/reconcile { agent, liveConnections }` → `{ pruned, prunedIds }`. The
429
+ * hub re-derives each key with ITS OWN connectionKey and tears down + REMOVES every
430
+ * grant for `agent` whose key is NOT among the live specs (an empty array prunes ALL
431
+ * of the agent's grants — the def is gone). This is how a removed connection (or a
432
+ * deleted `#agent/definition` note) stops orphaning a live `approved` grant row.
433
+ *
434
+ * `liveConnections` is the agent's CURRENTLY-declared connection SPECS (`def.wants`).
435
+ * We send SPECS, not keys, so there's no dependency on this module's connectionKey
436
+ * matching the hub's — the hub keys them the same way it stored them.
437
+ *
438
+ * SAFETY: only ever call this from a CONFIDENT live set — a clean successful def load
439
+ * (real `liveConnections`) or a confirmed removal (empty array). NEVER from a parse/load
440
+ * failure: a transient error must not present an empty/partial set that nukes
441
+ * approved grants. Pruning only ever REMOVES access (never escalates), so the host-admin
442
+ * Bearer is the right auth (mirrors PUT/GET /admin/grants) — the same one the module
443
+ * uses for register/list/material. Throws {@link GrantsApiError} on a non-ok response;
444
+ * the caller logs + continues (best-effort — a GC fault must never crash a load).
445
+ */
446
+ async reconcileGrants(
447
+ agent: string,
448
+ liveConnections: ConnectionSpec[],
449
+ ): Promise<{ pruned: number; prunedIds?: string[] }> {
450
+ // Send the live connection SPECS, not pre-computed keys: the hub re-derives
451
+ // each key with its OWN connectionKey (the one it stored them under). Sending
452
+ // keys we computed here would couple to the hub's separate connectionKey impl,
453
+ // which diverges for service / tagged-vault / mixed-case-mcp grants and would
454
+ // wrongly prune still-wanted grants (caught by live verification 2026-06-18).
455
+ const url = `${this.base}/admin/grants/reconcile`;
456
+ const res = await this.fetchFn(url, {
457
+ method: "POST",
458
+ headers: this.authHeaders({ "content-type": "application/json" }),
459
+ body: JSON.stringify({ agent, liveConnections }),
460
+ });
461
+ if (!res.ok) {
462
+ const detail = await res.text().catch(() => "");
463
+ throw new GrantsApiError(`reconcile grants failed (${res.status}) ${detail}`.trim(), res.status);
464
+ }
465
+ const parsed = (await res.json().catch(() => ({}))) as { pruned?: number; prunedIds?: string[] };
466
+ return {
467
+ pruned: typeof parsed.pruned === "number" ? parsed.pruned : 0,
468
+ ...(Array.isArray(parsed.prunedIds) ? { prunedIds: parsed.prunedIds } : {}),
469
+ };
470
+ }
471
+ }
472
+
473
+ // ---------------------------------------------------------------------------
474
+ // Status resolution — enabled vs pending from registered grants
475
+ // ---------------------------------------------------------------------------
476
+
477
+ /** The resolved status for a def after its connections are registered. */
478
+ export interface ConnectionStatus {
479
+ /** `enabled` iff every declared connection is `approved`; else `pending`. */
480
+ status: "enabled" | "pending";
481
+ /** The connection keys NOT yet approved (only when `pending`). */
482
+ pending?: string[];
483
+ }
484
+
485
+ /**
486
+ * Resolve the def's status from its declared connections + each one's registered
487
+ * grant status (design §2). `enabled` ONLY if EVERY connection is `approved`; else
488
+ * `pending` listing the unapproved connection keys. No connections → `enabled`.
489
+ *
490
+ * `grantStatusByKey` maps a {@link connectionKey} → the hub's grant status. A
491
+ * connection with no entry (registration failed / not found) counts as NOT approved.
492
+ */
493
+ export function resolveConnectionStatus(
494
+ connections: ConnectionSpec[],
495
+ grantStatusByKey: Map<string, string>,
496
+ ): ConnectionStatus {
497
+ if (connections.length === 0) return { status: "enabled" };
498
+ const pending: string[] = [];
499
+ for (const c of connections) {
500
+ const key = connectionKey(c);
501
+ if (grantStatusByKey.get(key) !== "approved") pending.push(key);
502
+ }
503
+ if (pending.length === 0) return { status: "enabled" };
504
+ return { status: "pending", pending };
505
+ }
506
+
507
+ // ---------------------------------------------------------------------------
508
+ // Spawn-time injection — approved grant material → MCP-config entries + env vars
509
+ // ---------------------------------------------------------------------------
510
+
511
+ /** Known service → the env var name its token injects as. Default: `<TARGET>_TOKEN`. */
512
+ const SERVICE_ENV_VAR: Record<string, string> = {
513
+ github: "GITHUB_TOKEN",
514
+ cloudflare: "CLOUDFLARE_API_TOKEN",
515
+ };
516
+
517
+ /** Known service → its remote MCP server URL (for the `inject:["mcp"]` shape). */
518
+ const SERVICE_MCP_URL: Record<string, string> = {
519
+ github: "https://api.githubcopilot.com/mcp/",
520
+ };
521
+
522
+ /** The env var name a service's token injects as (known map ?? `<TARGET>_TOKEN`). */
523
+ export function serviceEnvVar(service: string): string {
524
+ return SERVICE_ENV_VAR[service] ?? `${service.toUpperCase().replace(/[^A-Z0-9]+/g, "_")}_TOKEN`;
525
+ }
526
+
527
+ /** The known remote-MCP URL for a service, or undefined (no MCP injection for it). */
528
+ export function serviceMcpUrl(service: string): string | undefined {
529
+ return SERVICE_MCP_URL[service];
530
+ }
531
+
532
+ /** One MCP server entry to ADD to the agent's `--mcp-config` from a grant. */
533
+ export interface InjectedMcpEntry {
534
+ /** Entry key in `mcpServers` (unique per granted resource). */
535
+ name: string;
536
+ /** Streamable-HTTP MCP URL. */
537
+ url: string;
538
+ /** Bearer token for the Authorization header. */
539
+ token: string;
540
+ }
541
+
542
+ /** The result of resolving an agent's approved grants into spawn-injectable bits. */
543
+ export interface InjectedGrants {
544
+ /** MCP servers to ADD to the existing per-spawn `.mcp.json` (vault + service-mcp). */
545
+ mcpEntries: InjectedMcpEntry[];
546
+ /** Env vars to set for the agent's shell tools (service env injections). */
547
+ env: Record<string, string>;
548
+ }
549
+
550
+ /**
551
+ * Resolve an agent's APPROVED grants into spawn-injectable MCP entries + env vars
552
+ * (design §3). Fetches the agent's grant LIST, then for each `approved` grant fetches
553
+ * its MATERIAL FRESH (never cached — revocation takes effect next spawn) and maps it:
554
+ *
555
+ * - vault material (`{token, mcpUrl}`) → an MCP server entry (the agent
556
+ * reaches the OTHER vault alongside its own).
557
+ * - service material, inject includes `"env"` → an env var (github→GITHUB_TOKEN,
558
+ * cloudflare→CLOUDFLARE_API_TOKEN, default `<TARGET>_TOKEN`).
559
+ * - service material, inject includes `"mcp"` → the service's MCP server entry
560
+ * (known-service→URL map; a service with no known MCP logs + SKIPS the mcp
561
+ * inject, keeping the env one).
562
+ * - mcp material (`{token, mcpUrl}`, 4b-2) → an MCP server entry (the agent
563
+ * reaches the remote MCP / OAuth resource). An UNAPPROVED mcp grant has no
564
+ * material — `getMaterial` returns null (404/409), so it's simply absent.
565
+ *
566
+ * The MCP-entry KEYS are namespaced (`grant-vault-<name>`, `grant-service-<svc>`,
567
+ * `grant-mcp-<grant-id>`) so they never collide with the agent's own def-vault entry
568
+ * (`parachute-vault-<name>`).
569
+ *
570
+ * Best-effort + isolated: the grant LIST failing throws (the caller logs + spawns
571
+ * WITHOUT injected grants — own-vault still works); a SINGLE material fetch failing
572
+ * is logged + skipped (that one connection is absent; the rest inject). Secrets only
573
+ * flow into the returned struct → the ephemeral 0600 spawn config; never logged.
574
+ */
575
+ export async function resolveInjectedGrants(
576
+ client: GrantsClient,
577
+ agent: string,
578
+ ): Promise<InjectedGrants> {
579
+ const mcpEntries: InjectedMcpEntry[] = [];
580
+ const env: Record<string, string> = {};
581
+
582
+ const grants = await client.listGrants(agent); // throws → caller spawns without grants
583
+ for (const g of grants) {
584
+ if (g.status !== "approved") continue; // only approved grants have material
585
+ let material: GrantMaterial | null;
586
+ try {
587
+ material = await client.getMaterial(g.id);
588
+ } catch (err) {
589
+ // A single material fetch fault must not sink the others — that connection is
590
+ // simply absent this spawn. Never log the token (there is none in the error).
591
+ console.warn(
592
+ `parachute-agent: fetching grant material for "${agent}" (${connectionKey(g.connection)}) ` +
593
+ `failed (skipping this connection): ${(err as Error).message}`,
594
+ );
595
+ continue;
596
+ }
597
+ if (!material) continue; // 404/409 — not actually approved/available right now
598
+
599
+ if (material.kind === "vault") {
600
+ mcpEntries.push({
601
+ name: grantVaultEntryKey(g.connection.target),
602
+ url: material.mcpUrl,
603
+ token: material.token,
604
+ });
605
+ continue;
606
+ }
607
+
608
+ if (material.kind === "mcp") {
609
+ // Remote-MCP grant (4b-2): the /material wire shape is byte-identical to vault's
610
+ // (`{token, mcpUrl}`); inject it the SAME way. Key on the grant ID (not the URL)
611
+ // so two distinct remote MCPs never collide on the entry name.
612
+ mcpEntries.push({
613
+ name: grantMcpEntryKey(g.id),
614
+ url: material.mcpUrl,
615
+ token: material.token,
616
+ });
617
+ continue;
618
+ }
619
+
620
+ if (material.kind === "service") {
621
+ // service material — inject env and/or mcp per the material's `inject` list.
622
+ const service = g.connection.target;
623
+ const inject = material.inject ?? [];
624
+ if (inject.includes("env")) {
625
+ env[serviceEnvVar(service)] = material.token;
626
+ }
627
+ if (inject.includes("mcp")) {
628
+ const url = serviceMcpUrl(service);
629
+ if (url) {
630
+ mcpEntries.push({
631
+ name: grantServiceEntryKey(service),
632
+ url,
633
+ token: material.token,
634
+ });
635
+ } else {
636
+ // No known MCP URL for this service — keep the env inject, skip the mcp one.
637
+ console.warn(
638
+ `parachute-agent: service "${service}" granted with inject:"mcp" but no known MCP URL — ` +
639
+ `skipping the MCP injection (the env injection, if any, still applies).`,
640
+ );
641
+ }
642
+ }
643
+ continue;
644
+ }
645
+
646
+ // Exhaustiveness guard (future-safety): every known material kind `continue`s
647
+ // above, so `material` is `never` here today. If a future kind is added to the
648
+ // union without a branch, it lands here + is skipped LOUDLY rather than silently
649
+ // falling into the service path. Never log the token (the struct, not the value).
650
+ console.warn(
651
+ `parachute-agent: grant material for "${agent}" has an unhandled kind ` +
652
+ `"${(material as { kind?: string }).kind}" — skipping (no injection).`,
653
+ );
654
+ }
655
+
656
+ return { mcpEntries, env };
657
+ }
658
+
659
+ /** MCP entry key for a GRANTED vault — namespaced so it never collides with the
660
+ * agent's OWN def-vault entry (`parachute-vault-<name>`). */
661
+ export function grantVaultEntryKey(vault: string): string {
662
+ return `grant-vault-${vault}`;
663
+ }
664
+
665
+ /** MCP entry key for a GRANTED service MCP server. */
666
+ export function grantServiceEntryKey(service: string): string {
667
+ return `grant-service-${service}`;
668
+ }
669
+
670
+ /** MCP entry key for a GRANTED remote MCP (4b-2) — keyed by the grant id (stable +
671
+ * collision-free) and namespaced so it never collides with `grant-vault-*` /
672
+ * `grant-service-*` / the agent's OWN def-vault entry (`parachute-vault-<name>`). */
673
+ export function grantMcpEntryKey(slug: string): string {
674
+ return `grant-mcp-${slug}`;
675
+ }