@openparachute/agent 0.1.2 → 0.2.2

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