@openparachute/agent 0.1.2 → 0.2.2

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