@openparachute/agent 0.1.2 → 0.2.0

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