@openparachute/agent 0.1.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 (501) hide show
  1. package/.claude/scheduled_tasks.lock +1 -0
  2. package/.claude/settings.json +5 -0
  3. package/.claude/skills/add-atomic-chat-tool/SKILL.md +243 -0
  4. package/.claude/skills/add-atomic-chat-tool/atomic-chat-mcp-stdio.ts +229 -0
  5. package/.claude/skills/add-codex/SKILL.md +161 -0
  6. package/.claude/skills/add-dashboard/SKILL.md +138 -0
  7. package/.claude/skills/add-dashboard/resources/dashboard-pusher.ts +495 -0
  8. package/.claude/skills/add-emacs/SKILL.md +296 -0
  9. package/.claude/skills/add-gcal-tool/SKILL.md +210 -0
  10. package/.claude/skills/add-gchat/REMOVE.md +6 -0
  11. package/.claude/skills/add-gchat/SKILL.md +92 -0
  12. package/.claude/skills/add-gchat/VERIFY.md +3 -0
  13. package/.claude/skills/add-github/REMOVE.md +6 -0
  14. package/.claude/skills/add-github/SKILL.md +148 -0
  15. package/.claude/skills/add-github/VERIFY.md +3 -0
  16. package/.claude/skills/add-gmail-tool/SKILL.md +229 -0
  17. package/.claude/skills/add-imessage/REMOVE.md +6 -0
  18. package/.claude/skills/add-imessage/SKILL.md +113 -0
  19. package/.claude/skills/add-imessage/VERIFY.md +3 -0
  20. package/.claude/skills/add-karpathy-llm-wiki/SKILL.md +110 -0
  21. package/.claude/skills/add-karpathy-llm-wiki/llm-wiki.md +75 -0
  22. package/.claude/skills/add-linear/REMOVE.md +6 -0
  23. package/.claude/skills/add-linear/SKILL.md +168 -0
  24. package/.claude/skills/add-linear/VERIFY.md +3 -0
  25. package/.claude/skills/add-macos-statusbar/SKILL.md +133 -0
  26. package/.claude/skills/add-macos-statusbar/add/src/statusbar.swift +147 -0
  27. package/.claude/skills/add-matrix/REMOVE.md +6 -0
  28. package/.claude/skills/add-matrix/SKILL.md +148 -0
  29. package/.claude/skills/add-matrix/VERIFY.md +3 -0
  30. package/.claude/skills/add-ollama-provider/SKILL.md +179 -0
  31. package/.claude/skills/add-ollama-tool/SKILL.md +193 -0
  32. package/.claude/skills/add-opencode/SKILL.md +229 -0
  33. package/.claude/skills/add-parallel/SKILL.md +290 -0
  34. package/.claude/skills/add-resend/REMOVE.md +6 -0
  35. package/.claude/skills/add-resend/SKILL.md +93 -0
  36. package/.claude/skills/add-resend/VERIFY.md +3 -0
  37. package/.claude/skills/add-signal/REMOVE.md +13 -0
  38. package/.claude/skills/add-signal/SKILL.md +318 -0
  39. package/.claude/skills/add-signal/VERIFY.md +5 -0
  40. package/.claude/skills/add-slack/REMOVE.md +6 -0
  41. package/.claude/skills/add-slack/SKILL.md +112 -0
  42. package/.claude/skills/add-slack/VERIFY.md +3 -0
  43. package/.claude/skills/add-teams/REMOVE.md +6 -0
  44. package/.claude/skills/add-teams/SKILL.md +207 -0
  45. package/.claude/skills/add-teams/VERIFY.md +3 -0
  46. package/.claude/skills/add-vercel/SKILL.md +147 -0
  47. package/.claude/skills/add-vercel/container-skills/vercel-cli/SKILL.md +103 -0
  48. package/.claude/skills/add-webex/REMOVE.md +6 -0
  49. package/.claude/skills/add-webex/SKILL.md +88 -0
  50. package/.claude/skills/add-webex/VERIFY.md +3 -0
  51. package/.claude/skills/add-wechat/REMOVE.md +49 -0
  52. package/.claude/skills/add-wechat/SKILL.md +170 -0
  53. package/.claude/skills/add-wechat/scripts/wire-dm.ts +172 -0
  54. package/.claude/skills/add-whatsapp/SKILL.md +264 -0
  55. package/.claude/skills/add-whatsapp-cloud/REMOVE.md +6 -0
  56. package/.claude/skills/add-whatsapp-cloud/SKILL.md +95 -0
  57. package/.claude/skills/add-whatsapp-cloud/VERIFY.md +3 -0
  58. package/.claude/skills/claw/SKILL.md +131 -0
  59. package/.claude/skills/claw/scripts/claw +374 -0
  60. package/.claude/skills/convert-to-apple-container/SKILL.md +212 -0
  61. package/.claude/skills/customize/SKILL.md +110 -0
  62. package/.claude/skills/debug/SKILL.md +349 -0
  63. package/.claude/skills/get-qodo-rules/SKILL.md +122 -0
  64. package/.claude/skills/get-qodo-rules/references/output-format.md +41 -0
  65. package/.claude/skills/get-qodo-rules/references/pagination.md +33 -0
  66. package/.claude/skills/get-qodo-rules/references/repository-scope.md +26 -0
  67. package/.claude/skills/init-first-agent/SKILL.md +120 -0
  68. package/.claude/skills/init-onecli/SKILL.md +270 -0
  69. package/.claude/skills/manage-channels/SKILL.md +87 -0
  70. package/.claude/skills/manage-mounts/SKILL.md +47 -0
  71. package/.claude/skills/migrate-from-openclaw/MIGRATE_CRONS.md +100 -0
  72. package/.claude/skills/migrate-from-openclaw/SKILL.md +447 -0
  73. package/.claude/skills/migrate-from-openclaw/scripts/discover-openclaw.ts +734 -0
  74. package/.claude/skills/migrate-from-openclaw/scripts/extract-channel-credentials.ts +476 -0
  75. package/.claude/skills/migrate-nanoclaw/SKILL.md +484 -0
  76. package/.claude/skills/migrate-nanoclaw/diagnostics.md +51 -0
  77. package/.claude/skills/qodo-pr-resolver/SKILL.md +326 -0
  78. package/.claude/skills/qodo-pr-resolver/resources/providers.md +329 -0
  79. package/.claude/skills/update-nanoclaw/SKILL.md +243 -0
  80. package/.claude/skills/update-nanoclaw/diagnostics.md +48 -0
  81. package/.claude/skills/update-skills/SKILL.md +130 -0
  82. package/.claude/skills/use-native-credential-proxy/SKILL.md +167 -0
  83. package/.claude/skills/x-integration/SKILL.md +417 -0
  84. package/.claude/skills/x-integration/agent.ts +243 -0
  85. package/.claude/skills/x-integration/host.ts +155 -0
  86. package/.claude/skills/x-integration/lib/browser.ts +148 -0
  87. package/.claude/skills/x-integration/lib/config.ts +62 -0
  88. package/.claude/skills/x-integration/scripts/like.ts +56 -0
  89. package/.claude/skills/x-integration/scripts/post.ts +66 -0
  90. package/.claude/skills/x-integration/scripts/quote.ts +80 -0
  91. package/.claude/skills/x-integration/scripts/reply.ts +74 -0
  92. package/.claude/skills/x-integration/scripts/retweet.ts +62 -0
  93. package/.claude/skills/x-integration/scripts/setup.ts +87 -0
  94. package/.github/CODEOWNERS +10 -0
  95. package/.github/PULL_REQUEST_TEMPLATE.md +18 -0
  96. package/.github/workflows/bump-version.yml +35 -0
  97. package/.github/workflows/ci.yml +39 -0
  98. package/.github/workflows/label-pr.yml +40 -0
  99. package/.github/workflows/update-tokens.yml +43 -0
  100. package/.husky/pre-commit +1 -0
  101. package/.mcp.json +3 -0
  102. package/.nvmrc +1 -0
  103. package/.parachute/module.json +14 -0
  104. package/.prettierrc +4 -0
  105. package/CHANGELOG.md +215 -0
  106. package/CLAUDE.md +307 -0
  107. package/CODE_OF_CONDUCT.md +128 -0
  108. package/CONTRIBUTING.md +159 -0
  109. package/CONTRIBUTORS.md +26 -0
  110. package/LICENSE +21 -0
  111. package/README.md +190 -0
  112. package/README_ja.md +194 -0
  113. package/README_zh.md +194 -0
  114. package/assets/nanoclaw-favicon.png +0 -0
  115. package/assets/nanoclaw-icon.png +0 -0
  116. package/assets/nanoclaw-logo-dark.png +0 -0
  117. package/assets/nanoclaw-logo.png +0 -0
  118. package/assets/nanoclaw-profile.jpeg +0 -0
  119. package/assets/nanoclaw-sales.png +0 -0
  120. package/assets/social-preview.jpg +0 -0
  121. package/config-examples/mount-allowlist.json +25 -0
  122. package/container/.dockerignore +2 -0
  123. package/container/CLAUDE.md +21 -0
  124. package/container/Dockerfile +121 -0
  125. package/container/agent-runner/bun.lock +243 -0
  126. package/container/agent-runner/package.json +22 -0
  127. package/container/agent-runner/scripts/sdk-signal-probe.ts +169 -0
  128. package/container/agent-runner/src/config.ts +55 -0
  129. package/container/agent-runner/src/db/connection.ts +267 -0
  130. package/container/agent-runner/src/db/index.ts +20 -0
  131. package/container/agent-runner/src/db/messages-in.ts +138 -0
  132. package/container/agent-runner/src/db/messages-out.ts +143 -0
  133. package/container/agent-runner/src/db/session-routing.ts +30 -0
  134. package/container/agent-runner/src/db/session-state.test.ts +100 -0
  135. package/container/agent-runner/src/db/session-state.ts +79 -0
  136. package/container/agent-runner/src/destinations.ts +135 -0
  137. package/container/agent-runner/src/formatter.test.ts +167 -0
  138. package/container/agent-runner/src/formatter.ts +260 -0
  139. package/container/agent-runner/src/index.ts +110 -0
  140. package/container/agent-runner/src/integration.test.ts +121 -0
  141. package/container/agent-runner/src/mcp-tools/agents.instructions.md +26 -0
  142. package/container/agent-runner/src/mcp-tools/agents.ts +66 -0
  143. package/container/agent-runner/src/mcp-tools/core.instructions.md +27 -0
  144. package/container/agent-runner/src/mcp-tools/core.ts +262 -0
  145. package/container/agent-runner/src/mcp-tools/index.ts +22 -0
  146. package/container/agent-runner/src/mcp-tools/interactive.instructions.md +22 -0
  147. package/container/agent-runner/src/mcp-tools/interactive.ts +169 -0
  148. package/container/agent-runner/src/mcp-tools/scheduling.instructions.md +40 -0
  149. package/container/agent-runner/src/mcp-tools/scheduling.ts +299 -0
  150. package/container/agent-runner/src/mcp-tools/self-mod.instructions.md +25 -0
  151. package/container/agent-runner/src/mcp-tools/self-mod.ts +120 -0
  152. package/container/agent-runner/src/mcp-tools/server.ts +54 -0
  153. package/container/agent-runner/src/mcp-tools/types.ts +6 -0
  154. package/container/agent-runner/src/poll-loop.test.ts +248 -0
  155. package/container/agent-runner/src/poll-loop.ts +437 -0
  156. package/container/agent-runner/src/providers/claude.ts +379 -0
  157. package/container/agent-runner/src/providers/factory.test.ts +19 -0
  158. package/container/agent-runner/src/providers/factory.ts +13 -0
  159. package/container/agent-runner/src/providers/index.ts +6 -0
  160. package/container/agent-runner/src/providers/mock.ts +77 -0
  161. package/container/agent-runner/src/providers/provider-registry.ts +33 -0
  162. package/container/agent-runner/src/providers/types.ts +82 -0
  163. package/container/agent-runner/src/scheduling/task-script.ts +121 -0
  164. package/container/agent-runner/src/timezone.test.ts +93 -0
  165. package/container/agent-runner/src/timezone.ts +107 -0
  166. package/container/agent-runner/tsconfig.json +14 -0
  167. package/container/build.sh +48 -0
  168. package/container/entrypoint.sh +16 -0
  169. package/container/skills/agent-browser/SKILL.md +159 -0
  170. package/container/skills/frontend-engineer/SKILL.md +157 -0
  171. package/container/skills/self-customize/SKILL.md +87 -0
  172. package/container/skills/slack-formatting/SKILL.md +94 -0
  173. package/container/skills/vercel-cli/SKILL.md +111 -0
  174. package/container/skills/welcome/SKILL.md +85 -0
  175. package/docs/APPLE-CONTAINER-NETWORKING.md +90 -0
  176. package/docs/BRANCH-FORK-MAINTENANCE.md +81 -0
  177. package/docs/README.md +25 -0
  178. package/docs/SDK_DEEP_DIVE.md +643 -0
  179. package/docs/SECURITY.md +162 -0
  180. package/docs/agent-runner-details.md +749 -0
  181. package/docs/api-details.md +365 -0
  182. package/docs/architecture-diagram.html +422 -0
  183. package/docs/architecture-diagram.md +215 -0
  184. package/docs/architecture.md +751 -0
  185. package/docs/audit/2026-04-30-channel-endpoint-audit.md +36 -0
  186. package/docs/build-and-runtime.md +80 -0
  187. package/docs/cross-mount-stress/README.md +112 -0
  188. package/docs/cross-mount-stress/container-writer-retry.mjs +55 -0
  189. package/docs/cross-mount-stress/container-writer-slow.mjs +42 -0
  190. package/docs/cross-mount-stress/container-writer.mjs +47 -0
  191. package/docs/cross-mount-stress/host-writer-retry.mjs +55 -0
  192. package/docs/cross-mount-stress/host-writer-slow.mjs +43 -0
  193. package/docs/cross-mount-stress/host-writer.mjs +47 -0
  194. package/docs/db-central.md +316 -0
  195. package/docs/db-session.md +183 -0
  196. package/docs/db.md +119 -0
  197. package/docs/design/2026-04-29-vault-management-ui.md +231 -0
  198. package/docs/design/2026-04-30-channel-wiring-rework.md +234 -0
  199. package/docs/design/2026-05-01-channel-wiring-approvals-deep-dive.md +272 -0
  200. package/docs/design/2026-05-02-channel-policy-and-approval-routing.md +250 -0
  201. package/docs/docker-sandboxes.md +359 -0
  202. package/docs/isolation-model.md +88 -0
  203. package/docs/ollama.md +79 -0
  204. package/docs/parachute-integration.md +109 -0
  205. package/docs/post-night-rebirth-reflections.md +151 -0
  206. package/eslint.config.js +32 -0
  207. package/package.json +54 -0
  208. package/pnpm-workspace.yaml +8 -0
  209. package/repo-tokens/README.md +113 -0
  210. package/repo-tokens/action.yml +186 -0
  211. package/repo-tokens/badge.svg +23 -0
  212. package/repo-tokens/examples/green.svg +14 -0
  213. package/repo-tokens/examples/red.svg +14 -0
  214. package/repo-tokens/examples/yellow-green.svg +14 -0
  215. package/repo-tokens/examples/yellow.svg +14 -0
  216. package/scripts/chat.ts +101 -0
  217. package/scripts/cleanup-sessions.sh +150 -0
  218. package/scripts/init-cli-agent.ts +171 -0
  219. package/scripts/init-first-agent.ts +377 -0
  220. package/scripts/parachute.ts +158 -0
  221. package/scripts/run-migrations.ts +105 -0
  222. package/scripts/sanity-live-poll.ts +95 -0
  223. package/scripts/seed-discord.ts +79 -0
  224. package/scripts/test-v2-agent.ts +106 -0
  225. package/scripts/test-v2-channel-e2e.ts +265 -0
  226. package/scripts/test-v2-host.ts +184 -0
  227. package/src/channels/adapter.ts +214 -0
  228. package/src/channels/ask-question.ts +46 -0
  229. package/src/channels/channel-registry.test.ts +421 -0
  230. package/src/channels/channel-registry.ts +313 -0
  231. package/src/channels/chat-sdk-bridge.test.ts +84 -0
  232. package/src/channels/chat-sdk-bridge.ts +652 -0
  233. package/src/channels/cli.ts +276 -0
  234. package/src/channels/discord.ts +90 -0
  235. package/src/channels/index.ts +17 -0
  236. package/src/channels/telegram-markdown-sanitize.test.ts +78 -0
  237. package/src/channels/telegram-markdown-sanitize.ts +55 -0
  238. package/src/channels/telegram-pairing.test.ts +254 -0
  239. package/src/channels/telegram-pairing.ts +339 -0
  240. package/src/channels/telegram.ts +279 -0
  241. package/src/channels/trust-hint.test.ts +48 -0
  242. package/src/channels/trust-hint.ts +75 -0
  243. package/src/claude-md-compose.migrate.test.ts +64 -0
  244. package/src/claude-md-compose.ts +205 -0
  245. package/src/command-gate.ts +63 -0
  246. package/src/config.test.ts +93 -0
  247. package/src/config.ts +108 -0
  248. package/src/container-config.ts +167 -0
  249. package/src/container-runner.test.ts +32 -0
  250. package/src/container-runner.ts +576 -0
  251. package/src/container-runtime.test.ts +169 -0
  252. package/src/container-runtime.ts +92 -0
  253. package/src/db/_bun-sqlite-shim.ts +88 -0
  254. package/src/db/agent-activity.test.ts +155 -0
  255. package/src/db/agent-activity.ts +121 -0
  256. package/src/db/agent-groups.ts +77 -0
  257. package/src/db/connection.migrate.test.ts +143 -0
  258. package/src/db/connection.ts +224 -0
  259. package/src/db/db-v2.test.ts +440 -0
  260. package/src/db/dropped-messages.ts +44 -0
  261. package/src/db/index.ts +40 -0
  262. package/src/db/messaging-groups.ts +252 -0
  263. package/src/db/migrations/001-initial.ts +112 -0
  264. package/src/db/migrations/002-chat-sdk-state.ts +36 -0
  265. package/src/db/migrations/008-dropped-messages.ts +27 -0
  266. package/src/db/migrations/009-drop-pending-credentials.ts +13 -0
  267. package/src/db/migrations/010-engage-modes.ts +103 -0
  268. package/src/db/migrations/011-pending-sender-approvals.ts +40 -0
  269. package/src/db/migrations/012-channel-registration.ts +48 -0
  270. package/src/db/migrations/013-approval-render-metadata.ts +27 -0
  271. package/src/db/migrations/014-secrets.ts +44 -0
  272. package/src/db/migrations/015-secrets-drop-host-pattern.ts +18 -0
  273. package/src/db/migrations/016-secret-assignments.ts +30 -0
  274. package/src/db/migrations/017-agent-activity.ts +40 -0
  275. package/src/db/migrations/018-oauth-app-configs.ts +34 -0
  276. package/src/db/migrations/019-oauth-app-connections.ts +48 -0
  277. package/src/db/migrations/020-agent-app-connections.ts +28 -0
  278. package/src/db/migrations/021-pending-oauth-states.ts +35 -0
  279. package/src/db/migrations/022-app-connections-provider.ts +25 -0
  280. package/src/db/migrations/023-agent-group-secret-mode.test.ts +124 -0
  281. package/src/db/migrations/023-agent-group-secret-mode.ts +65 -0
  282. package/src/db/migrations/024-collapse-approvals.test.ts +249 -0
  283. package/src/db/migrations/024-collapse-approvals.ts +182 -0
  284. package/src/db/migrations/025-secret-mode-check.test.ts +155 -0
  285. package/src/db/migrations/025-secret-mode-check.ts +49 -0
  286. package/src/db/migrations/026-user-dms-bot-id.test.ts +116 -0
  287. package/src/db/migrations/026-user-dms-bot-id.ts +54 -0
  288. package/src/db/migrations/027-provider-credentials.ts +41 -0
  289. package/src/db/migrations/_test-helpers.ts +41 -0
  290. package/src/db/migrations/index.ts +127 -0
  291. package/src/db/migrations/module-agent-to-agent-destinations.ts +84 -0
  292. package/src/db/migrations/module-approvals-pending-approvals.ts +42 -0
  293. package/src/db/migrations/module-approvals-title-options.ts +40 -0
  294. package/src/db/schema.ts +258 -0
  295. package/src/db/session-db.test.ts +93 -0
  296. package/src/db/session-db.ts +325 -0
  297. package/src/db/sessions.ts +241 -0
  298. package/src/delivery.test.ts +148 -0
  299. package/src/delivery.ts +445 -0
  300. package/src/env.ts +74 -0
  301. package/src/group-folder.test.ts +35 -0
  302. package/src/group-folder.ts +44 -0
  303. package/src/group-init.ts +92 -0
  304. package/src/host-core.test.ts +456 -0
  305. package/src/host-sweep.test.ts +146 -0
  306. package/src/host-sweep.ts +287 -0
  307. package/src/index.ts +227 -0
  308. package/src/install-slug.ts +33 -0
  309. package/src/log.test.ts +81 -0
  310. package/src/log.ts +117 -0
  311. package/src/mcp/http.ts +72 -0
  312. package/src/mcp/server.ts +92 -0
  313. package/src/mcp/stdio.ts +51 -0
  314. package/src/mcp/tools/activity.ts +88 -0
  315. package/src/mcp/tools/agent-groups.ts +183 -0
  316. package/src/mcp/tools/approvals.ts +122 -0
  317. package/src/mcp/tools/channels.ts +199 -0
  318. package/src/mcp/tools/index.ts +27 -0
  319. package/src/mcp/tools/oauth.ts +48 -0
  320. package/src/mcp/tools/secrets.ts +169 -0
  321. package/src/mcp/tools/sessions.ts +135 -0
  322. package/src/mcp/types.ts +51 -0
  323. package/src/modules/agent-to-agent/agent-route.test.ts +46 -0
  324. package/src/modules/agent-to-agent/agent-route.ts +223 -0
  325. package/src/modules/agent-to-agent/create-agent.ts +127 -0
  326. package/src/modules/agent-to-agent/db/agent-destinations.ts +135 -0
  327. package/src/modules/agent-to-agent/index.ts +22 -0
  328. package/src/modules/agent-to-agent/write-destinations.ts +59 -0
  329. package/src/modules/approvals/agent.md +45 -0
  330. package/src/modules/approvals/index.ts +21 -0
  331. package/src/modules/approvals/picks.test.ts +291 -0
  332. package/src/modules/approvals/primitive.ts +279 -0
  333. package/src/modules/approvals/project.md +27 -0
  334. package/src/modules/approvals/response-handler.ts +87 -0
  335. package/src/modules/index.ts +24 -0
  336. package/src/modules/interactive/agent.md +21 -0
  337. package/src/modules/interactive/index.ts +69 -0
  338. package/src/modules/interactive/project.md +12 -0
  339. package/src/modules/mount-security/index.ts +448 -0
  340. package/src/modules/mount-security/migrate.test.ts +91 -0
  341. package/src/modules/permissions/access.ts +28 -0
  342. package/src/modules/permissions/channel-approval.test.ts +389 -0
  343. package/src/modules/permissions/channel-approval.ts +188 -0
  344. package/src/modules/permissions/db/agent-group-members.ts +44 -0
  345. package/src/modules/permissions/db/pending-channel-approvals.test.ts +86 -0
  346. package/src/modules/permissions/db/pending-channel-approvals.ts +66 -0
  347. package/src/modules/permissions/db/pending-sender-approvals.ts +60 -0
  348. package/src/modules/permissions/db/user-dms.ts +58 -0
  349. package/src/modules/permissions/db/user-roles.ts +85 -0
  350. package/src/modules/permissions/db/users.ts +38 -0
  351. package/src/modules/permissions/index.ts +421 -0
  352. package/src/modules/permissions/permissions.test.ts +358 -0
  353. package/src/modules/permissions/sender-approval.test.ts +470 -0
  354. package/src/modules/permissions/sender-approval.ts +165 -0
  355. package/src/modules/permissions/user-dm.ts +200 -0
  356. package/src/modules/provider-credentials/db.ts +121 -0
  357. package/src/modules/provider-credentials/index.ts +12 -0
  358. package/src/modules/provider-credentials/spawn.test.ts +206 -0
  359. package/src/modules/provider-credentials/spawn.ts +114 -0
  360. package/src/modules/scheduling/actions.ts +113 -0
  361. package/src/modules/scheduling/db.test.ts +282 -0
  362. package/src/modules/scheduling/db.ts +148 -0
  363. package/src/modules/scheduling/index.ts +34 -0
  364. package/src/modules/scheduling/recurrence.test.ts +98 -0
  365. package/src/modules/scheduling/recurrence.ts +54 -0
  366. package/src/modules/self-mod/agent.md +30 -0
  367. package/src/modules/self-mod/apply.ts +85 -0
  368. package/src/modules/self-mod/index.ts +30 -0
  369. package/src/modules/self-mod/project.md +39 -0
  370. package/src/modules/self-mod/request.ts +91 -0
  371. package/src/modules/typing/index.ts +165 -0
  372. package/src/oauth/agent-app-connections.ts +103 -0
  373. package/src/oauth/app-configs.test.ts +64 -0
  374. package/src/oauth/app-configs.ts +114 -0
  375. package/src/oauth/app-connections.test.ts +109 -0
  376. package/src/oauth/app-connections.ts +178 -0
  377. package/src/oauth/crypto.ts +56 -0
  378. package/src/oauth/flow.ts +104 -0
  379. package/src/oauth/providers/google.test.ts +38 -0
  380. package/src/oauth/providers/google.ts +46 -0
  381. package/src/oauth/providers/index.ts +48 -0
  382. package/src/oauth/state-store.test.ts +54 -0
  383. package/src/oauth/state-store.ts +93 -0
  384. package/src/parachute/README.md +27 -0
  385. package/src/parachute/create-agent.test.ts +83 -0
  386. package/src/parachute/create-agent.ts +122 -0
  387. package/src/parachute/group-status.test.ts +165 -0
  388. package/src/parachute/group-status.ts +136 -0
  389. package/src/parachute/types.ts +41 -0
  390. package/src/parachute/vault-mcp.test.ts +251 -0
  391. package/src/parachute/vault-mcp.ts +232 -0
  392. package/src/platform-id.test.ts +104 -0
  393. package/src/platform-id.ts +109 -0
  394. package/src/providers/index.ts +6 -0
  395. package/src/providers/provider-container-registry.ts +58 -0
  396. package/src/response-registry.ts +45 -0
  397. package/src/router.ts +530 -0
  398. package/src/secrets/crypto.test.ts +45 -0
  399. package/src/secrets/crypto.ts +55 -0
  400. package/src/secrets/index.ts +355 -0
  401. package/src/secrets/master-key.ts +70 -0
  402. package/src/secrets/secrets.test.ts +354 -0
  403. package/src/session-manager.migrate.test.ts +59 -0
  404. package/src/session-manager.ts +433 -0
  405. package/src/startup-bootstrap.test.ts +226 -0
  406. package/src/startup-bootstrap.ts +207 -0
  407. package/src/state-sqlite.ts +182 -0
  408. package/src/timezone.test.ts +64 -0
  409. package/src/timezone.ts +37 -0
  410. package/src/types.ts +230 -0
  411. package/src/web/auth.test.ts +335 -0
  412. package/src/web/auth.ts +214 -0
  413. package/src/web/discord-validate.test.ts +77 -0
  414. package/src/web/discord-validate.ts +88 -0
  415. package/src/web/hub-discovery.test.ts +98 -0
  416. package/src/web/hub-discovery.ts +69 -0
  417. package/src/web/routes/activity.ts +106 -0
  418. package/src/web/routes/agent-provider.test.ts +282 -0
  419. package/src/web/routes/agent-provider.ts +309 -0
  420. package/src/web/routes/approvals.ts +185 -0
  421. package/src/web/routes/apps.ts +434 -0
  422. package/src/web/routes/channels-mg-detail.test.ts +324 -0
  423. package/src/web/routes/channels-mga-detail.test.ts +425 -0
  424. package/src/web/routes/channels.ts +489 -0
  425. package/src/web/routes/oauth-providers.ts +42 -0
  426. package/src/web/routes/secrets.test.ts +175 -0
  427. package/src/web/routes/secrets.ts +282 -0
  428. package/src/web/routes/sessions.ts +123 -0
  429. package/src/web/routes/settings.test.ts +106 -0
  430. package/src/web/routes/settings.ts +247 -0
  431. package/src/web/routes/setup-status.ts +205 -0
  432. package/src/web/routes/vaults.test.ts +389 -0
  433. package/src/web/routes/vaults.ts +225 -0
  434. package/src/web/server-version.test.ts +16 -0
  435. package/src/web/server.ts +1003 -0
  436. package/src/web/services-manifest.test.ts +120 -0
  437. package/src/web/services-manifest.ts +61 -0
  438. package/src/web/static-serve.test.ts +255 -0
  439. package/src/web/static-serve.ts +104 -0
  440. package/src/web/telegram-validate.test.ts +116 -0
  441. package/src/web/telegram-validate.ts +107 -0
  442. package/src/web/vault-proxy.test.ts +214 -0
  443. package/src/web/vault-proxy.ts +120 -0
  444. package/src/web/wire-channel.ts +181 -0
  445. package/src/webhook-server.ts +134 -0
  446. package/tsconfig.json +21 -0
  447. package/vitest.config.ts +18 -0
  448. package/web/README.md +63 -0
  449. package/web/ui/index.html +13 -0
  450. package/web/ui/package.json +35 -0
  451. package/web/ui/pnpm-lock.yaml +2164 -0
  452. package/web/ui/scripts/verify-base.mjs +31 -0
  453. package/web/ui/src/App.tsx +88 -0
  454. package/web/ui/src/components/ActivityFeed.tsx +444 -0
  455. package/web/ui/src/components/AgentGroupPicker.tsx +263 -0
  456. package/web/ui/src/components/AgentProviderCards.tsx +220 -0
  457. package/web/ui/src/components/CredentialForm.tsx +214 -0
  458. package/web/ui/src/components/ScopeGrants.tsx +74 -0
  459. package/web/ui/src/components/StatusDot.tsx +43 -0
  460. package/web/ui/src/components/VaultPicker.tsx +127 -0
  461. package/web/ui/src/components/setup/AdapterInstallStep.tsx +178 -0
  462. package/web/ui/src/components/setup/AgentGroupStep.tsx +43 -0
  463. package/web/ui/src/components/setup/ChannelPickStep.tsx +74 -0
  464. package/web/ui/src/components/setup/DoneStep.tsx +49 -0
  465. package/web/ui/src/components/setup/PrereqStep.tsx +129 -0
  466. package/web/ui/src/components/setup/TestConnectionStep.tsx +108 -0
  467. package/web/ui/src/components/setup/TestMessageStep.tsx +104 -0
  468. package/web/ui/src/components/setup/WireChannelStep.tsx +166 -0
  469. package/web/ui/src/components/setup/types.ts +105 -0
  470. package/web/ui/src/lib/api.test.ts +410 -0
  471. package/web/ui/src/lib/api.ts +1210 -0
  472. package/web/ui/src/lib/auth.test.ts +139 -0
  473. package/web/ui/src/lib/auth.ts +348 -0
  474. package/web/ui/src/lib/channel-adapters.ts +136 -0
  475. package/web/ui/src/main.tsx +19 -0
  476. package/web/ui/src/routes/ApprovalsList.tsx +294 -0
  477. package/web/ui/src/routes/Apps.tsx +613 -0
  478. package/web/ui/src/routes/ChannelWireDetail.test.tsx +233 -0
  479. package/web/ui/src/routes/ChannelWireDetail.tsx +403 -0
  480. package/web/ui/src/routes/ChannelsList.tsx +158 -0
  481. package/web/ui/src/routes/GroupDetail.tsx +755 -0
  482. package/web/ui/src/routes/GroupList.tsx +187 -0
  483. package/web/ui/src/routes/MessagingGroupDetail.test.tsx +233 -0
  484. package/web/ui/src/routes/MessagingGroupDetail.tsx +306 -0
  485. package/web/ui/src/routes/NewGroupWizard.tsx +390 -0
  486. package/web/ui/src/routes/OAuthCallback.tsx +56 -0
  487. package/web/ui/src/routes/SecretsList.tsx +921 -0
  488. package/web/ui/src/routes/SessionsList.tsx +220 -0
  489. package/web/ui/src/routes/SettingsAgentProvider.tsx +109 -0
  490. package/web/ui/src/routes/SettingsApprovals.tsx +234 -0
  491. package/web/ui/src/routes/SetupWizard.tsx +219 -0
  492. package/web/ui/src/routes/VaultDetail.test.tsx +361 -0
  493. package/web/ui/src/routes/VaultDetail.tsx +960 -0
  494. package/web/ui/src/routes/VaultsList.tsx +295 -0
  495. package/web/ui/src/routes/WireChannelPage.tsx +413 -0
  496. package/web/ui/src/styles.css +608 -0
  497. package/web/ui/src/test/setup.ts +23 -0
  498. package/web/ui/src/vite-env.d.ts +10 -0
  499. package/web/ui/tsconfig.json +20 -0
  500. package/web/ui/vite.config.ts +34 -0
  501. package/web/ui/vitest.config.ts +25 -0
@@ -0,0 +1,389 @@
1
+ /**
2
+ * Route-level tests for `/api/vaults*`. The proxy primitives are exercised
3
+ * in `src/web/vault-proxy.test.ts`; this file covers the dispatcher in
4
+ * `routes/vaults.ts` — URL pattern matching, the attached-to-group merge
5
+ * by tokenLabel, vault-not-found 404s, and refresh-cache invalidation.
6
+ *
7
+ * Strategy: stub global `fetch` for the HTTP calls (vault REST + hub
8
+ * well-known), and `vi.mock` the DB/attachment readers so the test
9
+ * doesn't need a real central DB or filesystem groups dir.
10
+ */
11
+ import http from 'node:http';
12
+
13
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
14
+
15
+ import { clearHubDiscoveryCache } from '../hub-discovery.js';
16
+
17
+ vi.mock('../../db/connection.js', () => ({
18
+ openDb: () => ({
19
+ prepare: () => ({ all: () => [{ folder: 'group-a' }, { folder: 'group-b' }] }),
20
+ close: () => {},
21
+ }),
22
+ }));
23
+
24
+ vi.mock('../../parachute/vault-mcp.js', () => ({
25
+ listVaultAttachments: vi.fn(() => []),
26
+ }));
27
+
28
+ import { listVaultAttachments } from '../../parachute/vault-mcp.js';
29
+ import { handleVaultsRoute } from './vaults.js';
30
+
31
+ const mockedListVaultAttachments = listVaultAttachments as unknown as ReturnType<typeof vi.fn>;
32
+
33
+ let prevHub: string | undefined;
34
+
35
+ beforeEach(() => {
36
+ prevHub = process.env.PARACHUTE_HUB_ORIGIN;
37
+ delete process.env.PARACHUTE_AGENT_HUB_ORIGIN;
38
+ delete process.env.PARACLAW_HUB_ORIGIN;
39
+ process.env.PARACHUTE_HUB_ORIGIN = 'https://parachute.example';
40
+ clearHubDiscoveryCache();
41
+ mockedListVaultAttachments.mockReset();
42
+ mockedListVaultAttachments.mockReturnValue([]);
43
+ });
44
+
45
+ afterEach(() => {
46
+ if (prevHub === undefined) delete process.env.PARACHUTE_HUB_ORIGIN;
47
+ else process.env.PARACHUTE_HUB_ORIGIN = prevHub;
48
+ clearHubDiscoveryCache();
49
+ vi.unstubAllGlobals();
50
+ });
51
+
52
+ interface FakeResponse {
53
+ statusCode: number;
54
+ body: unknown;
55
+ headers: Record<string, string>;
56
+ res: http.ServerResponse;
57
+ }
58
+
59
+ function fakeRes(): FakeResponse {
60
+ const captured: FakeResponse = {
61
+ statusCode: 0,
62
+ body: undefined,
63
+ headers: {},
64
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
65
+ res: undefined as any,
66
+ };
67
+ const res = {
68
+ writeHead(status: number, headers: Record<string, string>) {
69
+ captured.statusCode = status;
70
+ captured.headers = headers;
71
+ },
72
+ end(chunk: string) {
73
+ try {
74
+ captured.body = chunk ? JSON.parse(chunk) : undefined;
75
+ } catch {
76
+ captured.body = chunk;
77
+ }
78
+ },
79
+ } as unknown as http.ServerResponse;
80
+ captured.res = res;
81
+ return captured;
82
+ }
83
+
84
+ function fakeReq(body?: unknown): http.IncomingMessage {
85
+ if (body === undefined) {
86
+ return Object.assign(Object.create(null), {
87
+ [Symbol.asyncIterator]: async function* () {},
88
+ }) as http.IncomingMessage;
89
+ }
90
+ const buf = Buffer.from(JSON.stringify(body));
91
+ return Object.assign(Object.create(null), {
92
+ [Symbol.asyncIterator]: async function* () {
93
+ yield buf;
94
+ },
95
+ }) as http.IncomingMessage;
96
+ }
97
+
98
+ function jsonOk(body: unknown, status = 200): Response {
99
+ return new Response(JSON.stringify(body), {
100
+ status,
101
+ headers: { 'content-type': 'application/json' },
102
+ });
103
+ }
104
+
105
+ /** Each call returns a fresh Response — Response bodies are single-use. */
106
+ function alwaysOk(body: unknown, status = 200) {
107
+ return vi.fn().mockImplementation(async () => jsonOk(body, status));
108
+ }
109
+
110
+ const HUB_VAULTS_BODY = {
111
+ vaults: [
112
+ { name: 'work', url: 'https://h/vault/work', version: '0.4.7' },
113
+ { name: 'personal', url: 'https://h/vault/personal', version: '0.4.7' },
114
+ ],
115
+ };
116
+
117
+ describe('handleVaultsRoute', () => {
118
+ it('GET /api/vaults returns the hub well-known list', async () => {
119
+ vi.stubGlobal('fetch', alwaysOk(HUB_VAULTS_BODY));
120
+ const cap = fakeRes();
121
+ const handled = await handleVaultsRoute({
122
+ pathname: '/api/vaults',
123
+ method: 'GET',
124
+ url: new URL('https://x/api/vaults'),
125
+ req: fakeReq(),
126
+ res: cap.res,
127
+ authHeader: 'Bearer x',
128
+ });
129
+ expect(handled).toBe(true);
130
+ expect(cap.statusCode).toBe(200);
131
+ expect(cap.body).toEqual({ vaults: HUB_VAULTS_BODY.vaults });
132
+ });
133
+
134
+ it('POST /api/vaults/refresh clears the cache so the next call refetches', async () => {
135
+ const stub = alwaysOk(HUB_VAULTS_BODY);
136
+ vi.stubGlobal('fetch', stub);
137
+ // Prime the cache via GET.
138
+ await handleVaultsRoute({
139
+ pathname: '/api/vaults',
140
+ method: 'GET',
141
+ url: new URL('https://x/api/vaults'),
142
+ req: fakeReq(),
143
+ res: fakeRes().res,
144
+ authHeader: 'Bearer x',
145
+ });
146
+ expect(stub).toHaveBeenCalledTimes(1);
147
+ // Same call — cache hit, no extra fetch.
148
+ await handleVaultsRoute({
149
+ pathname: '/api/vaults',
150
+ method: 'GET',
151
+ url: new URL('https://x/api/vaults'),
152
+ req: fakeReq(),
153
+ res: fakeRes().res,
154
+ authHeader: 'Bearer x',
155
+ });
156
+ expect(stub).toHaveBeenCalledTimes(1);
157
+ // Refresh forces a refetch.
158
+ const cap = fakeRes();
159
+ await handleVaultsRoute({
160
+ pathname: '/api/vaults/refresh',
161
+ method: 'POST',
162
+ url: new URL('https://x/api/vaults/refresh'),
163
+ req: fakeReq(),
164
+ res: cap.res,
165
+ authHeader: 'Bearer x',
166
+ });
167
+ expect(cap.statusCode).toBe(200);
168
+ expect(stub).toHaveBeenCalledTimes(2);
169
+ });
170
+
171
+ it('GET /api/vaults/:name returns 404 when the vault is unknown', async () => {
172
+ vi.stubGlobal('fetch', alwaysOk(HUB_VAULTS_BODY));
173
+ const cap = fakeRes();
174
+ await handleVaultsRoute({
175
+ pathname: '/api/vaults/ghost',
176
+ method: 'GET',
177
+ url: new URL('https://x/api/vaults/ghost'),
178
+ req: fakeReq(),
179
+ res: cap.res,
180
+ authHeader: 'Bearer x',
181
+ });
182
+ expect(cap.statusCode).toBe(404);
183
+ expect(cap.body).toMatchObject({ error: expect.stringContaining('ghost') });
184
+ });
185
+
186
+ it('GET /api/vaults/:name surfaces attached groups via listVaultAttachments', async () => {
187
+ vi.stubGlobal('fetch', alwaysOk(HUB_VAULTS_BODY));
188
+ mockedListVaultAttachments.mockReturnValue([
189
+ {
190
+ folder: 'group-a',
191
+ mcpName: 'parachute-vault',
192
+ attachment: {
193
+ vaultBaseUrl: 'https://h/vault/work',
194
+ scope: 'vault:read',
195
+ tokenLabel: 'claw-a',
196
+ attachedAt: '2026-04-29T00:00:00Z',
197
+ },
198
+ },
199
+ {
200
+ folder: 'group-b',
201
+ mcpName: 'parachute-vault',
202
+ attachment: {
203
+ // Different vault — must not appear in the response.
204
+ vaultBaseUrl: 'https://h/vault/personal',
205
+ scope: 'vault:write',
206
+ tokenLabel: 'claw-b',
207
+ attachedAt: '2026-04-29T00:00:00Z',
208
+ },
209
+ },
210
+ ]);
211
+ const cap = fakeRes();
212
+ await handleVaultsRoute({
213
+ pathname: '/api/vaults/work',
214
+ method: 'GET',
215
+ url: new URL('https://x/api/vaults/work'),
216
+ req: fakeReq(),
217
+ res: cap.res,
218
+ authHeader: 'Bearer x',
219
+ });
220
+ expect(cap.statusCode).toBe(200);
221
+ const body = cap.body as { vault: unknown; attachedGroups: Array<{ folder: string }> };
222
+ expect(body.vault).toMatchObject({ name: 'work' });
223
+ expect(body.attachedGroups).toHaveLength(1);
224
+ expect(body.attachedGroups[0]).toMatchObject({ folder: 'group-a', tokenLabel: 'claw-a' });
225
+ });
226
+
227
+ it('GET /api/vaults/:name/tokens enriches each token with attachedTo by tokenLabel', async () => {
228
+ const fetchStub = vi
229
+ .fn()
230
+ // 1st call: hub well-known (resolveVaultBaseUrl)
231
+ .mockResolvedValueOnce(jsonOk(HUB_VAULTS_BODY))
232
+ // 2nd call: vault GET /tokens
233
+ .mockResolvedValueOnce(
234
+ jsonOk({
235
+ tokens: [
236
+ { id: 't_1', label: 'claw-work', scopes: ['vault:read'] },
237
+ { id: 't_2', label: 'orphan-token', scopes: ['vault:read'] },
238
+ ],
239
+ }),
240
+ );
241
+ vi.stubGlobal('fetch', fetchStub);
242
+ mockedListVaultAttachments.mockReturnValue([
243
+ {
244
+ folder: 'group-a',
245
+ mcpName: 'parachute-vault',
246
+ attachment: {
247
+ vaultBaseUrl: 'https://h/vault/work',
248
+ scope: 'vault:read',
249
+ tokenLabel: 'claw-work',
250
+ attachedAt: '2026-04-29T00:00:00Z',
251
+ },
252
+ },
253
+ ]);
254
+
255
+ const cap = fakeRes();
256
+ await handleVaultsRoute({
257
+ pathname: '/api/vaults/work/tokens',
258
+ method: 'GET',
259
+ url: new URL('https://x/api/vaults/work/tokens'),
260
+ req: fakeReq(),
261
+ res: cap.res,
262
+ authHeader: 'Bearer the-jwt',
263
+ });
264
+ expect(cap.statusCode).toBe(200);
265
+ const body = cap.body as { tokens: Array<{ id: string; attachedTo: Array<{ folder: string }> }> };
266
+ const claw = body.tokens.find((t) => t.id === 't_1');
267
+ const orphan = body.tokens.find((t) => t.id === 't_2');
268
+ expect(claw?.attachedTo).toEqual([{ folder: 'group-a', scope: 'vault:read' }]);
269
+ expect(orphan?.attachedTo).toEqual([]);
270
+ // Vault was hit with the operator's JWT verbatim.
271
+ expect(fetchStub).toHaveBeenLastCalledWith(
272
+ 'https://h/vault/work/tokens',
273
+ expect.objectContaining({
274
+ headers: expect.objectContaining({ Authorization: 'Bearer the-jwt' }),
275
+ }),
276
+ );
277
+ });
278
+
279
+ it('GET /api/vaults/:name/tokens mirrors a vault 401 verbatim (consent prompt path)', async () => {
280
+ const fetchStub = vi
281
+ .fn()
282
+ .mockResolvedValueOnce(jsonOk(HUB_VAULTS_BODY))
283
+ .mockResolvedValueOnce(jsonOk({ error: 'missing vault:work:admin' }, 401));
284
+ vi.stubGlobal('fetch', fetchStub);
285
+ const cap = fakeRes();
286
+ await handleVaultsRoute({
287
+ pathname: '/api/vaults/work/tokens',
288
+ method: 'GET',
289
+ url: new URL('https://x/api/vaults/work/tokens'),
290
+ req: fakeReq(),
291
+ res: cap.res,
292
+ authHeader: 'Bearer x',
293
+ });
294
+ expect(cap.statusCode).toBe(401);
295
+ expect(cap.body).toMatchObject({ error: 'missing vault:work:admin' });
296
+ });
297
+
298
+ it('POST /api/vaults/:name/tokens forwards body and mirrors the vault 201', async () => {
299
+ const fetchStub = vi
300
+ .fn()
301
+ .mockResolvedValueOnce(jsonOk(HUB_VAULTS_BODY))
302
+ .mockResolvedValueOnce(jsonOk({ id: 't_new', token: 'pvt_new', label: 'fresh' }, 201));
303
+ vi.stubGlobal('fetch', fetchStub);
304
+ const cap = fakeRes();
305
+ await handleVaultsRoute({
306
+ pathname: '/api/vaults/work/tokens',
307
+ method: 'POST',
308
+ url: new URL('https://x/api/vaults/work/tokens'),
309
+ req: fakeReq({ label: 'fresh', scopes: ['vault:read'] }),
310
+ res: cap.res,
311
+ authHeader: 'Bearer x',
312
+ });
313
+ expect(cap.statusCode).toBe(201);
314
+ expect(cap.body).toMatchObject({ id: 't_new' });
315
+ const [, init] = fetchStub.mock.calls[1] as [string, RequestInit];
316
+ expect(JSON.parse(init.body as string)).toMatchObject({
317
+ label: 'fresh',
318
+ scopes: ['vault:read'],
319
+ });
320
+ });
321
+
322
+ it('POST /api/vaults/:name/tokens returns 400 on invalid JSON body', async () => {
323
+ vi.stubGlobal('fetch', alwaysOk(HUB_VAULTS_BODY));
324
+ const badReq = Object.assign(Object.create(null), {
325
+ [Symbol.asyncIterator]: async function* () {
326
+ yield Buffer.from('{not-json');
327
+ },
328
+ }) as http.IncomingMessage;
329
+ const cap = fakeRes();
330
+ await handleVaultsRoute({
331
+ pathname: '/api/vaults/work/tokens',
332
+ method: 'POST',
333
+ url: new URL('https://x/api/vaults/work/tokens'),
334
+ req: badReq,
335
+ res: cap.res,
336
+ authHeader: 'Bearer x',
337
+ });
338
+ expect(cap.statusCode).toBe(400);
339
+ });
340
+
341
+ it('DELETE /api/vaults/:name/tokens/:id forwards and mirrors the vault 200', async () => {
342
+ const fetchStub = vi
343
+ .fn()
344
+ .mockResolvedValueOnce(jsonOk(HUB_VAULTS_BODY))
345
+ .mockResolvedValueOnce(jsonOk({ ok: true }));
346
+ vi.stubGlobal('fetch', fetchStub);
347
+ const cap = fakeRes();
348
+ await handleVaultsRoute({
349
+ pathname: '/api/vaults/work/tokens/t_abc123',
350
+ method: 'DELETE',
351
+ url: new URL('https://x/api/vaults/work/tokens/t_abc123'),
352
+ req: fakeReq(),
353
+ res: cap.res,
354
+ authHeader: 'Bearer x',
355
+ });
356
+ expect(cap.statusCode).toBe(200);
357
+ expect(fetchStub).toHaveBeenLastCalledWith(
358
+ 'https://h/vault/work/tokens/t_abc123',
359
+ expect.objectContaining({ method: 'DELETE' }),
360
+ );
361
+ });
362
+
363
+ it('returns 404 for token routes when vault name is unknown', async () => {
364
+ vi.stubGlobal('fetch', alwaysOk(HUB_VAULTS_BODY));
365
+ const cap = fakeRes();
366
+ await handleVaultsRoute({
367
+ pathname: '/api/vaults/ghost/tokens',
368
+ method: 'GET',
369
+ url: new URL('https://x/api/vaults/ghost/tokens'),
370
+ req: fakeReq(),
371
+ res: cap.res,
372
+ authHeader: 'Bearer x',
373
+ });
374
+ expect(cap.statusCode).toBe(404);
375
+ });
376
+
377
+ it('returns false for an unmatched path so the dispatcher falls through', async () => {
378
+ const cap = fakeRes();
379
+ const handled = await handleVaultsRoute({
380
+ pathname: '/api/something-else',
381
+ method: 'GET',
382
+ url: new URL('https://x/api/something-else'),
383
+ req: fakeReq(),
384
+ res: cap.res,
385
+ authHeader: 'Bearer x',
386
+ });
387
+ expect(handled).toBe(false);
388
+ });
389
+ });
@@ -0,0 +1,225 @@
1
+ /**
2
+ * /api/vaults — vault management surface for `/agent/vaults`.
3
+ *
4
+ * Five endpoints, plus a list endpoint that lived inline in `server.ts`
5
+ * before this module landed. All admin operations forward the operator's
6
+ * hub-issued session JWT to the vault unmodified — see
7
+ * `docs/design/2026-04-29-vault-management-ui.md` § Admin auth model and
8
+ * `src/web/vault-proxy.ts` for the rationale.
9
+ *
10
+ * Scope at the paraclaw boundary is checked by `server.ts` via
11
+ * `pickVaultScope()` before dispatch reaches this handler. The vault
12
+ * validates `vault:<name>:admin` independently — paraclaw doesn't downgrade
13
+ * or re-issue. A 401/403 from the vault is mirrored verbatim so the
14
+ * browser can trigger an OAuth consent flow for the missing narrow scope.
15
+ */
16
+ import http from 'node:http';
17
+
18
+ import { CENTRAL_DB_PATH } from '../../config.js';
19
+ import { openDb } from '../../db/connection.js';
20
+ import { listVaultAttachments } from '../../parachute/vault-mcp.js';
21
+ import { clearHubDiscoveryCache, fetchHubVaults, type VaultListing } from '../hub-discovery.js';
22
+ import { forwardToVault, resolveVaultBaseUrl } from '../vault-proxy.js';
23
+
24
+ interface VaultTokenRecord {
25
+ id: string;
26
+ label: string;
27
+ scopes?: string[];
28
+ permission?: string;
29
+ expires_at?: string | null;
30
+ created_at?: string;
31
+ last_used_at?: string | null;
32
+ }
33
+
34
+ const json = (res: http.ServerResponse, status: number, body: unknown): void => {
35
+ res.writeHead(status, { 'content-type': 'application/json' });
36
+ res.end(JSON.stringify(body));
37
+ };
38
+
39
+ const error = (res: http.ServerResponse, status: number, message: string): void =>
40
+ json(res, status, { error: message });
41
+
42
+ async function readJsonBody<T>(req: http.IncomingMessage): Promise<T> {
43
+ const chunks: Buffer[] = [];
44
+ for await (const chunk of req) chunks.push(chunk as Buffer);
45
+ if (chunks.length === 0) return {} as T;
46
+ return JSON.parse(Buffer.concat(chunks).toString('utf8')) as T;
47
+ }
48
+
49
+ function listGroupFolders(): string[] {
50
+ const db = openDb(CENTRAL_DB_PATH, { readonly: true });
51
+ try {
52
+ return db
53
+ .prepare<{ folder: string }>('SELECT folder FROM agent_groups')
54
+ .all()
55
+ .map((r) => r.folder);
56
+ } finally {
57
+ db.close();
58
+ }
59
+ }
60
+
61
+ /**
62
+ * Build the "attached to" map for a vault: which agent groups currently
63
+ * have a parachute.json record pointing at this vault, keyed by tokenLabel
64
+ * for cheap merge into the tokens listing.
65
+ *
66
+ * Match by `vaultBaseUrl` (canonical, hub-published form, trailing slashes
67
+ * already trimmed at write time in `attachVaultToGroup`). Tokens without a
68
+ * matching group are still surfaced — they show as orphans in the UI.
69
+ */
70
+ function buildAttachedByLabel(vaultBaseUrl: string): Map<string, { folder: string; scope: string }[]> {
71
+ const target = vaultBaseUrl.replace(/\/+$/, '');
72
+ const folders = listGroupFolders();
73
+ const entries = listVaultAttachments(folders);
74
+ const byLabel = new Map<string, { folder: string; scope: string }[]>();
75
+ for (const e of entries) {
76
+ if (e.attachment.vaultBaseUrl.replace(/\/+$/, '') !== target) continue;
77
+ const list = byLabel.get(e.attachment.tokenLabel) ?? [];
78
+ list.push({ folder: e.folder, scope: e.attachment.scope });
79
+ byLabel.set(e.attachment.tokenLabel, list);
80
+ }
81
+ return byLabel;
82
+ }
83
+
84
+ export interface VaultsRouteContext {
85
+ pathname: string;
86
+ method: string;
87
+ url: URL;
88
+ req: http.IncomingMessage;
89
+ res: http.ServerResponse;
90
+ /** Operator's full `Authorization` header value, forwarded to vault. */
91
+ authHeader: string;
92
+ }
93
+
94
+ export async function handleVaultsRoute(ctx: VaultsRouteContext): Promise<boolean> {
95
+ const { pathname, method, req, res, authHeader } = ctx;
96
+
97
+ if (pathname === '/api/vaults' && method === 'GET') {
98
+ try {
99
+ const vaults = await fetchHubVaults();
100
+ json(res, 200, { vaults });
101
+ } catch (err) {
102
+ error(res, 502, err instanceof Error ? err.message : String(err));
103
+ }
104
+ return true;
105
+ }
106
+
107
+ if (pathname === '/api/vaults/refresh' && method === 'POST') {
108
+ clearHubDiscoveryCache();
109
+ try {
110
+ const vaults = await fetchHubVaults();
111
+ json(res, 200, { vaults });
112
+ } catch (err) {
113
+ error(res, 502, err instanceof Error ? err.message : String(err));
114
+ }
115
+ return true;
116
+ }
117
+
118
+ // Token-id-bound routes — match before the parent /tokens path so the
119
+ // more-specific match wins.
120
+ const tokenById = pathname.match(/^\/api\/vaults\/([^/]+)\/tokens\/([^/]+)$/);
121
+ if (tokenById && method === 'DELETE') {
122
+ const name = decodeURIComponent(tokenById[1]);
123
+ const id = decodeURIComponent(tokenById[2]);
124
+ const baseUrl = await resolveVaultBaseUrl(name);
125
+ if (!baseUrl) {
126
+ error(res, 404, `vault not found: ${name}`);
127
+ return true;
128
+ }
129
+ const result = await forwardToVault({
130
+ method: 'DELETE',
131
+ vaultBaseUrl: baseUrl,
132
+ subpath: `/tokens/${encodeURIComponent(id)}`,
133
+ authHeader,
134
+ });
135
+ json(res, result.status, result.body);
136
+ return true;
137
+ }
138
+
139
+ const tokensList = pathname.match(/^\/api\/vaults\/([^/]+)\/tokens$/);
140
+ if (tokensList) {
141
+ const name = decodeURIComponent(tokensList[1]);
142
+ const baseUrl = await resolveVaultBaseUrl(name);
143
+ if (!baseUrl) {
144
+ error(res, 404, `vault not found: ${name}`);
145
+ return true;
146
+ }
147
+ if (method === 'GET') {
148
+ const result = await forwardToVault({
149
+ method: 'GET',
150
+ vaultBaseUrl: baseUrl,
151
+ subpath: '/tokens',
152
+ authHeader,
153
+ });
154
+ if (result.status >= 400) {
155
+ json(res, result.status, result.body);
156
+ return true;
157
+ }
158
+ // Merge attached-to-group derivation by tokenLabel. Vault returns
159
+ // `{tokens: [{id, label, ...}]}`; we add `attachedTo: [{folder, scope}]`
160
+ // to each row so the UI doesn't need a separate fetch.
161
+ const tokens = (result.body as { tokens?: VaultTokenRecord[] }).tokens ?? [];
162
+ const byLabel = buildAttachedByLabel(baseUrl);
163
+ const enriched = tokens.map((t) => ({
164
+ ...t,
165
+ attachedTo: byLabel.get(t.label) ?? [],
166
+ }));
167
+ json(res, 200, { tokens: enriched });
168
+ return true;
169
+ }
170
+ if (method === 'POST') {
171
+ let body: { label?: string; scopes?: unknown; expires_at?: string | null };
172
+ try {
173
+ body = await readJsonBody(req);
174
+ } catch {
175
+ error(res, 400, 'invalid JSON body');
176
+ return true;
177
+ }
178
+ const result = await forwardToVault({
179
+ method: 'POST',
180
+ vaultBaseUrl: baseUrl,
181
+ subpath: '/tokens',
182
+ authHeader,
183
+ body,
184
+ });
185
+ json(res, result.status, result.body);
186
+ return true;
187
+ }
188
+ error(res, 405, `method not allowed: ${method} ${pathname}`);
189
+ return true;
190
+ }
191
+
192
+ const detail = pathname.match(/^\/api\/vaults\/([^/]+)$/);
193
+ if (detail && method === 'GET') {
194
+ const name = decodeURIComponent(detail[1]);
195
+ let vaults: VaultListing[];
196
+ try {
197
+ vaults = await fetchHubVaults();
198
+ } catch (err) {
199
+ error(res, 502, err instanceof Error ? err.message : String(err));
200
+ return true;
201
+ }
202
+ const hit = vaults.find((v) => v.name === name);
203
+ if (!hit) {
204
+ error(res, 404, `vault not found: ${name}`);
205
+ return true;
206
+ }
207
+ const folders = listGroupFolders();
208
+ const entries = listVaultAttachments(folders).filter(
209
+ (e) => e.attachment.vaultBaseUrl.replace(/\/+$/, '') === hit.url.replace(/\/+$/, ''),
210
+ );
211
+ json(res, 200, {
212
+ vault: hit,
213
+ attachedGroups: entries.map((e) => ({
214
+ folder: e.folder,
215
+ mcpName: e.mcpName,
216
+ scope: e.attachment.scope,
217
+ tokenLabel: e.attachment.tokenLabel,
218
+ attachedAt: e.attachment.attachedAt,
219
+ })),
220
+ });
221
+ return true;
222
+ }
223
+
224
+ return false;
225
+ }
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Regression test for paraclaw#101 — SERVICE_VERSION must read from
3
+ * package.json, not be hardcoded. Without this dynamism, every rc bump
4
+ * silently lies about the running version.
5
+ */
6
+ import { describe, expect, test } from 'vitest';
7
+
8
+ import { SERVICE_VERSION } from './server.js';
9
+ import pkg from '../../package.json' with { type: 'json' };
10
+
11
+ describe('SERVICE_VERSION', () => {
12
+ test('matches package.json version (no hardcoded drift)', () => {
13
+ expect(SERVICE_VERSION).toBe(pkg.version);
14
+ expect(SERVICE_VERSION).toMatch(/^\d+\.\d+\.\d+(-[a-z]+\.\d+)?$/);
15
+ });
16
+ });