@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,355 @@
1
+ /**
2
+ * Public API for paraclaw's secret store. Values are AES-256-GCM encrypted
3
+ * in-process before landing in the central DB; decrypted only when injected
4
+ * into per-session containers (`src/container-runner.ts`).
5
+ *
6
+ * Naming: a secret is keyed by `(name, agent_group_id)`. A NULL agent_group_id
7
+ * is global; a non-NULL agent_group_id scopes the secret to that group only.
8
+ *
9
+ * Resolution preference at injection time: agent-scoped secret with that
10
+ * name beats the global one. The host walks both rows and the scoped wins.
11
+ *
12
+ * Injection policy lives on the recipient `agent_groups.secret_mode` row
13
+ * (migration 023): `all` injects every in-scope secret; `selective` injects
14
+ * only those with an explicit `secret_assignments` row pointing to the group.
15
+ */
16
+ import crypto from 'crypto';
17
+
18
+ import { getDb } from '../db/connection.js';
19
+ import type { Database } from '../db/connection.js';
20
+ import { decryptSecret, deriveKey, encryptSecret } from './crypto.js';
21
+ import { loadOrCreateMasterKey } from './master-key.js';
22
+
23
+ // Domain tag for HKDF-derived secrets-store key. Bumping the version (v2…)
24
+ // would force re-encryption of every row in this table. See crypto.ts.
25
+ //
26
+ // ⚠ The `paraclaw.` prefix is a cryptographic domain separator and must
27
+ // stay frozen across the paraclaw → parachute-agent rename. Renaming it
28
+ // changes the derived key and renders every existing ciphertext row
29
+ // undecryptable. The brand-sweep documentation lives in commit messages
30
+ // and CHANGELOG; the bytes here do not change.
31
+ const SECRETS_INFO = 'paraclaw.secrets.v1';
32
+
33
+ function secretsKey(): Buffer {
34
+ return deriveKey(loadOrCreateMasterKey(), SECRETS_INFO);
35
+ }
36
+
37
+ export type SecretKind = 'channel-token' | 'api-key' | 'generic';
38
+ export type AssignedMode = 'all' | 'selective';
39
+
40
+ export interface SecretRow {
41
+ id: string;
42
+ name: string;
43
+ kind: SecretKind;
44
+ agent_group_id: string | null;
45
+ created_at: string;
46
+ updated_at: string;
47
+ }
48
+
49
+ export interface PutSecretOpts {
50
+ kind?: SecretKind;
51
+ agent_group_id?: string | null;
52
+ }
53
+
54
+ interface RawRow extends SecretRow {
55
+ value_encrypted: string;
56
+ }
57
+
58
+ function db(): Database {
59
+ return getDb();
60
+ }
61
+
62
+ function nowIso(): string {
63
+ return new Date().toISOString();
64
+ }
65
+
66
+ /** Insert or update a secret. Returns the row's id. */
67
+ export function putSecret(name: string, value: string, opts: PutSecretOpts = {}): string {
68
+ const key = secretsKey();
69
+ const ct = encryptSecret(value, key);
70
+ const agentGroupId = opts.agent_group_id ?? null;
71
+ const kind = opts.kind ?? 'generic';
72
+
73
+ const existing = db()
74
+ .prepare<{ id: string }>(`SELECT id FROM secrets WHERE name = @name AND agent_group_id IS @agent_group_id`)
75
+ .get({ name, agent_group_id: agentGroupId });
76
+
77
+ const now = nowIso();
78
+ if (existing) {
79
+ db()
80
+ .prepare(
81
+ `UPDATE secrets
82
+ SET value_encrypted = @value_encrypted,
83
+ kind = @kind,
84
+ updated_at = @updated_at
85
+ WHERE id = @id`,
86
+ )
87
+ .run({
88
+ id: existing.id,
89
+ value_encrypted: ct,
90
+ kind,
91
+ updated_at: now,
92
+ });
93
+ return existing.id;
94
+ }
95
+
96
+ const id = crypto.randomUUID();
97
+ db()
98
+ .prepare(
99
+ `INSERT INTO secrets
100
+ (id, name, value_encrypted, kind, agent_group_id, created_at, updated_at)
101
+ VALUES
102
+ (@id, @name, @value_encrypted, @kind, @agent_group_id, @created_at, @updated_at)`,
103
+ )
104
+ .run({
105
+ id,
106
+ name,
107
+ value_encrypted: ct,
108
+ kind,
109
+ agent_group_id: agentGroupId,
110
+ created_at: now,
111
+ updated_at: now,
112
+ });
113
+ return id;
114
+ }
115
+
116
+ /**
117
+ * Decrypt and return a secret's plaintext value. Returns undefined if the
118
+ * named secret does not exist for the given scope. Resolution: an
119
+ * agent-scoped secret beats a global one with the same name.
120
+ */
121
+ export function getSecret(name: string, agentGroupId?: string | null): string | undefined {
122
+ const key = secretsKey();
123
+ const scoped = agentGroupId
124
+ ? db()
125
+ .prepare<RawRow>(`SELECT * FROM secrets WHERE name = @name AND agent_group_id = @agent_group_id`)
126
+ .get({ name, agent_group_id: agentGroupId })
127
+ : undefined;
128
+ const row =
129
+ scoped ?? db().prepare<RawRow>(`SELECT * FROM secrets WHERE name = @name AND agent_group_id IS NULL`).get({ name });
130
+ if (!row) return undefined;
131
+ return decryptSecret(row.value_encrypted, key);
132
+ }
133
+
134
+ /** Names + metadata only — never decrypts. */
135
+ export function listSecrets(agentGroupId?: string | null): SecretRow[] {
136
+ if (agentGroupId === undefined) {
137
+ return db()
138
+ .prepare<SecretRow>(
139
+ `SELECT id, name, kind, agent_group_id, created_at, updated_at
140
+ FROM secrets ORDER BY name`,
141
+ )
142
+ .all();
143
+ }
144
+ if (agentGroupId === null) {
145
+ return db()
146
+ .prepare<SecretRow>(
147
+ `SELECT id, name, kind, agent_group_id, created_at, updated_at
148
+ FROM secrets WHERE agent_group_id IS NULL ORDER BY name`,
149
+ )
150
+ .all();
151
+ }
152
+ return db()
153
+ .prepare<SecretRow>(
154
+ `SELECT id, name, kind, agent_group_id, created_at, updated_at
155
+ FROM secrets
156
+ WHERE agent_group_id = @agent_group_id OR agent_group_id IS NULL
157
+ ORDER BY name`,
158
+ )
159
+ .all({ agent_group_id: agentGroupId });
160
+ }
161
+
162
+ export function deleteSecret(id: string): boolean {
163
+ const r = db().prepare(`DELETE FROM secrets WHERE id = @id`).run({ id });
164
+ return r.changes > 0;
165
+ }
166
+
167
+ /**
168
+ * Resolve the secrets that should be injected into a session for the given
169
+ * agent group. Returns plaintext values; callers are expected to inject as
170
+ * env vars and never log. Agent-scoped wins over global on name collision.
171
+ *
172
+ * Mode lives on the recipient `agent_groups.secret_mode`:
173
+ * - `all` — inject every in-scope secret (scoped + globals).
174
+ * - `selective` — inject only those with an explicit assignment row
175
+ * pointing to this group. Lets operators stage credentials
176
+ * in the store before any agent gets them and revoke per
177
+ * agent without rotating the value.
178
+ *
179
+ * Unknown agent_group_id is treated as `selective` (the safe default) — the
180
+ * group-level row is the source of truth, so a missing row means we err on
181
+ * the side of withholding.
182
+ */
183
+ export function resolveInjectableSecrets(agentGroupId: string): Map<string, string> {
184
+ const key = secretsKey();
185
+ const rows = db()
186
+ .prepare<RawRow>(
187
+ `SELECT s.*
188
+ FROM secrets s
189
+ LEFT JOIN secret_assignments a
190
+ ON a.secret_id = s.id
191
+ AND a.agent_group_id = @agent_group_id
192
+ LEFT JOIN agent_groups g
193
+ ON g.id = @agent_group_id
194
+ WHERE (s.agent_group_id = @agent_group_id OR s.agent_group_id IS NULL)
195
+ AND (g.secret_mode = 'all' OR a.secret_id IS NOT NULL)
196
+ ORDER BY s.agent_group_id IS NULL`,
197
+ )
198
+ .all({ agent_group_id: agentGroupId });
199
+
200
+ const out = new Map<string, string>();
201
+ for (const row of rows) {
202
+ if (out.has(row.name)) continue;
203
+ out.set(row.name, decryptSecret(row.value_encrypted, key));
204
+ }
205
+ return out;
206
+ }
207
+
208
+ // ── Assignments ──
209
+
210
+ export interface SecretAssignment {
211
+ secret_id: string;
212
+ agent_group_id: string;
213
+ created_at: string;
214
+ }
215
+
216
+ /** All agent_group_ids assigned to this secret (selective-mode allowlist). */
217
+ export function listAssignments(secretId: string): string[] {
218
+ const rows = db()
219
+ .prepare<{ agent_group_id: string }>(
220
+ `SELECT agent_group_id FROM secret_assignments
221
+ WHERE secret_id = @secret_id
222
+ ORDER BY agent_group_id`,
223
+ )
224
+ .all({ secret_id: secretId });
225
+ return rows.map((r) => r.agent_group_id);
226
+ }
227
+
228
+ /**
229
+ * Replace the assignment set atomically. Empty array = revoke everything.
230
+ * Throws if the secret doesn't exist; FK ON DELETE CASCADE handles agent
231
+ * groups that vanish. The whole replace runs inside one transaction so
232
+ * the UI's "save" button is all-or-nothing.
233
+ */
234
+ export function replaceAssignments(secretId: string, agentGroupIds: string[]): void {
235
+ const exists = db().prepare<{ id: string }>(`SELECT id FROM secrets WHERE id = @id`).get({ id: secretId });
236
+ if (!exists) throw new Error(`secret not found: ${secretId}`);
237
+ const now = nowIso();
238
+ db().transaction(() => {
239
+ db().prepare(`DELETE FROM secret_assignments WHERE secret_id = @secret_id`).run({ secret_id: secretId });
240
+ const insert = db().prepare(
241
+ `INSERT INTO secret_assignments (secret_id, agent_group_id, created_at)
242
+ VALUES (@secret_id, @agent_group_id, @created_at)`,
243
+ );
244
+ for (const gid of agentGroupIds) {
245
+ insert.run({ secret_id: secretId, agent_group_id: gid, created_at: now });
246
+ }
247
+ })();
248
+ }
249
+
250
+ /** Idempotent — re-adding an existing assignment is a no-op (composite PK). */
251
+ export function addAssignment(secretId: string, agentGroupId: string): boolean {
252
+ const r = db()
253
+ .prepare(
254
+ `INSERT INTO secret_assignments (secret_id, agent_group_id, created_at)
255
+ VALUES (@secret_id, @agent_group_id, @created_at)
256
+ ON CONFLICT (secret_id, agent_group_id) DO NOTHING`,
257
+ )
258
+ .run({ secret_id: secretId, agent_group_id: agentGroupId, created_at: nowIso() });
259
+ return r.changes > 0;
260
+ }
261
+
262
+ export function removeAssignment(secretId: string, agentGroupId: string): boolean {
263
+ const r = db()
264
+ .prepare(`DELETE FROM secret_assignments WHERE secret_id = @secret_id AND agent_group_id = @agent_group_id`)
265
+ .run({ secret_id: secretId, agent_group_id: agentGroupId });
266
+ return r.changes > 0;
267
+ }
268
+
269
+ // ── Staleness detection (Bug B) ──
270
+
271
+ export interface StaleSession {
272
+ sessionId: string;
273
+ agentGroupId: string;
274
+ agentGroupName: string;
275
+ agentGroupFolder: string;
276
+ sessionCreatedAt: string;
277
+ secretUpdatedAt: string;
278
+ }
279
+
280
+ /**
281
+ * Sessions whose container was spawned BEFORE this secret was last updated
282
+ * AND whose agent group would inject the secret. The injection predicate
283
+ * mirrors `resolveInjectableSecrets` for the configurations the UI can
284
+ * actually create:
285
+ * - scoped secret → matches its parent group (`s.agent_group_id = g.id`)
286
+ * - global secret → matches any group with `secret_mode='all'` OR an
287
+ * explicit `secret_assignments` row
288
+ *
289
+ * Note on a subtle asymmetry: `resolveInjectableSecrets` additionally gates
290
+ * scoped secrets through `(secret_mode='all' OR assignment row exists)` on
291
+ * the recipient group. The SQL here accepts the scoped match unconditionally.
292
+ * The asymmetry is benign — the only configs where it would diverge (a
293
+ * scoped secret paired with its parent group in `selective` mode and no
294
+ * assignment row) are unreachable via the UI, which always seeds an
295
+ * assignment row when scoping. If a future code path makes that config
296
+ * reachable, tighten the SQL to add the same gate.
297
+ *
298
+ * The host injects env vars at spawn time only — there is no in-process
299
+ * update path. This helper powers the post-save banner that prompts the
300
+ * operator to restart the specific sessions that need to see the change.
301
+ *
302
+ * Returns empty when the secret doesn't exist (caller handles 404).
303
+ */
304
+ export function findStaleSessionsForSecret(secretId: string): StaleSession[] {
305
+ const rows = db()
306
+ .prepare<{
307
+ session_id: string;
308
+ agent_group_id: string;
309
+ agent_group_name: string;
310
+ agent_group_folder: string;
311
+ session_created_at: string;
312
+ secret_updated_at: string;
313
+ }>(
314
+ `SELECT
315
+ sess.id AS session_id,
316
+ g.id AS agent_group_id,
317
+ g.name AS agent_group_name,
318
+ g.folder AS agent_group_folder,
319
+ sess.created_at AS session_created_at,
320
+ s.updated_at AS secret_updated_at
321
+ FROM secrets s
322
+ JOIN agent_groups g
323
+ ON s.agent_group_id = g.id
324
+ OR s.agent_group_id IS NULL
325
+ LEFT JOIN secret_assignments a
326
+ ON a.secret_id = s.id AND a.agent_group_id = g.id
327
+ JOIN sessions sess
328
+ ON sess.agent_group_id = g.id
329
+ WHERE s.id = @secret_id
330
+ AND sess.container_status = 'running'
331
+ AND sess.created_at < s.updated_at
332
+ AND (
333
+ s.agent_group_id = g.id
334
+ OR (s.agent_group_id IS NULL
335
+ AND (g.secret_mode = 'all' OR a.secret_id IS NOT NULL))
336
+ )
337
+ ORDER BY sess.created_at DESC`,
338
+ )
339
+ .all({ secret_id: secretId });
340
+ return rows.map((r) => ({
341
+ sessionId: r.session_id,
342
+ agentGroupId: r.agent_group_id,
343
+ agentGroupName: r.agent_group_name,
344
+ agentGroupFolder: r.agent_group_folder,
345
+ sessionCreatedAt: r.session_created_at,
346
+ secretUpdatedAt: r.secret_updated_at,
347
+ }));
348
+ }
349
+
350
+ /** Metadata-only single-row read by id. Returns undefined if missing. */
351
+ export function getSecretById(id: string): SecretRow | undefined {
352
+ return db()
353
+ .prepare<SecretRow>(`SELECT id, name, kind, agent_group_id, created_at, updated_at FROM secrets WHERE id = ?`)
354
+ .get(id);
355
+ }
@@ -0,0 +1,70 @@
1
+ /**
2
+ * Master key bootstrap. Stores a 32-byte (256-bit) random key at
3
+ * `~/.parachute/agent/master.key` with mode 0600. Generated on first start;
4
+ * loaded from disk on subsequent starts.
5
+ *
6
+ * The key is never written to logs, never sent over the wire, never put in
7
+ * env vars. Loss of the file = loss of every encrypted secret (no recovery
8
+ * path); rotation requires re-encrypting every row.
9
+ */
10
+ import crypto from 'crypto';
11
+ import fs from 'fs';
12
+ import path from 'path';
13
+
14
+ import { CENTRAL_DB_DIR } from '../config.js';
15
+
16
+ const KEY_LEN = 32;
17
+ // `master.key` lives next to the central DB so a single backup of
18
+ // `<PARACHUTE_DIR>/agent/` captures both crypto material and DB state, and
19
+ // so `PARACHUTE_HOME` overrides reroute both atoms together — sandboxes
20
+ // that override the home dir get a fresh DB AND a fresh master.key.
21
+ const KEY_DIR = CENTRAL_DB_DIR;
22
+ const KEY_PATH = path.join(KEY_DIR, 'master.key');
23
+
24
+ let cached: Buffer | null = null;
25
+
26
+ export function getMasterKeyPath(): string {
27
+ return KEY_PATH;
28
+ }
29
+
30
+ export function loadOrCreateMasterKey(): Buffer {
31
+ if (cached) return cached;
32
+
33
+ if (fs.existsSync(KEY_PATH)) {
34
+ // Refuse to load a key file that's group/world readable. The file was
35
+ // created with mode 0600; if something has loosened it (chmod, restore
36
+ // from a backup tarball, etc.) we'd rather fail loud than silently keep
37
+ // serving secrets out of a file anyone on the box can read.
38
+ const stat = fs.statSync(KEY_PATH);
39
+ const perm = stat.mode & 0o777;
40
+ if ((stat.mode & 0o077) !== 0) {
41
+ throw new Error(
42
+ `Master key at ${KEY_PATH} has permissive mode 0${perm.toString(8).padStart(3, '0')}; ` +
43
+ `expected 0600. Run: chmod 600 ${KEY_PATH}`,
44
+ );
45
+ }
46
+ const buf = fs.readFileSync(KEY_PATH);
47
+ if (buf.length !== KEY_LEN) {
48
+ throw new Error(`Master key at ${KEY_PATH} is ${buf.length} bytes; expected ${KEY_LEN}`);
49
+ }
50
+ cached = buf;
51
+ return buf;
52
+ }
53
+
54
+ fs.mkdirSync(KEY_DIR, { recursive: true, mode: 0o700 });
55
+ const key = crypto.randomBytes(KEY_LEN);
56
+ fs.writeFileSync(KEY_PATH, key, { mode: 0o600 });
57
+ cached = key;
58
+ return key;
59
+ }
60
+
61
+ /** Test-only: clear the cached key so a different one can be loaded. */
62
+ export function _resetMasterKeyCache(): void {
63
+ cached = null;
64
+ }
65
+
66
+ /** Test-only: install a key without touching disk. */
67
+ export function _setMasterKeyForTest(key: Buffer): void {
68
+ if (key.length !== KEY_LEN) throw new Error(`test key must be ${KEY_LEN} bytes`);
69
+ cached = key;
70
+ }