@openparachute/agent 0.1.1 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (598) hide show
  1. package/.parachute/module.json +124 -8
  2. package/LICENSE +2 -16
  3. package/README.md +118 -166
  4. package/package.json +32 -43
  5. package/scripts/spawn-agent.ts +371 -0
  6. package/src/_parked/interactive-spawn.test.ts +324 -0
  7. package/src/_parked/interactive-spawn.ts +701 -0
  8. package/src/agent-defs.test.ts +1504 -0
  9. package/src/agent-defs.ts +1702 -0
  10. package/src/agent-mcp-config.test.ts +115 -0
  11. package/src/agent-mcp-config.ts +115 -0
  12. package/src/agents.test.ts +360 -0
  13. package/src/agents.ts +379 -0
  14. package/src/auth.test.ts +46 -0
  15. package/src/auth.ts +140 -0
  16. package/src/backends/attached-queue.test.ts +376 -0
  17. package/src/backends/attached-queue.ts +372 -0
  18. package/src/backends/programmatic.test.ts +1715 -0
  19. package/src/backends/programmatic.ts +927 -0
  20. package/src/backends/registry.test.ts +1494 -0
  21. package/src/backends/registry.ts +1202 -0
  22. package/src/backends/stream-json.test.ts +570 -0
  23. package/src/backends/stream-json.ts +392 -0
  24. package/src/backends/types.ts +223 -0
  25. package/src/bridge.ts +417 -0
  26. package/src/channel-backend-wiring.test.ts +237 -0
  27. package/src/credentials.test.ts +274 -0
  28. package/src/credentials.ts +380 -0
  29. package/src/cron.test.ts +342 -0
  30. package/src/cron.ts +380 -0
  31. package/src/daemon-agent-def-api.test.ts +166 -0
  32. package/src/daemon-agent-defs-api.test.ts +953 -0
  33. package/src/daemon-agent-env-api.test.ts +338 -0
  34. package/src/daemon-attached-queue-store.test.ts +65 -0
  35. package/src/daemon-config-api.test.ts +962 -0
  36. package/src/daemon-jobs-api.test.ts +271 -0
  37. package/src/daemon-vault-chat.test.ts +250 -0
  38. package/src/daemon.test.ts +746 -0
  39. package/src/daemon.ts +3314 -0
  40. package/src/def-vaults.test.ts +136 -0
  41. package/src/def-vaults.ts +165 -0
  42. package/src/delivery-state.test.ts +110 -0
  43. package/src/delivery-state.ts +154 -0
  44. package/src/effective-env.test.ts +114 -0
  45. package/src/effective-env.ts +184 -0
  46. package/src/env-compat.ts +39 -0
  47. package/src/grants.test.ts +638 -0
  48. package/src/grants.ts +675 -0
  49. package/src/hub-jwt.test.ts +161 -0
  50. package/src/hub-jwt.ts +182 -0
  51. package/src/jobs.test.ts +245 -0
  52. package/src/jobs.ts +266 -0
  53. package/src/mcp-http.test.ts +265 -0
  54. package/src/mcp-http.ts +771 -0
  55. package/src/mint-token.test.ts +152 -0
  56. package/src/mint-token.ts +139 -0
  57. package/src/module-manifest.test.ts +158 -0
  58. package/src/oauth-discovery.ts +134 -0
  59. package/src/programmatic-wiring.test.ts +838 -0
  60. package/src/registry.test.ts +227 -0
  61. package/src/registry.ts +228 -0
  62. package/src/resolve-port.test.ts +64 -0
  63. package/src/routing.test.ts +184 -0
  64. package/src/routing.ts +76 -0
  65. package/src/runner.test.ts +506 -0
  66. package/src/runner.ts +255 -0
  67. package/src/sandbox/config.test.ts +150 -0
  68. package/src/sandbox/config.ts +102 -0
  69. package/src/sandbox/egress.test.ts +113 -0
  70. package/src/sandbox/egress.ts +123 -0
  71. package/src/sandbox/index.ts +180 -0
  72. package/src/sandbox/live-seatbelt.test.ts +277 -0
  73. package/src/sandbox/mounts.test.ts +154 -0
  74. package/src/sandbox/mounts.ts +133 -0
  75. package/src/sandbox/sandbox.test.ts +168 -0
  76. package/src/sandbox/types.ts +382 -0
  77. package/src/services-manifest.test.ts +106 -0
  78. package/src/services-manifest.ts +95 -0
  79. package/src/spa-serve.test.ts +116 -0
  80. package/src/spa-serve.ts +116 -0
  81. package/src/spawn-agent-cli.test.ts +172 -0
  82. package/src/spawn-agent.test.ts +1218 -0
  83. package/src/spawn-agent.ts +569 -0
  84. package/src/spawn-deps.test.ts +54 -0
  85. package/src/spawn-deps.ts +166 -0
  86. package/src/telegram/api.ts +153 -0
  87. package/src/terminal-assets.test.ts +50 -0
  88. package/src/terminal-assets.ts +79 -0
  89. package/src/terminal-ui.ts +305 -0
  90. package/src/terminal.test.ts +530 -0
  91. package/src/terminal.ts +458 -0
  92. package/src/transport.ts +270 -0
  93. package/src/transports/http-ui.test.ts +455 -0
  94. package/src/transports/http-ui.ts +201 -0
  95. package/src/transports/telegram.test.ts +174 -0
  96. package/src/transports/telegram.ts +426 -0
  97. package/src/transports/vault.test.ts +2011 -0
  98. package/src/transports/vault.ts +1790 -0
  99. package/src/ui-kit.test.ts +178 -0
  100. package/src/ui-kit.ts +402 -0
  101. package/tsconfig.json +8 -14
  102. package/web/ui/tsconfig.json +2 -1
  103. package/.claude/scheduled_tasks.lock +0 -1
  104. package/.claude/settings.json +0 -5
  105. package/.claude/skills/add-atomic-chat-tool/SKILL.md +0 -243
  106. package/.claude/skills/add-atomic-chat-tool/atomic-chat-mcp-stdio.ts +0 -229
  107. package/.claude/skills/add-codex/SKILL.md +0 -161
  108. package/.claude/skills/add-dashboard/SKILL.md +0 -138
  109. package/.claude/skills/add-dashboard/resources/dashboard-pusher.ts +0 -495
  110. package/.claude/skills/add-emacs/SKILL.md +0 -296
  111. package/.claude/skills/add-gcal-tool/SKILL.md +0 -210
  112. package/.claude/skills/add-gchat/REMOVE.md +0 -6
  113. package/.claude/skills/add-gchat/SKILL.md +0 -92
  114. package/.claude/skills/add-gchat/VERIFY.md +0 -3
  115. package/.claude/skills/add-github/REMOVE.md +0 -6
  116. package/.claude/skills/add-github/SKILL.md +0 -148
  117. package/.claude/skills/add-github/VERIFY.md +0 -3
  118. package/.claude/skills/add-gmail-tool/SKILL.md +0 -229
  119. package/.claude/skills/add-imessage/REMOVE.md +0 -6
  120. package/.claude/skills/add-imessage/SKILL.md +0 -113
  121. package/.claude/skills/add-imessage/VERIFY.md +0 -3
  122. package/.claude/skills/add-karpathy-llm-wiki/SKILL.md +0 -110
  123. package/.claude/skills/add-karpathy-llm-wiki/llm-wiki.md +0 -75
  124. package/.claude/skills/add-linear/REMOVE.md +0 -6
  125. package/.claude/skills/add-linear/SKILL.md +0 -168
  126. package/.claude/skills/add-linear/VERIFY.md +0 -3
  127. package/.claude/skills/add-macos-statusbar/SKILL.md +0 -133
  128. package/.claude/skills/add-macos-statusbar/add/src/statusbar.swift +0 -147
  129. package/.claude/skills/add-matrix/REMOVE.md +0 -6
  130. package/.claude/skills/add-matrix/SKILL.md +0 -148
  131. package/.claude/skills/add-matrix/VERIFY.md +0 -3
  132. package/.claude/skills/add-ollama-provider/SKILL.md +0 -179
  133. package/.claude/skills/add-ollama-tool/SKILL.md +0 -193
  134. package/.claude/skills/add-opencode/SKILL.md +0 -229
  135. package/.claude/skills/add-parallel/SKILL.md +0 -290
  136. package/.claude/skills/add-resend/REMOVE.md +0 -6
  137. package/.claude/skills/add-resend/SKILL.md +0 -93
  138. package/.claude/skills/add-resend/VERIFY.md +0 -3
  139. package/.claude/skills/add-signal/REMOVE.md +0 -13
  140. package/.claude/skills/add-signal/SKILL.md +0 -318
  141. package/.claude/skills/add-signal/VERIFY.md +0 -5
  142. package/.claude/skills/add-slack/REMOVE.md +0 -6
  143. package/.claude/skills/add-slack/SKILL.md +0 -112
  144. package/.claude/skills/add-slack/VERIFY.md +0 -3
  145. package/.claude/skills/add-teams/REMOVE.md +0 -6
  146. package/.claude/skills/add-teams/SKILL.md +0 -207
  147. package/.claude/skills/add-teams/VERIFY.md +0 -3
  148. package/.claude/skills/add-vercel/SKILL.md +0 -147
  149. package/.claude/skills/add-vercel/container-skills/vercel-cli/SKILL.md +0 -103
  150. package/.claude/skills/add-webex/REMOVE.md +0 -6
  151. package/.claude/skills/add-webex/SKILL.md +0 -88
  152. package/.claude/skills/add-webex/VERIFY.md +0 -3
  153. package/.claude/skills/add-wechat/REMOVE.md +0 -49
  154. package/.claude/skills/add-wechat/SKILL.md +0 -170
  155. package/.claude/skills/add-wechat/scripts/wire-dm.ts +0 -172
  156. package/.claude/skills/add-whatsapp/SKILL.md +0 -264
  157. package/.claude/skills/add-whatsapp-cloud/REMOVE.md +0 -6
  158. package/.claude/skills/add-whatsapp-cloud/SKILL.md +0 -95
  159. package/.claude/skills/add-whatsapp-cloud/VERIFY.md +0 -3
  160. package/.claude/skills/claw/SKILL.md +0 -131
  161. package/.claude/skills/claw/scripts/claw +0 -374
  162. package/.claude/skills/convert-to-apple-container/SKILL.md +0 -212
  163. package/.claude/skills/customize/SKILL.md +0 -110
  164. package/.claude/skills/debug/SKILL.md +0 -349
  165. package/.claude/skills/get-qodo-rules/SKILL.md +0 -122
  166. package/.claude/skills/get-qodo-rules/references/output-format.md +0 -41
  167. package/.claude/skills/get-qodo-rules/references/pagination.md +0 -33
  168. package/.claude/skills/get-qodo-rules/references/repository-scope.md +0 -26
  169. package/.claude/skills/init-first-agent/SKILL.md +0 -120
  170. package/.claude/skills/init-onecli/SKILL.md +0 -270
  171. package/.claude/skills/manage-channels/SKILL.md +0 -87
  172. package/.claude/skills/manage-mounts/SKILL.md +0 -47
  173. package/.claude/skills/migrate-from-openclaw/MIGRATE_CRONS.md +0 -100
  174. package/.claude/skills/migrate-from-openclaw/SKILL.md +0 -447
  175. package/.claude/skills/migrate-from-openclaw/scripts/discover-openclaw.ts +0 -734
  176. package/.claude/skills/migrate-from-openclaw/scripts/extract-channel-credentials.ts +0 -476
  177. package/.claude/skills/migrate-nanoclaw/SKILL.md +0 -484
  178. package/.claude/skills/migrate-nanoclaw/diagnostics.md +0 -51
  179. package/.claude/skills/qodo-pr-resolver/SKILL.md +0 -326
  180. package/.claude/skills/qodo-pr-resolver/resources/providers.md +0 -329
  181. package/.claude/skills/update-nanoclaw/SKILL.md +0 -243
  182. package/.claude/skills/update-nanoclaw/diagnostics.md +0 -48
  183. package/.claude/skills/update-skills/SKILL.md +0 -130
  184. package/.claude/skills/use-native-credential-proxy/SKILL.md +0 -167
  185. package/.claude/skills/x-integration/SKILL.md +0 -417
  186. package/.claude/skills/x-integration/agent.ts +0 -243
  187. package/.claude/skills/x-integration/host.ts +0 -155
  188. package/.claude/skills/x-integration/lib/browser.ts +0 -148
  189. package/.claude/skills/x-integration/lib/config.ts +0 -62
  190. package/.claude/skills/x-integration/scripts/like.ts +0 -56
  191. package/.claude/skills/x-integration/scripts/post.ts +0 -66
  192. package/.claude/skills/x-integration/scripts/quote.ts +0 -80
  193. package/.claude/skills/x-integration/scripts/reply.ts +0 -74
  194. package/.claude/skills/x-integration/scripts/retweet.ts +0 -62
  195. package/.claude/skills/x-integration/scripts/setup.ts +0 -87
  196. package/.github/CODEOWNERS +0 -10
  197. package/.github/PULL_REQUEST_TEMPLATE.md +0 -18
  198. package/.github/workflows/bump-version.yml +0 -35
  199. package/.github/workflows/ci.yml +0 -39
  200. package/.github/workflows/label-pr.yml +0 -40
  201. package/.github/workflows/update-tokens.yml +0 -43
  202. package/.husky/pre-commit +0 -1
  203. package/.mcp.json +0 -3
  204. package/.nvmrc +0 -1
  205. package/.prettierrc +0 -4
  206. package/CHANGELOG.md +0 -221
  207. package/CLAUDE.md +0 -307
  208. package/CODE_OF_CONDUCT.md +0 -128
  209. package/CONTRIBUTING.md +0 -159
  210. package/CONTRIBUTORS.md +0 -26
  211. package/LICENSE-NANOCLAW-MIT +0 -21
  212. package/README_ja.md +0 -194
  213. package/README_zh.md +0 -194
  214. package/assets/nanoclaw-favicon.png +0 -0
  215. package/assets/nanoclaw-icon.png +0 -0
  216. package/assets/nanoclaw-logo-dark.png +0 -0
  217. package/assets/nanoclaw-logo.png +0 -0
  218. package/assets/nanoclaw-profile.jpeg +0 -0
  219. package/assets/nanoclaw-sales.png +0 -0
  220. package/assets/social-preview.jpg +0 -0
  221. package/config-examples/mount-allowlist.json +0 -25
  222. package/container/.dockerignore +0 -2
  223. package/container/CLAUDE.md +0 -21
  224. package/container/Dockerfile +0 -121
  225. package/container/agent-runner/bun.lock +0 -243
  226. package/container/agent-runner/package.json +0 -22
  227. package/container/agent-runner/scripts/sdk-signal-probe.ts +0 -169
  228. package/container/agent-runner/src/config.ts +0 -55
  229. package/container/agent-runner/src/db/connection.ts +0 -267
  230. package/container/agent-runner/src/db/index.ts +0 -20
  231. package/container/agent-runner/src/db/messages-in.ts +0 -138
  232. package/container/agent-runner/src/db/messages-out.ts +0 -143
  233. package/container/agent-runner/src/db/session-routing.ts +0 -30
  234. package/container/agent-runner/src/db/session-state.test.ts +0 -100
  235. package/container/agent-runner/src/db/session-state.ts +0 -79
  236. package/container/agent-runner/src/destinations.ts +0 -135
  237. package/container/agent-runner/src/formatter.test.ts +0 -167
  238. package/container/agent-runner/src/formatter.ts +0 -260
  239. package/container/agent-runner/src/index.ts +0 -110
  240. package/container/agent-runner/src/integration.test.ts +0 -121
  241. package/container/agent-runner/src/mcp-tools/agents.instructions.md +0 -26
  242. package/container/agent-runner/src/mcp-tools/agents.ts +0 -66
  243. package/container/agent-runner/src/mcp-tools/core.instructions.md +0 -27
  244. package/container/agent-runner/src/mcp-tools/core.ts +0 -262
  245. package/container/agent-runner/src/mcp-tools/index.ts +0 -22
  246. package/container/agent-runner/src/mcp-tools/interactive.instructions.md +0 -22
  247. package/container/agent-runner/src/mcp-tools/interactive.ts +0 -169
  248. package/container/agent-runner/src/mcp-tools/scheduling.instructions.md +0 -40
  249. package/container/agent-runner/src/mcp-tools/scheduling.ts +0 -299
  250. package/container/agent-runner/src/mcp-tools/self-mod.instructions.md +0 -25
  251. package/container/agent-runner/src/mcp-tools/self-mod.ts +0 -120
  252. package/container/agent-runner/src/mcp-tools/server.ts +0 -54
  253. package/container/agent-runner/src/mcp-tools/types.ts +0 -6
  254. package/container/agent-runner/src/poll-loop.test.ts +0 -248
  255. package/container/agent-runner/src/poll-loop.ts +0 -437
  256. package/container/agent-runner/src/providers/claude.ts +0 -379
  257. package/container/agent-runner/src/providers/factory.test.ts +0 -19
  258. package/container/agent-runner/src/providers/factory.ts +0 -13
  259. package/container/agent-runner/src/providers/index.ts +0 -6
  260. package/container/agent-runner/src/providers/mock.ts +0 -77
  261. package/container/agent-runner/src/providers/provider-registry.ts +0 -33
  262. package/container/agent-runner/src/providers/types.ts +0 -82
  263. package/container/agent-runner/src/scheduling/task-script.ts +0 -121
  264. package/container/agent-runner/src/timezone.test.ts +0 -93
  265. package/container/agent-runner/src/timezone.ts +0 -107
  266. package/container/agent-runner/tsconfig.json +0 -14
  267. package/container/build.sh +0 -48
  268. package/container/entrypoint.sh +0 -16
  269. package/container/skills/agent-browser/SKILL.md +0 -159
  270. package/container/skills/frontend-engineer/SKILL.md +0 -157
  271. package/container/skills/self-customize/SKILL.md +0 -87
  272. package/container/skills/slack-formatting/SKILL.md +0 -94
  273. package/container/skills/vercel-cli/SKILL.md +0 -111
  274. package/container/skills/welcome/SKILL.md +0 -85
  275. package/docs/APPLE-CONTAINER-NETWORKING.md +0 -90
  276. package/docs/BRANCH-FORK-MAINTENANCE.md +0 -81
  277. package/docs/README.md +0 -25
  278. package/docs/SDK_DEEP_DIVE.md +0 -643
  279. package/docs/SECURITY.md +0 -162
  280. package/docs/agent-runner-details.md +0 -749
  281. package/docs/api-details.md +0 -365
  282. package/docs/architecture-diagram.html +0 -422
  283. package/docs/architecture-diagram.md +0 -215
  284. package/docs/architecture.md +0 -751
  285. package/docs/audit/2026-04-30-channel-endpoint-audit.md +0 -36
  286. package/docs/build-and-runtime.md +0 -80
  287. package/docs/cross-mount-stress/README.md +0 -112
  288. package/docs/cross-mount-stress/container-writer-retry.mjs +0 -55
  289. package/docs/cross-mount-stress/container-writer-slow.mjs +0 -42
  290. package/docs/cross-mount-stress/container-writer.mjs +0 -47
  291. package/docs/cross-mount-stress/host-writer-retry.mjs +0 -55
  292. package/docs/cross-mount-stress/host-writer-slow.mjs +0 -43
  293. package/docs/cross-mount-stress/host-writer.mjs +0 -47
  294. package/docs/db-central.md +0 -316
  295. package/docs/db-session.md +0 -183
  296. package/docs/db.md +0 -119
  297. package/docs/design/2026-04-29-vault-management-ui.md +0 -231
  298. package/docs/design/2026-04-30-channel-wiring-rework.md +0 -234
  299. package/docs/design/2026-05-01-channel-wiring-approvals-deep-dive.md +0 -272
  300. package/docs/design/2026-05-02-channel-policy-and-approval-routing.md +0 -250
  301. package/docs/docker-sandboxes.md +0 -359
  302. package/docs/isolation-model.md +0 -88
  303. package/docs/ollama.md +0 -79
  304. package/docs/parachute-integration.md +0 -109
  305. package/docs/post-night-rebirth-reflections.md +0 -151
  306. package/eslint.config.js +0 -32
  307. package/pnpm-workspace.yaml +0 -8
  308. package/repo-tokens/README.md +0 -113
  309. package/repo-tokens/action.yml +0 -186
  310. package/repo-tokens/badge.svg +0 -23
  311. package/repo-tokens/examples/green.svg +0 -14
  312. package/repo-tokens/examples/red.svg +0 -14
  313. package/repo-tokens/examples/yellow-green.svg +0 -14
  314. package/repo-tokens/examples/yellow.svg +0 -14
  315. package/scripts/chat.ts +0 -101
  316. package/scripts/cleanup-sessions.sh +0 -150
  317. package/scripts/init-cli-agent.ts +0 -171
  318. package/scripts/init-first-agent.ts +0 -377
  319. package/scripts/parachute.ts +0 -158
  320. package/scripts/run-migrations.ts +0 -105
  321. package/scripts/sanity-live-poll.ts +0 -95
  322. package/scripts/seed-discord.ts +0 -79
  323. package/scripts/test-v2-agent.ts +0 -106
  324. package/scripts/test-v2-channel-e2e.ts +0 -265
  325. package/scripts/test-v2-host.ts +0 -184
  326. package/src/channels/adapter.ts +0 -214
  327. package/src/channels/ask-question.ts +0 -46
  328. package/src/channels/channel-registry.test.ts +0 -421
  329. package/src/channels/channel-registry.ts +0 -313
  330. package/src/channels/chat-sdk-bridge.test.ts +0 -84
  331. package/src/channels/chat-sdk-bridge.ts +0 -652
  332. package/src/channels/cli.ts +0 -276
  333. package/src/channels/discord.ts +0 -90
  334. package/src/channels/index.ts +0 -17
  335. package/src/channels/telegram-markdown-sanitize.test.ts +0 -78
  336. package/src/channels/telegram-markdown-sanitize.ts +0 -55
  337. package/src/channels/telegram-pairing.test.ts +0 -254
  338. package/src/channels/telegram-pairing.ts +0 -339
  339. package/src/channels/telegram.ts +0 -279
  340. package/src/channels/trust-hint.test.ts +0 -48
  341. package/src/channels/trust-hint.ts +0 -75
  342. package/src/claude-md-compose.migrate.test.ts +0 -64
  343. package/src/claude-md-compose.ts +0 -205
  344. package/src/command-gate.ts +0 -63
  345. package/src/config.test.ts +0 -93
  346. package/src/config.ts +0 -108
  347. package/src/container-config.ts +0 -167
  348. package/src/container-runner.test.ts +0 -32
  349. package/src/container-runner.ts +0 -576
  350. package/src/container-runtime.test.ts +0 -169
  351. package/src/container-runtime.ts +0 -92
  352. package/src/db/_bun-sqlite-shim.ts +0 -88
  353. package/src/db/agent-activity.test.ts +0 -155
  354. package/src/db/agent-activity.ts +0 -121
  355. package/src/db/agent-groups.ts +0 -77
  356. package/src/db/connection.migrate.test.ts +0 -143
  357. package/src/db/connection.ts +0 -224
  358. package/src/db/db-v2.test.ts +0 -440
  359. package/src/db/dropped-messages.ts +0 -44
  360. package/src/db/index.ts +0 -40
  361. package/src/db/messaging-groups.ts +0 -252
  362. package/src/db/migrations/001-initial.ts +0 -112
  363. package/src/db/migrations/002-chat-sdk-state.ts +0 -36
  364. package/src/db/migrations/008-dropped-messages.ts +0 -27
  365. package/src/db/migrations/009-drop-pending-credentials.ts +0 -13
  366. package/src/db/migrations/010-engage-modes.ts +0 -103
  367. package/src/db/migrations/011-pending-sender-approvals.ts +0 -40
  368. package/src/db/migrations/012-channel-registration.ts +0 -48
  369. package/src/db/migrations/013-approval-render-metadata.ts +0 -27
  370. package/src/db/migrations/014-secrets.ts +0 -44
  371. package/src/db/migrations/015-secrets-drop-host-pattern.ts +0 -18
  372. package/src/db/migrations/016-secret-assignments.ts +0 -30
  373. package/src/db/migrations/017-agent-activity.ts +0 -40
  374. package/src/db/migrations/018-oauth-app-configs.ts +0 -34
  375. package/src/db/migrations/019-oauth-app-connections.ts +0 -48
  376. package/src/db/migrations/020-agent-app-connections.ts +0 -28
  377. package/src/db/migrations/021-pending-oauth-states.ts +0 -35
  378. package/src/db/migrations/022-app-connections-provider.ts +0 -25
  379. package/src/db/migrations/023-agent-group-secret-mode.test.ts +0 -124
  380. package/src/db/migrations/023-agent-group-secret-mode.ts +0 -65
  381. package/src/db/migrations/024-collapse-approvals.test.ts +0 -249
  382. package/src/db/migrations/024-collapse-approvals.ts +0 -182
  383. package/src/db/migrations/025-secret-mode-check.test.ts +0 -155
  384. package/src/db/migrations/025-secret-mode-check.ts +0 -49
  385. package/src/db/migrations/026-user-dms-bot-id.test.ts +0 -116
  386. package/src/db/migrations/026-user-dms-bot-id.ts +0 -54
  387. package/src/db/migrations/027-provider-credentials.ts +0 -41
  388. package/src/db/migrations/_test-helpers.ts +0 -41
  389. package/src/db/migrations/index.ts +0 -127
  390. package/src/db/migrations/module-agent-to-agent-destinations.ts +0 -84
  391. package/src/db/migrations/module-approvals-pending-approvals.ts +0 -42
  392. package/src/db/migrations/module-approvals-title-options.ts +0 -40
  393. package/src/db/schema.ts +0 -258
  394. package/src/db/session-db.test.ts +0 -93
  395. package/src/db/session-db.ts +0 -325
  396. package/src/db/sessions.ts +0 -241
  397. package/src/delivery.test.ts +0 -148
  398. package/src/delivery.ts +0 -445
  399. package/src/env.ts +0 -74
  400. package/src/group-folder.test.ts +0 -35
  401. package/src/group-folder.ts +0 -44
  402. package/src/group-init.ts +0 -92
  403. package/src/host-core.test.ts +0 -456
  404. package/src/host-sweep.test.ts +0 -146
  405. package/src/host-sweep.ts +0 -287
  406. package/src/index.ts +0 -227
  407. package/src/install-slug.ts +0 -33
  408. package/src/log.test.ts +0 -81
  409. package/src/log.ts +0 -117
  410. package/src/mcp/http.ts +0 -72
  411. package/src/mcp/server.ts +0 -92
  412. package/src/mcp/stdio.ts +0 -51
  413. package/src/mcp/tools/activity.ts +0 -88
  414. package/src/mcp/tools/agent-groups.ts +0 -183
  415. package/src/mcp/tools/approvals.ts +0 -122
  416. package/src/mcp/tools/channels.ts +0 -199
  417. package/src/mcp/tools/index.ts +0 -27
  418. package/src/mcp/tools/oauth.ts +0 -48
  419. package/src/mcp/tools/secrets.ts +0 -169
  420. package/src/mcp/tools/sessions.ts +0 -135
  421. package/src/mcp/types.ts +0 -51
  422. package/src/modules/agent-to-agent/agent-route.test.ts +0 -46
  423. package/src/modules/agent-to-agent/agent-route.ts +0 -223
  424. package/src/modules/agent-to-agent/create-agent.ts +0 -127
  425. package/src/modules/agent-to-agent/db/agent-destinations.ts +0 -135
  426. package/src/modules/agent-to-agent/index.ts +0 -22
  427. package/src/modules/agent-to-agent/write-destinations.ts +0 -59
  428. package/src/modules/approvals/agent.md +0 -45
  429. package/src/modules/approvals/index.ts +0 -21
  430. package/src/modules/approvals/picks.test.ts +0 -291
  431. package/src/modules/approvals/primitive.ts +0 -279
  432. package/src/modules/approvals/project.md +0 -27
  433. package/src/modules/approvals/response-handler.ts +0 -87
  434. package/src/modules/index.ts +0 -24
  435. package/src/modules/interactive/agent.md +0 -21
  436. package/src/modules/interactive/index.ts +0 -69
  437. package/src/modules/interactive/project.md +0 -12
  438. package/src/modules/mount-security/index.ts +0 -448
  439. package/src/modules/mount-security/migrate.test.ts +0 -91
  440. package/src/modules/permissions/access.ts +0 -28
  441. package/src/modules/permissions/channel-approval.test.ts +0 -389
  442. package/src/modules/permissions/channel-approval.ts +0 -188
  443. package/src/modules/permissions/db/agent-group-members.ts +0 -44
  444. package/src/modules/permissions/db/pending-channel-approvals.test.ts +0 -86
  445. package/src/modules/permissions/db/pending-channel-approvals.ts +0 -66
  446. package/src/modules/permissions/db/pending-sender-approvals.ts +0 -60
  447. package/src/modules/permissions/db/user-dms.ts +0 -58
  448. package/src/modules/permissions/db/user-roles.ts +0 -85
  449. package/src/modules/permissions/db/users.ts +0 -38
  450. package/src/modules/permissions/index.ts +0 -421
  451. package/src/modules/permissions/permissions.test.ts +0 -358
  452. package/src/modules/permissions/sender-approval.test.ts +0 -470
  453. package/src/modules/permissions/sender-approval.ts +0 -165
  454. package/src/modules/permissions/user-dm.ts +0 -200
  455. package/src/modules/provider-credentials/db.ts +0 -121
  456. package/src/modules/provider-credentials/index.ts +0 -12
  457. package/src/modules/provider-credentials/spawn.test.ts +0 -206
  458. package/src/modules/provider-credentials/spawn.ts +0 -114
  459. package/src/modules/scheduling/actions.ts +0 -113
  460. package/src/modules/scheduling/db.test.ts +0 -282
  461. package/src/modules/scheduling/db.ts +0 -148
  462. package/src/modules/scheduling/index.ts +0 -34
  463. package/src/modules/scheduling/recurrence.test.ts +0 -98
  464. package/src/modules/scheduling/recurrence.ts +0 -54
  465. package/src/modules/self-mod/agent.md +0 -30
  466. package/src/modules/self-mod/apply.ts +0 -85
  467. package/src/modules/self-mod/index.ts +0 -30
  468. package/src/modules/self-mod/project.md +0 -39
  469. package/src/modules/self-mod/request.ts +0 -91
  470. package/src/modules/typing/index.ts +0 -165
  471. package/src/oauth/agent-app-connections.ts +0 -103
  472. package/src/oauth/app-configs.test.ts +0 -64
  473. package/src/oauth/app-configs.ts +0 -114
  474. package/src/oauth/app-connections.test.ts +0 -109
  475. package/src/oauth/app-connections.ts +0 -178
  476. package/src/oauth/crypto.ts +0 -56
  477. package/src/oauth/flow.ts +0 -104
  478. package/src/oauth/providers/google.test.ts +0 -38
  479. package/src/oauth/providers/google.ts +0 -46
  480. package/src/oauth/providers/index.ts +0 -48
  481. package/src/oauth/state-store.test.ts +0 -54
  482. package/src/oauth/state-store.ts +0 -93
  483. package/src/parachute/README.md +0 -27
  484. package/src/parachute/create-agent.test.ts +0 -83
  485. package/src/parachute/create-agent.ts +0 -122
  486. package/src/parachute/group-status.test.ts +0 -165
  487. package/src/parachute/group-status.ts +0 -136
  488. package/src/parachute/types.ts +0 -41
  489. package/src/parachute/vault-mcp.test.ts +0 -251
  490. package/src/parachute/vault-mcp.ts +0 -232
  491. package/src/platform-id.test.ts +0 -104
  492. package/src/platform-id.ts +0 -109
  493. package/src/providers/index.ts +0 -6
  494. package/src/providers/provider-container-registry.ts +0 -58
  495. package/src/response-registry.ts +0 -45
  496. package/src/router.ts +0 -530
  497. package/src/secrets/crypto.test.ts +0 -45
  498. package/src/secrets/crypto.ts +0 -55
  499. package/src/secrets/index.ts +0 -355
  500. package/src/secrets/master-key.ts +0 -70
  501. package/src/secrets/secrets.test.ts +0 -354
  502. package/src/session-manager.migrate.test.ts +0 -59
  503. package/src/session-manager.ts +0 -433
  504. package/src/startup-bootstrap.test.ts +0 -226
  505. package/src/startup-bootstrap.ts +0 -207
  506. package/src/state-sqlite.ts +0 -182
  507. package/src/timezone.test.ts +0 -64
  508. package/src/timezone.ts +0 -37
  509. package/src/types.ts +0 -230
  510. package/src/web/auth.test.ts +0 -335
  511. package/src/web/auth.ts +0 -214
  512. package/src/web/discord-validate.test.ts +0 -77
  513. package/src/web/discord-validate.ts +0 -88
  514. package/src/web/hub-discovery.test.ts +0 -98
  515. package/src/web/hub-discovery.ts +0 -69
  516. package/src/web/routes/activity.ts +0 -106
  517. package/src/web/routes/agent-provider.test.ts +0 -282
  518. package/src/web/routes/agent-provider.ts +0 -309
  519. package/src/web/routes/approvals.ts +0 -185
  520. package/src/web/routes/apps.ts +0 -434
  521. package/src/web/routes/channels-mg-detail.test.ts +0 -324
  522. package/src/web/routes/channels-mga-detail.test.ts +0 -425
  523. package/src/web/routes/channels.ts +0 -489
  524. package/src/web/routes/oauth-providers.ts +0 -42
  525. package/src/web/routes/secrets.test.ts +0 -175
  526. package/src/web/routes/secrets.ts +0 -282
  527. package/src/web/routes/sessions.ts +0 -123
  528. package/src/web/routes/settings.test.ts +0 -106
  529. package/src/web/routes/settings.ts +0 -247
  530. package/src/web/routes/setup-status.ts +0 -205
  531. package/src/web/routes/vaults.test.ts +0 -389
  532. package/src/web/routes/vaults.ts +0 -225
  533. package/src/web/server-version.test.ts +0 -16
  534. package/src/web/server.ts +0 -1003
  535. package/src/web/services-manifest.test.ts +0 -120
  536. package/src/web/services-manifest.ts +0 -61
  537. package/src/web/static-serve.test.ts +0 -255
  538. package/src/web/static-serve.ts +0 -104
  539. package/src/web/telegram-validate.test.ts +0 -116
  540. package/src/web/telegram-validate.ts +0 -107
  541. package/src/web/vault-proxy.test.ts +0 -214
  542. package/src/web/vault-proxy.ts +0 -120
  543. package/src/web/wire-channel.ts +0 -181
  544. package/src/webhook-server.ts +0 -134
  545. package/vitest.config.ts +0 -18
  546. package/web/README.md +0 -63
  547. package/web/ui/index.html +0 -13
  548. package/web/ui/package.json +0 -35
  549. package/web/ui/pnpm-lock.yaml +0 -2164
  550. package/web/ui/scripts/verify-base.mjs +0 -31
  551. package/web/ui/src/App.tsx +0 -88
  552. package/web/ui/src/components/ActivityFeed.tsx +0 -444
  553. package/web/ui/src/components/AgentGroupPicker.tsx +0 -263
  554. package/web/ui/src/components/AgentProviderCards.tsx +0 -220
  555. package/web/ui/src/components/CredentialForm.tsx +0 -214
  556. package/web/ui/src/components/ScopeGrants.tsx +0 -74
  557. package/web/ui/src/components/StatusDot.tsx +0 -43
  558. package/web/ui/src/components/VaultPicker.tsx +0 -127
  559. package/web/ui/src/components/setup/AdapterInstallStep.tsx +0 -178
  560. package/web/ui/src/components/setup/AgentGroupStep.tsx +0 -43
  561. package/web/ui/src/components/setup/ChannelPickStep.tsx +0 -74
  562. package/web/ui/src/components/setup/DoneStep.tsx +0 -49
  563. package/web/ui/src/components/setup/PrereqStep.tsx +0 -129
  564. package/web/ui/src/components/setup/TestConnectionStep.tsx +0 -108
  565. package/web/ui/src/components/setup/TestMessageStep.tsx +0 -104
  566. package/web/ui/src/components/setup/WireChannelStep.tsx +0 -166
  567. package/web/ui/src/components/setup/types.ts +0 -105
  568. package/web/ui/src/lib/api.test.ts +0 -410
  569. package/web/ui/src/lib/api.ts +0 -1210
  570. package/web/ui/src/lib/auth.test.ts +0 -139
  571. package/web/ui/src/lib/auth.ts +0 -348
  572. package/web/ui/src/lib/channel-adapters.ts +0 -136
  573. package/web/ui/src/main.tsx +0 -19
  574. package/web/ui/src/routes/ApprovalsList.tsx +0 -294
  575. package/web/ui/src/routes/Apps.tsx +0 -613
  576. package/web/ui/src/routes/ChannelWireDetail.test.tsx +0 -233
  577. package/web/ui/src/routes/ChannelWireDetail.tsx +0 -403
  578. package/web/ui/src/routes/ChannelsList.tsx +0 -158
  579. package/web/ui/src/routes/GroupDetail.tsx +0 -755
  580. package/web/ui/src/routes/GroupList.tsx +0 -187
  581. package/web/ui/src/routes/MessagingGroupDetail.test.tsx +0 -233
  582. package/web/ui/src/routes/MessagingGroupDetail.tsx +0 -306
  583. package/web/ui/src/routes/NewGroupWizard.tsx +0 -390
  584. package/web/ui/src/routes/OAuthCallback.tsx +0 -56
  585. package/web/ui/src/routes/SecretsList.tsx +0 -921
  586. package/web/ui/src/routes/SessionsList.tsx +0 -220
  587. package/web/ui/src/routes/SettingsAgentProvider.tsx +0 -109
  588. package/web/ui/src/routes/SettingsApprovals.tsx +0 -234
  589. package/web/ui/src/routes/SetupWizard.tsx +0 -219
  590. package/web/ui/src/routes/VaultDetail.test.tsx +0 -361
  591. package/web/ui/src/routes/VaultDetail.tsx +0 -960
  592. package/web/ui/src/routes/VaultsList.tsx +0 -295
  593. package/web/ui/src/routes/WireChannelPage.tsx +0 -413
  594. package/web/ui/src/styles.css +0 -608
  595. package/web/ui/src/test/setup.ts +0 -23
  596. package/web/ui/src/vite-env.d.ts +0 -10
  597. package/web/ui/vite.config.ts +0 -34
  598. package/web/ui/vitest.config.ts +0 -25
@@ -0,0 +1,274 @@
1
+ /**
2
+ * Per-channel Claude OAuth credential store (design §6).
3
+ *
4
+ * Covers: store/retrieve round-trip, 0600 on the secret file, redaction (the
5
+ * raw token never appears in the inspection helper / serialized output), and
6
+ * default-vs-override resolution (override wins, falls back to default, errors
7
+ * when neither). All hermetic under a throwaway state dir.
8
+ */
9
+ import { describe, test, expect, beforeEach, afterEach } from "bun:test";
10
+ import { mkdtempSync, rmSync, existsSync, readFileSync, statSync } from "fs";
11
+ import { join } from "path";
12
+ import { tmpdir } from "os";
13
+ import {
14
+ setDefaultClaudeCredential,
15
+ setChannelClaudeCredential,
16
+ removeChannelClaudeCredential,
17
+ resolveClaudeCredential,
18
+ describeClaudeCredentials,
19
+ readCredentialsFile,
20
+ credentialsFilePath,
21
+ CredentialNotConfiguredError,
22
+ setChannelEnvVar,
23
+ removeChannelEnvVar,
24
+ resolveChannelEnv,
25
+ describeChannelEnv,
26
+ DenylistedEnvError,
27
+ DENYLISTED_ENV,
28
+ } from "./credentials.ts";
29
+
30
+ const DEFAULT_TOKEN = "oat_DEFAULT-OPERATOR-TOKEN-SECRET";
31
+ const OVERRIDE_TOKEN = "oat_PER-CHANNEL-OVERRIDE-SECRET";
32
+
33
+ let dir: string;
34
+ beforeEach(() => {
35
+ dir = mkdtempSync(join(tmpdir(), "channel-creds-"));
36
+ });
37
+ afterEach(() => {
38
+ rmSync(dir, { recursive: true, force: true });
39
+ });
40
+
41
+ describe("store / retrieve round-trip", () => {
42
+ test("default token: set then resolve returns it", () => {
43
+ setDefaultClaudeCredential(DEFAULT_TOKEN, dir);
44
+ expect(resolveClaudeCredential("any-channel", dir)).toBe(DEFAULT_TOKEN);
45
+ });
46
+
47
+ test("per-channel override: set then resolve returns it for that channel", () => {
48
+ setChannelClaudeCredential("aaron-dev", OVERRIDE_TOKEN, dir);
49
+ expect(resolveClaudeCredential("aaron-dev", dir)).toBe(OVERRIDE_TOKEN);
50
+ });
51
+
52
+ test("setting one slice preserves the other (read-modify-write)", () => {
53
+ setDefaultClaudeCredential(DEFAULT_TOKEN, dir);
54
+ setChannelClaudeCredential("aaron-dev", OVERRIDE_TOKEN, dir);
55
+ setChannelClaudeCredential("ops", "oat_OPS", dir);
56
+ const file = readCredentialsFile(dir);
57
+ expect(file.claude!.default).toBe(DEFAULT_TOKEN);
58
+ expect(file.claude!.channels!["aaron-dev"]).toBe(OVERRIDE_TOKEN);
59
+ expect(file.claude!.channels!["ops"]).toBe("oat_OPS");
60
+ });
61
+
62
+ test("empty token is rejected (never persists a blank credential)", () => {
63
+ expect(() => setDefaultClaudeCredential("", dir)).toThrow(/non-empty token/);
64
+ expect(() => setChannelClaudeCredential("c", "", dir)).toThrow(/non-empty token/);
65
+ expect(existsSync(credentialsFilePath(dir))).toBe(false);
66
+ });
67
+ });
68
+
69
+ describe("0600 on the secret file", () => {
70
+ test("the credentials file is written 0600 (holds a secret)", () => {
71
+ setDefaultClaudeCredential(DEFAULT_TOKEN, dir);
72
+ const file = credentialsFilePath(dir);
73
+ expect(existsSync(file)).toBe(true);
74
+ expect(statSync(file).mode & 0o777).toBe(0o600);
75
+ });
76
+
77
+ test("a subsequent write keeps it 0600 (chmod is unconditional)", () => {
78
+ setDefaultClaudeCredential(DEFAULT_TOKEN, dir);
79
+ // Loosen perms behind the store's back, then write again → must re-tighten.
80
+ const fs = require("fs") as typeof import("fs");
81
+ fs.chmodSync(credentialsFilePath(dir), 0o644);
82
+ setChannelClaudeCredential("c", OVERRIDE_TOKEN, dir);
83
+ expect(statSync(credentialsFilePath(dir)).mode & 0o777).toBe(0o600);
84
+ });
85
+ });
86
+
87
+ describe("redaction — the raw token never leaks via the inspection helper", () => {
88
+ test("describeClaudeCredentials reports presence + channel names, NOT the token", () => {
89
+ setDefaultClaudeCredential(DEFAULT_TOKEN, dir);
90
+ setChannelClaudeCredential("aaron-dev", OVERRIDE_TOKEN, dir);
91
+ setChannelClaudeCredential("ops", "oat_OPS", dir);
92
+ const desc = describeClaudeCredentials(dir);
93
+ expect(desc.defaultSet).toBe(true);
94
+ expect(desc.channels).toEqual(["aaron-dev", "ops"]); // sorted, names only
95
+ const serialized = JSON.stringify(desc);
96
+ expect(serialized).not.toContain(DEFAULT_TOKEN);
97
+ expect(serialized).not.toContain(OVERRIDE_TOKEN);
98
+ expect(serialized).not.toContain("oat_OPS");
99
+ });
100
+
101
+ test("describe on an empty store: defaultSet false, no channels", () => {
102
+ const desc = describeClaudeCredentials(dir);
103
+ expect(desc).toEqual({ defaultSet: false, channels: [] });
104
+ });
105
+ });
106
+
107
+ describe("default-vs-override resolution", () => {
108
+ test("override WINS over the default for its channel", () => {
109
+ setDefaultClaudeCredential(DEFAULT_TOKEN, dir);
110
+ setChannelClaudeCredential("aaron-dev", OVERRIDE_TOKEN, dir);
111
+ expect(resolveClaudeCredential("aaron-dev", dir)).toBe(OVERRIDE_TOKEN);
112
+ // A different channel with no override falls back to the default.
113
+ expect(resolveClaudeCredential("other", dir)).toBe(DEFAULT_TOKEN);
114
+ });
115
+
116
+ test("falls back to the default when the channel has no override", () => {
117
+ setDefaultClaudeCredential(DEFAULT_TOKEN, dir);
118
+ expect(resolveClaudeCredential("never-configured", dir)).toBe(DEFAULT_TOKEN);
119
+ });
120
+
121
+ test("ERRORS when neither an override nor a default is set", () => {
122
+ expect(() => resolveClaudeCredential("ghost", dir)).toThrow(CredentialNotConfiguredError);
123
+ expect(() => resolveClaudeCredential("ghost", dir)).toThrow(/no Claude credential for channel "ghost"/);
124
+ });
125
+
126
+ test("removing an override falls back to the default; removing a missing one is a no-op", () => {
127
+ setDefaultClaudeCredential(DEFAULT_TOKEN, dir);
128
+ setChannelClaudeCredential("aaron-dev", OVERRIDE_TOKEN, dir);
129
+ expect(removeChannelClaudeCredential("aaron-dev", dir)).toBe(true);
130
+ expect(resolveClaudeCredential("aaron-dev", dir)).toBe(DEFAULT_TOKEN); // back to default
131
+ expect(removeChannelClaudeCredential("aaron-dev", dir)).toBe(false); // already gone
132
+ // The default is untouched by an override removal.
133
+ expect(readCredentialsFile(dir).claude!.default).toBe(DEFAULT_TOKEN);
134
+ });
135
+
136
+ test("resolution is read dynamically — a rotate takes effect on the next resolve", () => {
137
+ setDefaultClaudeCredential(DEFAULT_TOKEN, dir);
138
+ expect(resolveClaudeCredential("c", dir)).toBe(DEFAULT_TOKEN);
139
+ setDefaultClaudeCredential("oat_ROTATED", dir);
140
+ expect(resolveClaudeCredential("c", dir)).toBe("oat_ROTATED");
141
+ });
142
+ });
143
+
144
+ // ===========================================================================
145
+ // Generic per-channel env store (GH_TOKEN / CLOUDFLARE_API_TOKEN / …)
146
+ // ===========================================================================
147
+ const GH = "ghp_GITHUB-TOKEN-SECRET";
148
+ const CF = "cf_CLOUDFLARE-TOKEN-SECRET";
149
+
150
+ describe("env store — set / resolve / channel-over-default merge", () => {
151
+ test("default var: set with null channel, resolves for any channel", () => {
152
+ setChannelEnvVar(null, "GH_TOKEN", GH, dir);
153
+ expect(resolveChannelEnv("anything", dir)).toEqual({ GH_TOKEN: GH });
154
+ // An empty-string channel also targets the default layer.
155
+ setChannelEnvVar("", "CF_TOKEN", CF, dir);
156
+ expect(resolveChannelEnv("anything", dir)).toEqual({ GH_TOKEN: GH, CF_TOKEN: CF });
157
+ });
158
+
159
+ test("per-channel override WINS over the default for that channel; others see only the default", () => {
160
+ setChannelEnvVar(null, "GH_TOKEN", "ghp_DEFAULT", dir);
161
+ setChannelEnvVar("aaron-dev", "GH_TOKEN", "ghp_AARON", dir);
162
+ setChannelEnvVar("aaron-dev", "CLOUDFLARE_API_TOKEN", CF, dir);
163
+ // channel layer wins on GH_TOKEN, plus its own CF token, plus inherits nothing extra.
164
+ expect(resolveChannelEnv("aaron-dev", dir)).toEqual({ GH_TOKEN: "ghp_AARON", CLOUDFLARE_API_TOKEN: CF });
165
+ // a different channel falls back to the default only.
166
+ expect(resolveChannelEnv("other", dir)).toEqual({ GH_TOKEN: "ghp_DEFAULT" });
167
+ });
168
+
169
+ test("resolves to {} when nothing is configured (env injection is optional)", () => {
170
+ expect(resolveChannelEnv("ghost", dir)).toEqual({});
171
+ });
172
+
173
+ test("setting an env var preserves the Claude slice (independent namespaces)", () => {
174
+ setDefaultClaudeCredential(DEFAULT_TOKEN, dir);
175
+ setChannelEnvVar(null, "GH_TOKEN", GH, dir);
176
+ const file = readCredentialsFile(dir);
177
+ expect(file.claude!.default).toBe(DEFAULT_TOKEN); // untouched
178
+ expect(file.env!.default!.GH_TOKEN).toBe(GH);
179
+ });
180
+
181
+ test("read dynamically — a value change takes effect on the next resolve", () => {
182
+ setChannelEnvVar("c", "GH_TOKEN", "ghp_OLD", dir);
183
+ expect(resolveChannelEnv("c", dir).GH_TOKEN).toBe("ghp_OLD");
184
+ setChannelEnvVar("c", "GH_TOKEN", "ghp_NEW", dir);
185
+ expect(resolveChannelEnv("c", dir).GH_TOKEN).toBe("ghp_NEW");
186
+ });
187
+
188
+ test("the env store file is written 0600 (holds secrets)", () => {
189
+ setChannelEnvVar(null, "GH_TOKEN", GH, dir);
190
+ expect(statSync(credentialsFilePath(dir)).mode & 0o777).toBe(0o600);
191
+ });
192
+ });
193
+
194
+ describe("env store — remove", () => {
195
+ test("remove a default var; remove a missing one is a no-op (false)", () => {
196
+ setChannelEnvVar(null, "GH_TOKEN", GH, dir);
197
+ setChannelEnvVar(null, "CF_TOKEN", CF, dir);
198
+ expect(removeChannelEnvVar(null, "GH_TOKEN", dir)).toBe(true);
199
+ expect(resolveChannelEnv("any", dir)).toEqual({ CF_TOKEN: CF });
200
+ expect(removeChannelEnvVar(null, "GH_TOKEN", dir)).toBe(false); // already gone
201
+ });
202
+
203
+ test("remove a channel override; the default for that name re-emerges", () => {
204
+ setChannelEnvVar(null, "GH_TOKEN", "ghp_DEFAULT", dir);
205
+ setChannelEnvVar("aaron-dev", "GH_TOKEN", "ghp_AARON", dir);
206
+ expect(removeChannelEnvVar("aaron-dev", "GH_TOKEN", dir)).toBe(true);
207
+ expect(resolveChannelEnv("aaron-dev", dir)).toEqual({ GH_TOKEN: "ghp_DEFAULT" }); // back to default
208
+ });
209
+
210
+ test("removing the last var of a channel prunes the empty channel map", () => {
211
+ setChannelEnvVar("c", "GH_TOKEN", GH, dir);
212
+ removeChannelEnvVar("c", "GH_TOKEN", dir);
213
+ const file = readCredentialsFile(dir);
214
+ // The channel (and the now-empty channels map) is pruned, not left as {}.
215
+ expect(file.env?.channels).toBeUndefined();
216
+ });
217
+ });
218
+
219
+ describe("env store — redaction (describeChannelEnv returns NAMES only)", () => {
220
+ test("describe reports names per layer, never the values", () => {
221
+ setChannelEnvVar(null, "GH_TOKEN", GH, dir);
222
+ setChannelEnvVar("aaron-dev", "CLOUDFLARE_API_TOKEN", CF, dir);
223
+ setChannelEnvVar("aaron-dev", "GH_TOKEN", "ghp_AARON", dir);
224
+ const desc = describeChannelEnv(dir);
225
+ expect(desc.default).toEqual(["GH_TOKEN"]);
226
+ expect(desc.channels["aaron-dev"]).toEqual(["CLOUDFLARE_API_TOKEN", "GH_TOKEN"]); // sorted
227
+ const serialized = JSON.stringify(desc);
228
+ expect(serialized).not.toContain(GH);
229
+ expect(serialized).not.toContain(CF);
230
+ expect(serialized).not.toContain("ghp_AARON");
231
+ });
232
+
233
+ test("describe on an empty store: no default, no channels", () => {
234
+ expect(describeChannelEnv(dir)).toEqual({ default: [], channels: {} });
235
+ });
236
+ });
237
+
238
+ describe("env store — denylist (the Claude-auth trio is never settable)", () => {
239
+ test("the denylist is exactly the Claude-auth vars", () => {
240
+ expect([...DENYLISTED_ENV].sort()).toEqual(
241
+ ["ANTHROPIC_API_KEY", "CLAUDE_API_KEY", "CLAUDE_CODE_OAUTH_TOKEN"].sort(),
242
+ );
243
+ });
244
+
245
+ test("setter REJECTS each denylisted name (default + channel), nothing persisted", () => {
246
+ for (const name of DENYLISTED_ENV) {
247
+ expect(() => setChannelEnvVar(null, name, "x", dir)).toThrow(DenylistedEnvError);
248
+ expect(() => setChannelEnvVar("c", name, "x", dir)).toThrow(DenylistedEnvError);
249
+ }
250
+ expect(existsSync(credentialsFilePath(dir))).toBe(false);
251
+ });
252
+
253
+ test("setter rejects a malformed name + an empty value", () => {
254
+ expect(() => setChannelEnvVar(null, "9BAD", "x", dir)).toThrow(/invalid/);
255
+ expect(() => setChannelEnvVar(null, "has space", "x", dir)).toThrow(/invalid/);
256
+ expect(() => setChannelEnvVar(null, "GH_TOKEN", "", dir)).toThrow(/non-empty/);
257
+ expect(existsSync(credentialsFilePath(dir))).toBe(false);
258
+ });
259
+
260
+ test("resolve defensively STRIPS a denylisted key planted by a hand-edited file", () => {
261
+ // Plant a denylisted key directly on disk (bypassing the setter), then prove
262
+ // resolveChannelEnv never returns it — the injection defense's first line.
263
+ setChannelEnvVar(null, "GH_TOKEN", GH, dir);
264
+ const fs = require("fs") as typeof import("fs");
265
+ const file = JSON.parse(fs.readFileSync(credentialsFilePath(dir), "utf8")) as {
266
+ env: { default: Record<string, string> };
267
+ };
268
+ file.env.default.ANTHROPIC_API_KEY = "sk-ant-SMUGGLED";
269
+ fs.writeFileSync(credentialsFilePath(dir), JSON.stringify(file));
270
+ const resolved = resolveChannelEnv("any", dir);
271
+ expect(resolved.GH_TOKEN).toBe(GH);
272
+ expect(resolved.ANTHROPIC_API_KEY).toBeUndefined();
273
+ });
274
+ });
@@ -0,0 +1,380 @@
1
+ /**
2
+ * Per-channel Claude OAuth credential store (design §6).
3
+ *
4
+ * The Claude `CLAUDE_CODE_OAUTH_TOKEN` (from `claude setup-token`, the documented
5
+ * 1-year headless/CI auth path) is the credential a launched agent session runs
6
+ * on — injected into the sandbox at launch as the session's auth (NEVER
7
+ * `ANTHROPIC_API_KEY`, which would silently route onto API billing; see
8
+ * `spawn-agent.ts`). This module persists that secret, following the SAME
9
+ * file-store discipline `registry.ts` uses for per-channel transport tokens:
10
+ * a read-modify-write JSON file, written 0600 and `chmod`-ed 0600 unconditionally
11
+ * (so an existing file created under a looser umask is tightened on every write).
12
+ *
13
+ * Two principal levels (design §6 — "default one operator token; per-channel
14
+ * override"):
15
+ *
16
+ * - a **default / operator-level** token, used when a channel has no override,
17
+ * - a **per-channel override**, the multi-principal seam (multi-user isn't a
18
+ * rewrite — just populating per-channel, eventually per-principal, tokens).
19
+ *
20
+ * Resolution (`resolveClaudeCredential`): channel override ?? default ?? error.
21
+ *
22
+ * The secret lives in its OWN file (`credentials.json`), separate from
23
+ * `channels.json`: the default/operator token isn't tied to any single channel,
24
+ * and the credential lifecycle (set the operator token once, override per
25
+ * channel) is distinct from the channel-registry lifecycle. The file is
26
+ * NAMESPACED by credential type (`{ claude: { ... } }`) so a future credential
27
+ * type can coexist without a schema migration.
28
+ *
29
+ * Redaction discipline: the raw token is NEVER returned by the listing/inspection
30
+ * helper (`describeClaudeCredentials`) and NEVER logged — exactly the posture the
31
+ * config API + transports already keep for `config.token` / `webhookSecret`.
32
+ */
33
+
34
+ import { readFileSync, writeFileSync, existsSync, chmodSync, mkdirSync } from "fs";
35
+ import { join } from "path";
36
+ import { defaultStateDir } from "./registry.ts";
37
+
38
+ /** The Claude-credential slice of the store. */
39
+ export interface ClaudeCredentialStore {
40
+ /** Default / operator-level OAuth token, used when a channel has no override. */
41
+ default?: string;
42
+ /** Per-channel overrides, keyed by channel name. */
43
+ channels?: Record<string, string>;
44
+ }
45
+
46
+ /**
47
+ * The generic per-channel ENVIRONMENT-VARIABLE slice (`env`). Same two-principal
48
+ * shape as the Claude slice (an operator-level default layer + per-channel
49
+ * overrides), but each layer is a NAME→VALUE map rather than a single token: an
50
+ * operator scopes a channel's spawned agent a `GH_TOKEN`, `CLOUDFLARE_API_TOKEN`,
51
+ * etc. {@link resolveChannelEnv} flattens the two layers into one map (channel
52
+ * wins) at spawn time, and {@link buildAgentChildEnv} (spawn-agent.ts) merges that
53
+ * into the sandboxed child's env so the agent's `gh`/`git`/build tooling sees the
54
+ * tokens — while Claude's own auth (`CLAUDE_CODE_OAUTH_TOKEN`) stays untouched.
55
+ */
56
+ export interface ChannelEnvStore {
57
+ /** Operator-level default env vars, used by every channel (lowest precedence). */
58
+ default?: Record<string, string>;
59
+ /** Per-channel env overrides, keyed by channel name (wins over the default). */
60
+ channels?: Record<string, Record<string, string>>;
61
+ }
62
+
63
+ /** The on-disk `credentials.json` shape (namespaced by credential type). */
64
+ export interface CredentialsFile {
65
+ claude?: ClaudeCredentialStore;
66
+ /** Generic per-channel env-var injection (the GH_TOKEN/CLOUDFLARE_* slice). */
67
+ env?: ChannelEnvStore;
68
+ }
69
+
70
+ /**
71
+ * Env-var names that MUST NEVER be settable through the env store — they'd break
72
+ * the module's two load-bearing guarantees:
73
+ *
74
+ * - `ANTHROPIC_API_KEY` / `CLAUDE_API_KEY` would route the spawned session onto
75
+ * METERED API billing instead of the interactive subscription (the exact thing
76
+ * `buildAgentChildEnv` deliberately scrubs — see spawn-agent.ts §6).
77
+ * - `CLAUDE_CODE_OAUTH_TOKEN` is the session's MANAGED auth, resolved per-channel
78
+ * from the Claude slice; letting the generic env store override it would let an
79
+ * operator (or a future less-trusted caller) silently swap the session's
80
+ * identity out from under the credential resolver.
81
+ *
82
+ * The setters REJECT these (throw {@link DenylistedEnvError}); the injection step
83
+ * (`buildAgentChildEnv`) ALSO drops them defensively, so even a hand-edited
84
+ * credentials.json can't smuggle one through.
85
+ *
86
+ * `PATH`/`HOME` are deliberately NOT denylisted: `buildAgentChildEnv` layers the
87
+ * resolved channel env UNDER its own structural passthrough + the seeded-HOME
88
+ * overrides, so a channel-set PATH/HOME can't clobber the sandbox's own (the
89
+ * passthrough copies the real PATH/HOME after, and seedAgentHome's CLAUDE_CONFIG_DIR
90
+ * /XDG/TMP win last). Rejecting them would only deny a harmless no-op; allowing
91
+ * them keeps the denylist focused on the keys that actually matter (the Claude-auth
92
+ * trio). See the layering comment in `buildAgentChildEnv`.
93
+ */
94
+ export const DENYLISTED_ENV: ReadonlySet<string> = new Set([
95
+ "ANTHROPIC_API_KEY",
96
+ "CLAUDE_API_KEY",
97
+ "CLAUDE_CODE_OAUTH_TOKEN",
98
+ ]);
99
+
100
+ /** A basic POSIX-ish env-var name guard (letters/digits/underscore, no leading digit). */
101
+ const ENV_NAME_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
102
+
103
+ /** Thrown when a setter is asked to set/override a denylisted env-var name. */
104
+ export class DenylistedEnvError extends Error {
105
+ constructor(name: string) {
106
+ super(
107
+ `env var "${name}" is not settable here: it controls Claude auth / billing ` +
108
+ `(ANTHROPIC_API_KEY, CLAUDE_API_KEY, CLAUDE_CODE_OAUTH_TOKEN are reserved by ` +
109
+ `the managed subscription-billing path). Set the Claude credential via ` +
110
+ `POST /api/credentials/claude instead.`,
111
+ );
112
+ this.name = "DenylistedEnvError";
113
+ }
114
+ }
115
+
116
+ /** The default credential reference an unspecified spec resolves against. */
117
+ export const DEFAULT_CREDENTIAL_REF = "operator" as const;
118
+
119
+ /** Absolute path to the credentials.json store in a state dir. */
120
+ export function credentialsFilePath(stateDir?: string): string {
121
+ return join(stateDir ?? defaultStateDir(), "credentials.json");
122
+ }
123
+
124
+ /**
125
+ * Read `credentials.json` as a plain `CredentialsFile`. Returns an empty `{}` if
126
+ * the file is absent. Mirrors `registry.readChannelsFile` — the read half of the
127
+ * read-modify-write the setters use.
128
+ */
129
+ export function readCredentialsFile(stateDir?: string): CredentialsFile {
130
+ const file = credentialsFilePath(stateDir);
131
+ if (!existsSync(file)) return {};
132
+ const parsed = JSON.parse(readFileSync(file, "utf8")) as CredentialsFile;
133
+ if (!parsed || typeof parsed !== "object") {
134
+ throw new Error(`credentials: ${file} must be a JSON object`);
135
+ }
136
+ return parsed;
137
+ }
138
+
139
+ /**
140
+ * Persist the store back to `credentials.json` with 0600 perms — the file holds
141
+ * the Claude OAuth secret. Creates the state dir if needed. `chmod`s 0600
142
+ * unconditionally (writeFileSync's `mode` only applies on CREATE, so an existing
143
+ * file created under a looser umask is tightened on every write) — the exact
144
+ * discipline `registry.upsertChannelEntry` keeps for the secret-bearing
145
+ * channels.json.
146
+ */
147
+ function writeCredentialsFile(file: CredentialsFile, stateDir?: string): void {
148
+ const dir = stateDir ?? defaultStateDir();
149
+ mkdirSync(dir, { recursive: true });
150
+ const path = credentialsFilePath(dir);
151
+ writeFileSync(path, JSON.stringify(file, null, 2) + "\n", { mode: 0o600 });
152
+ chmodSync(path, 0o600);
153
+ }
154
+
155
+ /**
156
+ * Set the default / operator-level Claude OAuth token. Used by any channel that
157
+ * has no per-channel override. Read-modify-write so existing per-channel
158
+ * overrides are preserved.
159
+ */
160
+ export function setDefaultClaudeCredential(token: string, stateDir?: string): void {
161
+ if (typeof token !== "string" || token.length === 0) {
162
+ throw new Error("credentials: a non-empty token is required");
163
+ }
164
+ const file = readCredentialsFile(stateDir);
165
+ const claude = file.claude ?? {};
166
+ claude.default = token;
167
+ file.claude = claude;
168
+ writeCredentialsFile(file, stateDir);
169
+ }
170
+
171
+ /**
172
+ * Set a per-channel Claude OAuth override. Wins over the default for that channel.
173
+ * Read-modify-write so the default + other channels' overrides are preserved.
174
+ */
175
+ export function setChannelClaudeCredential(
176
+ channel: string,
177
+ token: string,
178
+ stateDir?: string,
179
+ ): void {
180
+ if (typeof channel !== "string" || channel.length === 0) {
181
+ throw new Error("credentials: a channel name is required");
182
+ }
183
+ if (typeof token !== "string" || token.length === 0) {
184
+ throw new Error("credentials: a non-empty token is required");
185
+ }
186
+ const file = readCredentialsFile(stateDir);
187
+ const claude = file.claude ?? {};
188
+ const channels = claude.channels ?? {};
189
+ channels[channel] = token;
190
+ claude.channels = channels;
191
+ file.claude = claude;
192
+ writeCredentialsFile(file, stateDir);
193
+ }
194
+
195
+ /**
196
+ * Remove a per-channel override (the channel falls back to the default after
197
+ * this). Returns true if an override existed, false if there was nothing to
198
+ * remove. The default token is untouched.
199
+ */
200
+ export function removeChannelClaudeCredential(channel: string, stateDir?: string): boolean {
201
+ const file = readCredentialsFile(stateDir);
202
+ const channels = file.claude?.channels;
203
+ if (!channels || !(channel in channels)) return false;
204
+ delete channels[channel];
205
+ writeCredentialsFile(file, stateDir);
206
+ return true;
207
+ }
208
+
209
+ /** Thrown when neither a per-channel override nor a default token is configured. */
210
+ export class CredentialNotConfiguredError extends Error {
211
+ constructor(channel: string) {
212
+ super(
213
+ `no Claude credential for channel "${channel}": set a per-channel override or the ` +
214
+ `default/operator token (POST /api/credentials/claude). Get one with ` +
215
+ `\`claude setup-token\`.`,
216
+ );
217
+ this.name = "CredentialNotConfiguredError";
218
+ }
219
+ }
220
+
221
+ /**
222
+ * Resolve the Claude OAuth token a session on `channel` should run on:
223
+ *
224
+ * channel override ?? default ?? throw CredentialNotConfiguredError
225
+ *
226
+ * Read at resolve time (not cached) so a token set/rotated via the config API
227
+ * takes effect on the next spawn without a daemon restart — the dynamic-read
228
+ * discipline. Throwing (rather than returning empty) means a misconfigured
229
+ * install fails loud BEFORE a session launches with no auth.
230
+ */
231
+ export function resolveClaudeCredential(channel: string, stateDir?: string): string {
232
+ const claude = readCredentialsFile(stateDir).claude;
233
+ const override = claude?.channels?.[channel];
234
+ if (override) return override;
235
+ const fallback = claude?.default;
236
+ if (fallback) return fallback;
237
+ throw new CredentialNotConfiguredError(channel);
238
+ }
239
+
240
+ /**
241
+ * Describe the credential store for an operator-facing read WITHOUT leaking the
242
+ * secret: whether a default is set, and which channels carry an override (names
243
+ * only). The raw token is never returned — same redaction posture the config
244
+ * API keeps for transport tokens. (`GET /api/credentials/claude`.)
245
+ */
246
+ export function describeClaudeCredentials(
247
+ stateDir?: string,
248
+ ): { defaultSet: boolean; channels: string[] } {
249
+ const claude = readCredentialsFile(stateDir).claude;
250
+ return {
251
+ defaultSet: Boolean(claude?.default),
252
+ channels: Object.keys(claude?.channels ?? {}).sort(),
253
+ };
254
+ }
255
+
256
+ // ---------------------------------------------------------------------------
257
+ // Generic per-channel env-var store (the GH_TOKEN / CLOUDFLARE_API_TOKEN slice).
258
+ //
259
+ // Mirrors the Claude helpers exactly: read-modify-write JSON, 0600 + unconditional
260
+ // chmod, sibling-preserving, dynamic-read-at-resolve. A `null`/`undefined` channel
261
+ // targets the operator-level DEFAULT layer; a channel name targets that channel's
262
+ // override layer. Every setter enforces DENYLISTED_ENV (the Claude-auth trio) so
263
+ // the subscription-billing guarantee can't be subverted via this surface.
264
+ // ---------------------------------------------------------------------------
265
+
266
+ /** Validate an env-var NAME for the setters: non-denylisted + a sane shape. */
267
+ function assertSettableEnvName(name: string): void {
268
+ if (typeof name !== "string" || name.length === 0) {
269
+ throw new Error("credentials: an env var name is required");
270
+ }
271
+ if (DENYLISTED_ENV.has(name)) throw new DenylistedEnvError(name);
272
+ if (!ENV_NAME_RE.test(name)) {
273
+ throw new Error(
274
+ `credentials: env var name "${name}" is invalid (letters, digits, underscore; no leading digit)`,
275
+ );
276
+ }
277
+ }
278
+
279
+ /**
280
+ * Set ONE env var on the operator-level default layer (`channel` is null/undefined)
281
+ * or on a specific channel's override layer. Read-modify-write so the Claude slice,
282
+ * the other layer, and other vars are preserved. Rejects denylisted names
283
+ * ({@link DenylistedEnvError}) and an empty value.
284
+ */
285
+ export function setChannelEnvVar(
286
+ channel: string | null | undefined,
287
+ name: string,
288
+ value: string,
289
+ stateDir?: string,
290
+ ): void {
291
+ assertSettableEnvName(name);
292
+ if (typeof value !== "string" || value.length === 0) {
293
+ throw new Error("credentials: a non-empty env var value is required");
294
+ }
295
+ const file = readCredentialsFile(stateDir);
296
+ const env = file.env ?? {};
297
+ if (channel === null || channel === undefined || channel === "") {
298
+ const def = env.default ?? {};
299
+ def[name] = value;
300
+ env.default = def;
301
+ } else {
302
+ const channels = env.channels ?? {};
303
+ const forChannel = channels[channel] ?? {};
304
+ forChannel[name] = value;
305
+ channels[channel] = forChannel;
306
+ env.channels = channels;
307
+ }
308
+ file.env = env;
309
+ writeCredentialsFile(file, stateDir);
310
+ }
311
+
312
+ /**
313
+ * Remove ONE env var from the operator-level default layer (`channel` null/undefined)
314
+ * or a channel's override layer. Returns true if it existed, false if there was
315
+ * nothing to remove. Prunes an emptied channel map so a removed-everything channel
316
+ * doesn't linger as `{}`. Read-modify-write; the Claude slice + other vars untouched.
317
+ */
318
+ export function removeChannelEnvVar(
319
+ channel: string | null | undefined,
320
+ name: string,
321
+ stateDir?: string,
322
+ ): boolean {
323
+ const file = readCredentialsFile(stateDir);
324
+ const env = file.env;
325
+ if (!env) return false;
326
+ if (channel === null || channel === undefined || channel === "") {
327
+ if (!env.default || !(name in env.default)) return false;
328
+ delete env.default[name];
329
+ if (Object.keys(env.default).length === 0) delete env.default;
330
+ } else {
331
+ const forChannel = env.channels?.[channel];
332
+ if (!forChannel || !(name in forChannel)) return false;
333
+ delete forChannel[name];
334
+ if (Object.keys(forChannel).length === 0) delete env.channels![channel];
335
+ if (env.channels && Object.keys(env.channels).length === 0) delete env.channels;
336
+ }
337
+ writeCredentialsFile(file, stateDir);
338
+ return true;
339
+ }
340
+
341
+ /**
342
+ * Resolve the FLATTENED env a session on `channel` should run with:
343
+ *
344
+ * { ...env.default, ...env.channels[channel] } (the channel layer wins)
345
+ *
346
+ * Read at resolve time (not cached), like the Claude resolver — so a var set via the
347
+ * config API takes effect on the next spawn (or per-session restart) without a daemon
348
+ * restart. Defensively SKIPS any denylisted key that somehow landed on disk (a
349
+ * hand-edited file): the setter blocks them, but the resolver never returns one
350
+ * either, so `buildAgentChildEnv`'s own denylist drop is a belt to this suspenders.
351
+ * Returns an empty map when nothing is configured (a channel with no env is fine).
352
+ */
353
+ export function resolveChannelEnv(channel: string, stateDir?: string): Record<string, string> {
354
+ const env = readCredentialsFile(stateDir).env;
355
+ const merged: Record<string, string> = { ...(env?.default ?? {}), ...(env?.channels?.[channel] ?? {}) };
356
+ for (const k of Object.keys(merged)) {
357
+ if (DENYLISTED_ENV.has(k)) delete merged[k];
358
+ }
359
+ return merged;
360
+ }
361
+
362
+ /**
363
+ * Describe the env store for an operator-facing read WITHOUT leaking values: the
364
+ * NAMES set on the default layer, and the names set per channel. The raw values are
365
+ * NEVER returned (`GET /api/credentials/env`) — same redaction posture as
366
+ * `describeClaudeCredentials`.
367
+ */
368
+ export function describeChannelEnv(
369
+ stateDir?: string,
370
+ ): { default: string[]; channels: Record<string, string[]> } {
371
+ const env = readCredentialsFile(stateDir).env;
372
+ const channels: Record<string, string[]> = {};
373
+ for (const [ch, vars] of Object.entries(env?.channels ?? {})) {
374
+ channels[ch] = Object.keys(vars).sort();
375
+ }
376
+ return {
377
+ default: Object.keys(env?.default ?? {}).sort(),
378
+ channels,
379
+ };
380
+ }