@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/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
+ }