@openparachute/agent 0.1.1 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (598) hide show
  1. package/.parachute/module.json +124 -8
  2. package/LICENSE +2 -16
  3. package/README.md +118 -166
  4. package/package.json +32 -43
  5. package/scripts/spawn-agent.ts +371 -0
  6. package/src/_parked/interactive-spawn.test.ts +324 -0
  7. package/src/_parked/interactive-spawn.ts +701 -0
  8. package/src/agent-defs.test.ts +1504 -0
  9. package/src/agent-defs.ts +1702 -0
  10. package/src/agent-mcp-config.test.ts +115 -0
  11. package/src/agent-mcp-config.ts +115 -0
  12. package/src/agents.test.ts +360 -0
  13. package/src/agents.ts +379 -0
  14. package/src/auth.test.ts +46 -0
  15. package/src/auth.ts +140 -0
  16. package/src/backends/attached-queue.test.ts +376 -0
  17. package/src/backends/attached-queue.ts +372 -0
  18. package/src/backends/programmatic.test.ts +1715 -0
  19. package/src/backends/programmatic.ts +927 -0
  20. package/src/backends/registry.test.ts +1494 -0
  21. package/src/backends/registry.ts +1202 -0
  22. package/src/backends/stream-json.test.ts +570 -0
  23. package/src/backends/stream-json.ts +392 -0
  24. package/src/backends/types.ts +223 -0
  25. package/src/bridge.ts +417 -0
  26. package/src/channel-backend-wiring.test.ts +237 -0
  27. package/src/credentials.test.ts +274 -0
  28. package/src/credentials.ts +380 -0
  29. package/src/cron.test.ts +342 -0
  30. package/src/cron.ts +380 -0
  31. package/src/daemon-agent-def-api.test.ts +166 -0
  32. package/src/daemon-agent-defs-api.test.ts +953 -0
  33. package/src/daemon-agent-env-api.test.ts +338 -0
  34. package/src/daemon-attached-queue-store.test.ts +65 -0
  35. package/src/daemon-config-api.test.ts +962 -0
  36. package/src/daemon-jobs-api.test.ts +271 -0
  37. package/src/daemon-vault-chat.test.ts +250 -0
  38. package/src/daemon.test.ts +746 -0
  39. package/src/daemon.ts +3314 -0
  40. package/src/def-vaults.test.ts +136 -0
  41. package/src/def-vaults.ts +165 -0
  42. package/src/delivery-state.test.ts +110 -0
  43. package/src/delivery-state.ts +154 -0
  44. package/src/effective-env.test.ts +114 -0
  45. package/src/effective-env.ts +184 -0
  46. package/src/env-compat.ts +39 -0
  47. package/src/grants.test.ts +638 -0
  48. package/src/grants.ts +675 -0
  49. package/src/hub-jwt.test.ts +161 -0
  50. package/src/hub-jwt.ts +182 -0
  51. package/src/jobs.test.ts +245 -0
  52. package/src/jobs.ts +266 -0
  53. package/src/mcp-http.test.ts +265 -0
  54. package/src/mcp-http.ts +771 -0
  55. package/src/mint-token.test.ts +152 -0
  56. package/src/mint-token.ts +139 -0
  57. package/src/module-manifest.test.ts +158 -0
  58. package/src/oauth-discovery.ts +134 -0
  59. package/src/programmatic-wiring.test.ts +838 -0
  60. package/src/registry.test.ts +227 -0
  61. package/src/registry.ts +228 -0
  62. package/src/resolve-port.test.ts +64 -0
  63. package/src/routing.test.ts +184 -0
  64. package/src/routing.ts +76 -0
  65. package/src/runner.test.ts +506 -0
  66. package/src/runner.ts +255 -0
  67. package/src/sandbox/config.test.ts +150 -0
  68. package/src/sandbox/config.ts +102 -0
  69. package/src/sandbox/egress.test.ts +113 -0
  70. package/src/sandbox/egress.ts +123 -0
  71. package/src/sandbox/index.ts +180 -0
  72. package/src/sandbox/live-seatbelt.test.ts +277 -0
  73. package/src/sandbox/mounts.test.ts +154 -0
  74. package/src/sandbox/mounts.ts +133 -0
  75. package/src/sandbox/sandbox.test.ts +168 -0
  76. package/src/sandbox/types.ts +382 -0
  77. package/src/services-manifest.test.ts +106 -0
  78. package/src/services-manifest.ts +95 -0
  79. package/src/spa-serve.test.ts +116 -0
  80. package/src/spa-serve.ts +116 -0
  81. package/src/spawn-agent-cli.test.ts +172 -0
  82. package/src/spawn-agent.test.ts +1218 -0
  83. package/src/spawn-agent.ts +569 -0
  84. package/src/spawn-deps.test.ts +54 -0
  85. package/src/spawn-deps.ts +166 -0
  86. package/src/telegram/api.ts +153 -0
  87. package/src/terminal-assets.test.ts +50 -0
  88. package/src/terminal-assets.ts +79 -0
  89. package/src/terminal-ui.ts +305 -0
  90. package/src/terminal.test.ts +530 -0
  91. package/src/terminal.ts +458 -0
  92. package/src/transport.ts +270 -0
  93. package/src/transports/http-ui.test.ts +455 -0
  94. package/src/transports/http-ui.ts +201 -0
  95. package/src/transports/telegram.test.ts +174 -0
  96. package/src/transports/telegram.ts +426 -0
  97. package/src/transports/vault.test.ts +2011 -0
  98. package/src/transports/vault.ts +1790 -0
  99. package/src/ui-kit.test.ts +178 -0
  100. package/src/ui-kit.ts +402 -0
  101. package/tsconfig.json +8 -14
  102. package/web/ui/tsconfig.json +2 -1
  103. package/.claude/scheduled_tasks.lock +0 -1
  104. package/.claude/settings.json +0 -5
  105. package/.claude/skills/add-atomic-chat-tool/SKILL.md +0 -243
  106. package/.claude/skills/add-atomic-chat-tool/atomic-chat-mcp-stdio.ts +0 -229
  107. package/.claude/skills/add-codex/SKILL.md +0 -161
  108. package/.claude/skills/add-dashboard/SKILL.md +0 -138
  109. package/.claude/skills/add-dashboard/resources/dashboard-pusher.ts +0 -495
  110. package/.claude/skills/add-emacs/SKILL.md +0 -296
  111. package/.claude/skills/add-gcal-tool/SKILL.md +0 -210
  112. package/.claude/skills/add-gchat/REMOVE.md +0 -6
  113. package/.claude/skills/add-gchat/SKILL.md +0 -92
  114. package/.claude/skills/add-gchat/VERIFY.md +0 -3
  115. package/.claude/skills/add-github/REMOVE.md +0 -6
  116. package/.claude/skills/add-github/SKILL.md +0 -148
  117. package/.claude/skills/add-github/VERIFY.md +0 -3
  118. package/.claude/skills/add-gmail-tool/SKILL.md +0 -229
  119. package/.claude/skills/add-imessage/REMOVE.md +0 -6
  120. package/.claude/skills/add-imessage/SKILL.md +0 -113
  121. package/.claude/skills/add-imessage/VERIFY.md +0 -3
  122. package/.claude/skills/add-karpathy-llm-wiki/SKILL.md +0 -110
  123. package/.claude/skills/add-karpathy-llm-wiki/llm-wiki.md +0 -75
  124. package/.claude/skills/add-linear/REMOVE.md +0 -6
  125. package/.claude/skills/add-linear/SKILL.md +0 -168
  126. package/.claude/skills/add-linear/VERIFY.md +0 -3
  127. package/.claude/skills/add-macos-statusbar/SKILL.md +0 -133
  128. package/.claude/skills/add-macos-statusbar/add/src/statusbar.swift +0 -147
  129. package/.claude/skills/add-matrix/REMOVE.md +0 -6
  130. package/.claude/skills/add-matrix/SKILL.md +0 -148
  131. package/.claude/skills/add-matrix/VERIFY.md +0 -3
  132. package/.claude/skills/add-ollama-provider/SKILL.md +0 -179
  133. package/.claude/skills/add-ollama-tool/SKILL.md +0 -193
  134. package/.claude/skills/add-opencode/SKILL.md +0 -229
  135. package/.claude/skills/add-parallel/SKILL.md +0 -290
  136. package/.claude/skills/add-resend/REMOVE.md +0 -6
  137. package/.claude/skills/add-resend/SKILL.md +0 -93
  138. package/.claude/skills/add-resend/VERIFY.md +0 -3
  139. package/.claude/skills/add-signal/REMOVE.md +0 -13
  140. package/.claude/skills/add-signal/SKILL.md +0 -318
  141. package/.claude/skills/add-signal/VERIFY.md +0 -5
  142. package/.claude/skills/add-slack/REMOVE.md +0 -6
  143. package/.claude/skills/add-slack/SKILL.md +0 -112
  144. package/.claude/skills/add-slack/VERIFY.md +0 -3
  145. package/.claude/skills/add-teams/REMOVE.md +0 -6
  146. package/.claude/skills/add-teams/SKILL.md +0 -207
  147. package/.claude/skills/add-teams/VERIFY.md +0 -3
  148. package/.claude/skills/add-vercel/SKILL.md +0 -147
  149. package/.claude/skills/add-vercel/container-skills/vercel-cli/SKILL.md +0 -103
  150. package/.claude/skills/add-webex/REMOVE.md +0 -6
  151. package/.claude/skills/add-webex/SKILL.md +0 -88
  152. package/.claude/skills/add-webex/VERIFY.md +0 -3
  153. package/.claude/skills/add-wechat/REMOVE.md +0 -49
  154. package/.claude/skills/add-wechat/SKILL.md +0 -170
  155. package/.claude/skills/add-wechat/scripts/wire-dm.ts +0 -172
  156. package/.claude/skills/add-whatsapp/SKILL.md +0 -264
  157. package/.claude/skills/add-whatsapp-cloud/REMOVE.md +0 -6
  158. package/.claude/skills/add-whatsapp-cloud/SKILL.md +0 -95
  159. package/.claude/skills/add-whatsapp-cloud/VERIFY.md +0 -3
  160. package/.claude/skills/claw/SKILL.md +0 -131
  161. package/.claude/skills/claw/scripts/claw +0 -374
  162. package/.claude/skills/convert-to-apple-container/SKILL.md +0 -212
  163. package/.claude/skills/customize/SKILL.md +0 -110
  164. package/.claude/skills/debug/SKILL.md +0 -349
  165. package/.claude/skills/get-qodo-rules/SKILL.md +0 -122
  166. package/.claude/skills/get-qodo-rules/references/output-format.md +0 -41
  167. package/.claude/skills/get-qodo-rules/references/pagination.md +0 -33
  168. package/.claude/skills/get-qodo-rules/references/repository-scope.md +0 -26
  169. package/.claude/skills/init-first-agent/SKILL.md +0 -120
  170. package/.claude/skills/init-onecli/SKILL.md +0 -270
  171. package/.claude/skills/manage-channels/SKILL.md +0 -87
  172. package/.claude/skills/manage-mounts/SKILL.md +0 -47
  173. package/.claude/skills/migrate-from-openclaw/MIGRATE_CRONS.md +0 -100
  174. package/.claude/skills/migrate-from-openclaw/SKILL.md +0 -447
  175. package/.claude/skills/migrate-from-openclaw/scripts/discover-openclaw.ts +0 -734
  176. package/.claude/skills/migrate-from-openclaw/scripts/extract-channel-credentials.ts +0 -476
  177. package/.claude/skills/migrate-nanoclaw/SKILL.md +0 -484
  178. package/.claude/skills/migrate-nanoclaw/diagnostics.md +0 -51
  179. package/.claude/skills/qodo-pr-resolver/SKILL.md +0 -326
  180. package/.claude/skills/qodo-pr-resolver/resources/providers.md +0 -329
  181. package/.claude/skills/update-nanoclaw/SKILL.md +0 -243
  182. package/.claude/skills/update-nanoclaw/diagnostics.md +0 -48
  183. package/.claude/skills/update-skills/SKILL.md +0 -130
  184. package/.claude/skills/use-native-credential-proxy/SKILL.md +0 -167
  185. package/.claude/skills/x-integration/SKILL.md +0 -417
  186. package/.claude/skills/x-integration/agent.ts +0 -243
  187. package/.claude/skills/x-integration/host.ts +0 -155
  188. package/.claude/skills/x-integration/lib/browser.ts +0 -148
  189. package/.claude/skills/x-integration/lib/config.ts +0 -62
  190. package/.claude/skills/x-integration/scripts/like.ts +0 -56
  191. package/.claude/skills/x-integration/scripts/post.ts +0 -66
  192. package/.claude/skills/x-integration/scripts/quote.ts +0 -80
  193. package/.claude/skills/x-integration/scripts/reply.ts +0 -74
  194. package/.claude/skills/x-integration/scripts/retweet.ts +0 -62
  195. package/.claude/skills/x-integration/scripts/setup.ts +0 -87
  196. package/.github/CODEOWNERS +0 -10
  197. package/.github/PULL_REQUEST_TEMPLATE.md +0 -18
  198. package/.github/workflows/bump-version.yml +0 -35
  199. package/.github/workflows/ci.yml +0 -39
  200. package/.github/workflows/label-pr.yml +0 -40
  201. package/.github/workflows/update-tokens.yml +0 -43
  202. package/.husky/pre-commit +0 -1
  203. package/.mcp.json +0 -3
  204. package/.nvmrc +0 -1
  205. package/.prettierrc +0 -4
  206. package/CHANGELOG.md +0 -221
  207. package/CLAUDE.md +0 -307
  208. package/CODE_OF_CONDUCT.md +0 -128
  209. package/CONTRIBUTING.md +0 -159
  210. package/CONTRIBUTORS.md +0 -26
  211. package/LICENSE-NANOCLAW-MIT +0 -21
  212. package/README_ja.md +0 -194
  213. package/README_zh.md +0 -194
  214. package/assets/nanoclaw-favicon.png +0 -0
  215. package/assets/nanoclaw-icon.png +0 -0
  216. package/assets/nanoclaw-logo-dark.png +0 -0
  217. package/assets/nanoclaw-logo.png +0 -0
  218. package/assets/nanoclaw-profile.jpeg +0 -0
  219. package/assets/nanoclaw-sales.png +0 -0
  220. package/assets/social-preview.jpg +0 -0
  221. package/config-examples/mount-allowlist.json +0 -25
  222. package/container/.dockerignore +0 -2
  223. package/container/CLAUDE.md +0 -21
  224. package/container/Dockerfile +0 -121
  225. package/container/agent-runner/bun.lock +0 -243
  226. package/container/agent-runner/package.json +0 -22
  227. package/container/agent-runner/scripts/sdk-signal-probe.ts +0 -169
  228. package/container/agent-runner/src/config.ts +0 -55
  229. package/container/agent-runner/src/db/connection.ts +0 -267
  230. package/container/agent-runner/src/db/index.ts +0 -20
  231. package/container/agent-runner/src/db/messages-in.ts +0 -138
  232. package/container/agent-runner/src/db/messages-out.ts +0 -143
  233. package/container/agent-runner/src/db/session-routing.ts +0 -30
  234. package/container/agent-runner/src/db/session-state.test.ts +0 -100
  235. package/container/agent-runner/src/db/session-state.ts +0 -79
  236. package/container/agent-runner/src/destinations.ts +0 -135
  237. package/container/agent-runner/src/formatter.test.ts +0 -167
  238. package/container/agent-runner/src/formatter.ts +0 -260
  239. package/container/agent-runner/src/index.ts +0 -110
  240. package/container/agent-runner/src/integration.test.ts +0 -121
  241. package/container/agent-runner/src/mcp-tools/agents.instructions.md +0 -26
  242. package/container/agent-runner/src/mcp-tools/agents.ts +0 -66
  243. package/container/agent-runner/src/mcp-tools/core.instructions.md +0 -27
  244. package/container/agent-runner/src/mcp-tools/core.ts +0 -262
  245. package/container/agent-runner/src/mcp-tools/index.ts +0 -22
  246. package/container/agent-runner/src/mcp-tools/interactive.instructions.md +0 -22
  247. package/container/agent-runner/src/mcp-tools/interactive.ts +0 -169
  248. package/container/agent-runner/src/mcp-tools/scheduling.instructions.md +0 -40
  249. package/container/agent-runner/src/mcp-tools/scheduling.ts +0 -299
  250. package/container/agent-runner/src/mcp-tools/self-mod.instructions.md +0 -25
  251. package/container/agent-runner/src/mcp-tools/self-mod.ts +0 -120
  252. package/container/agent-runner/src/mcp-tools/server.ts +0 -54
  253. package/container/agent-runner/src/mcp-tools/types.ts +0 -6
  254. package/container/agent-runner/src/poll-loop.test.ts +0 -248
  255. package/container/agent-runner/src/poll-loop.ts +0 -437
  256. package/container/agent-runner/src/providers/claude.ts +0 -379
  257. package/container/agent-runner/src/providers/factory.test.ts +0 -19
  258. package/container/agent-runner/src/providers/factory.ts +0 -13
  259. package/container/agent-runner/src/providers/index.ts +0 -6
  260. package/container/agent-runner/src/providers/mock.ts +0 -77
  261. package/container/agent-runner/src/providers/provider-registry.ts +0 -33
  262. package/container/agent-runner/src/providers/types.ts +0 -82
  263. package/container/agent-runner/src/scheduling/task-script.ts +0 -121
  264. package/container/agent-runner/src/timezone.test.ts +0 -93
  265. package/container/agent-runner/src/timezone.ts +0 -107
  266. package/container/agent-runner/tsconfig.json +0 -14
  267. package/container/build.sh +0 -48
  268. package/container/entrypoint.sh +0 -16
  269. package/container/skills/agent-browser/SKILL.md +0 -159
  270. package/container/skills/frontend-engineer/SKILL.md +0 -157
  271. package/container/skills/self-customize/SKILL.md +0 -87
  272. package/container/skills/slack-formatting/SKILL.md +0 -94
  273. package/container/skills/vercel-cli/SKILL.md +0 -111
  274. package/container/skills/welcome/SKILL.md +0 -85
  275. package/docs/APPLE-CONTAINER-NETWORKING.md +0 -90
  276. package/docs/BRANCH-FORK-MAINTENANCE.md +0 -81
  277. package/docs/README.md +0 -25
  278. package/docs/SDK_DEEP_DIVE.md +0 -643
  279. package/docs/SECURITY.md +0 -162
  280. package/docs/agent-runner-details.md +0 -749
  281. package/docs/api-details.md +0 -365
  282. package/docs/architecture-diagram.html +0 -422
  283. package/docs/architecture-diagram.md +0 -215
  284. package/docs/architecture.md +0 -751
  285. package/docs/audit/2026-04-30-channel-endpoint-audit.md +0 -36
  286. package/docs/build-and-runtime.md +0 -80
  287. package/docs/cross-mount-stress/README.md +0 -112
  288. package/docs/cross-mount-stress/container-writer-retry.mjs +0 -55
  289. package/docs/cross-mount-stress/container-writer-slow.mjs +0 -42
  290. package/docs/cross-mount-stress/container-writer.mjs +0 -47
  291. package/docs/cross-mount-stress/host-writer-retry.mjs +0 -55
  292. package/docs/cross-mount-stress/host-writer-slow.mjs +0 -43
  293. package/docs/cross-mount-stress/host-writer.mjs +0 -47
  294. package/docs/db-central.md +0 -316
  295. package/docs/db-session.md +0 -183
  296. package/docs/db.md +0 -119
  297. package/docs/design/2026-04-29-vault-management-ui.md +0 -231
  298. package/docs/design/2026-04-30-channel-wiring-rework.md +0 -234
  299. package/docs/design/2026-05-01-channel-wiring-approvals-deep-dive.md +0 -272
  300. package/docs/design/2026-05-02-channel-policy-and-approval-routing.md +0 -250
  301. package/docs/docker-sandboxes.md +0 -359
  302. package/docs/isolation-model.md +0 -88
  303. package/docs/ollama.md +0 -79
  304. package/docs/parachute-integration.md +0 -109
  305. package/docs/post-night-rebirth-reflections.md +0 -151
  306. package/eslint.config.js +0 -32
  307. package/pnpm-workspace.yaml +0 -8
  308. package/repo-tokens/README.md +0 -113
  309. package/repo-tokens/action.yml +0 -186
  310. package/repo-tokens/badge.svg +0 -23
  311. package/repo-tokens/examples/green.svg +0 -14
  312. package/repo-tokens/examples/red.svg +0 -14
  313. package/repo-tokens/examples/yellow-green.svg +0 -14
  314. package/repo-tokens/examples/yellow.svg +0 -14
  315. package/scripts/chat.ts +0 -101
  316. package/scripts/cleanup-sessions.sh +0 -150
  317. package/scripts/init-cli-agent.ts +0 -171
  318. package/scripts/init-first-agent.ts +0 -377
  319. package/scripts/parachute.ts +0 -158
  320. package/scripts/run-migrations.ts +0 -105
  321. package/scripts/sanity-live-poll.ts +0 -95
  322. package/scripts/seed-discord.ts +0 -79
  323. package/scripts/test-v2-agent.ts +0 -106
  324. package/scripts/test-v2-channel-e2e.ts +0 -265
  325. package/scripts/test-v2-host.ts +0 -184
  326. package/src/channels/adapter.ts +0 -214
  327. package/src/channels/ask-question.ts +0 -46
  328. package/src/channels/channel-registry.test.ts +0 -421
  329. package/src/channels/channel-registry.ts +0 -313
  330. package/src/channels/chat-sdk-bridge.test.ts +0 -84
  331. package/src/channels/chat-sdk-bridge.ts +0 -652
  332. package/src/channels/cli.ts +0 -276
  333. package/src/channels/discord.ts +0 -90
  334. package/src/channels/index.ts +0 -17
  335. package/src/channels/telegram-markdown-sanitize.test.ts +0 -78
  336. package/src/channels/telegram-markdown-sanitize.ts +0 -55
  337. package/src/channels/telegram-pairing.test.ts +0 -254
  338. package/src/channels/telegram-pairing.ts +0 -339
  339. package/src/channels/telegram.ts +0 -279
  340. package/src/channels/trust-hint.test.ts +0 -48
  341. package/src/channels/trust-hint.ts +0 -75
  342. package/src/claude-md-compose.migrate.test.ts +0 -64
  343. package/src/claude-md-compose.ts +0 -205
  344. package/src/command-gate.ts +0 -63
  345. package/src/config.test.ts +0 -93
  346. package/src/config.ts +0 -108
  347. package/src/container-config.ts +0 -167
  348. package/src/container-runner.test.ts +0 -32
  349. package/src/container-runner.ts +0 -576
  350. package/src/container-runtime.test.ts +0 -169
  351. package/src/container-runtime.ts +0 -92
  352. package/src/db/_bun-sqlite-shim.ts +0 -88
  353. package/src/db/agent-activity.test.ts +0 -155
  354. package/src/db/agent-activity.ts +0 -121
  355. package/src/db/agent-groups.ts +0 -77
  356. package/src/db/connection.migrate.test.ts +0 -143
  357. package/src/db/connection.ts +0 -224
  358. package/src/db/db-v2.test.ts +0 -440
  359. package/src/db/dropped-messages.ts +0 -44
  360. package/src/db/index.ts +0 -40
  361. package/src/db/messaging-groups.ts +0 -252
  362. package/src/db/migrations/001-initial.ts +0 -112
  363. package/src/db/migrations/002-chat-sdk-state.ts +0 -36
  364. package/src/db/migrations/008-dropped-messages.ts +0 -27
  365. package/src/db/migrations/009-drop-pending-credentials.ts +0 -13
  366. package/src/db/migrations/010-engage-modes.ts +0 -103
  367. package/src/db/migrations/011-pending-sender-approvals.ts +0 -40
  368. package/src/db/migrations/012-channel-registration.ts +0 -48
  369. package/src/db/migrations/013-approval-render-metadata.ts +0 -27
  370. package/src/db/migrations/014-secrets.ts +0 -44
  371. package/src/db/migrations/015-secrets-drop-host-pattern.ts +0 -18
  372. package/src/db/migrations/016-secret-assignments.ts +0 -30
  373. package/src/db/migrations/017-agent-activity.ts +0 -40
  374. package/src/db/migrations/018-oauth-app-configs.ts +0 -34
  375. package/src/db/migrations/019-oauth-app-connections.ts +0 -48
  376. package/src/db/migrations/020-agent-app-connections.ts +0 -28
  377. package/src/db/migrations/021-pending-oauth-states.ts +0 -35
  378. package/src/db/migrations/022-app-connections-provider.ts +0 -25
  379. package/src/db/migrations/023-agent-group-secret-mode.test.ts +0 -124
  380. package/src/db/migrations/023-agent-group-secret-mode.ts +0 -65
  381. package/src/db/migrations/024-collapse-approvals.test.ts +0 -249
  382. package/src/db/migrations/024-collapse-approvals.ts +0 -182
  383. package/src/db/migrations/025-secret-mode-check.test.ts +0 -155
  384. package/src/db/migrations/025-secret-mode-check.ts +0 -49
  385. package/src/db/migrations/026-user-dms-bot-id.test.ts +0 -116
  386. package/src/db/migrations/026-user-dms-bot-id.ts +0 -54
  387. package/src/db/migrations/027-provider-credentials.ts +0 -41
  388. package/src/db/migrations/_test-helpers.ts +0 -41
  389. package/src/db/migrations/index.ts +0 -127
  390. package/src/db/migrations/module-agent-to-agent-destinations.ts +0 -84
  391. package/src/db/migrations/module-approvals-pending-approvals.ts +0 -42
  392. package/src/db/migrations/module-approvals-title-options.ts +0 -40
  393. package/src/db/schema.ts +0 -258
  394. package/src/db/session-db.test.ts +0 -93
  395. package/src/db/session-db.ts +0 -325
  396. package/src/db/sessions.ts +0 -241
  397. package/src/delivery.test.ts +0 -148
  398. package/src/delivery.ts +0 -445
  399. package/src/env.ts +0 -74
  400. package/src/group-folder.test.ts +0 -35
  401. package/src/group-folder.ts +0 -44
  402. package/src/group-init.ts +0 -92
  403. package/src/host-core.test.ts +0 -456
  404. package/src/host-sweep.test.ts +0 -146
  405. package/src/host-sweep.ts +0 -287
  406. package/src/index.ts +0 -227
  407. package/src/install-slug.ts +0 -33
  408. package/src/log.test.ts +0 -81
  409. package/src/log.ts +0 -117
  410. package/src/mcp/http.ts +0 -72
  411. package/src/mcp/server.ts +0 -92
  412. package/src/mcp/stdio.ts +0 -51
  413. package/src/mcp/tools/activity.ts +0 -88
  414. package/src/mcp/tools/agent-groups.ts +0 -183
  415. package/src/mcp/tools/approvals.ts +0 -122
  416. package/src/mcp/tools/channels.ts +0 -199
  417. package/src/mcp/tools/index.ts +0 -27
  418. package/src/mcp/tools/oauth.ts +0 -48
  419. package/src/mcp/tools/secrets.ts +0 -169
  420. package/src/mcp/tools/sessions.ts +0 -135
  421. package/src/mcp/types.ts +0 -51
  422. package/src/modules/agent-to-agent/agent-route.test.ts +0 -46
  423. package/src/modules/agent-to-agent/agent-route.ts +0 -223
  424. package/src/modules/agent-to-agent/create-agent.ts +0 -127
  425. package/src/modules/agent-to-agent/db/agent-destinations.ts +0 -135
  426. package/src/modules/agent-to-agent/index.ts +0 -22
  427. package/src/modules/agent-to-agent/write-destinations.ts +0 -59
  428. package/src/modules/approvals/agent.md +0 -45
  429. package/src/modules/approvals/index.ts +0 -21
  430. package/src/modules/approvals/picks.test.ts +0 -291
  431. package/src/modules/approvals/primitive.ts +0 -279
  432. package/src/modules/approvals/project.md +0 -27
  433. package/src/modules/approvals/response-handler.ts +0 -87
  434. package/src/modules/index.ts +0 -24
  435. package/src/modules/interactive/agent.md +0 -21
  436. package/src/modules/interactive/index.ts +0 -69
  437. package/src/modules/interactive/project.md +0 -12
  438. package/src/modules/mount-security/index.ts +0 -448
  439. package/src/modules/mount-security/migrate.test.ts +0 -91
  440. package/src/modules/permissions/access.ts +0 -28
  441. package/src/modules/permissions/channel-approval.test.ts +0 -389
  442. package/src/modules/permissions/channel-approval.ts +0 -188
  443. package/src/modules/permissions/db/agent-group-members.ts +0 -44
  444. package/src/modules/permissions/db/pending-channel-approvals.test.ts +0 -86
  445. package/src/modules/permissions/db/pending-channel-approvals.ts +0 -66
  446. package/src/modules/permissions/db/pending-sender-approvals.ts +0 -60
  447. package/src/modules/permissions/db/user-dms.ts +0 -58
  448. package/src/modules/permissions/db/user-roles.ts +0 -85
  449. package/src/modules/permissions/db/users.ts +0 -38
  450. package/src/modules/permissions/index.ts +0 -421
  451. package/src/modules/permissions/permissions.test.ts +0 -358
  452. package/src/modules/permissions/sender-approval.test.ts +0 -470
  453. package/src/modules/permissions/sender-approval.ts +0 -165
  454. package/src/modules/permissions/user-dm.ts +0 -200
  455. package/src/modules/provider-credentials/db.ts +0 -121
  456. package/src/modules/provider-credentials/index.ts +0 -12
  457. package/src/modules/provider-credentials/spawn.test.ts +0 -206
  458. package/src/modules/provider-credentials/spawn.ts +0 -114
  459. package/src/modules/scheduling/actions.ts +0 -113
  460. package/src/modules/scheduling/db.test.ts +0 -282
  461. package/src/modules/scheduling/db.ts +0 -148
  462. package/src/modules/scheduling/index.ts +0 -34
  463. package/src/modules/scheduling/recurrence.test.ts +0 -98
  464. package/src/modules/scheduling/recurrence.ts +0 -54
  465. package/src/modules/self-mod/agent.md +0 -30
  466. package/src/modules/self-mod/apply.ts +0 -85
  467. package/src/modules/self-mod/index.ts +0 -30
  468. package/src/modules/self-mod/project.md +0 -39
  469. package/src/modules/self-mod/request.ts +0 -91
  470. package/src/modules/typing/index.ts +0 -165
  471. package/src/oauth/agent-app-connections.ts +0 -103
  472. package/src/oauth/app-configs.test.ts +0 -64
  473. package/src/oauth/app-configs.ts +0 -114
  474. package/src/oauth/app-connections.test.ts +0 -109
  475. package/src/oauth/app-connections.ts +0 -178
  476. package/src/oauth/crypto.ts +0 -56
  477. package/src/oauth/flow.ts +0 -104
  478. package/src/oauth/providers/google.test.ts +0 -38
  479. package/src/oauth/providers/google.ts +0 -46
  480. package/src/oauth/providers/index.ts +0 -48
  481. package/src/oauth/state-store.test.ts +0 -54
  482. package/src/oauth/state-store.ts +0 -93
  483. package/src/parachute/README.md +0 -27
  484. package/src/parachute/create-agent.test.ts +0 -83
  485. package/src/parachute/create-agent.ts +0 -122
  486. package/src/parachute/group-status.test.ts +0 -165
  487. package/src/parachute/group-status.ts +0 -136
  488. package/src/parachute/types.ts +0 -41
  489. package/src/parachute/vault-mcp.test.ts +0 -251
  490. package/src/parachute/vault-mcp.ts +0 -232
  491. package/src/platform-id.test.ts +0 -104
  492. package/src/platform-id.ts +0 -109
  493. package/src/providers/index.ts +0 -6
  494. package/src/providers/provider-container-registry.ts +0 -58
  495. package/src/response-registry.ts +0 -45
  496. package/src/router.ts +0 -530
  497. package/src/secrets/crypto.test.ts +0 -45
  498. package/src/secrets/crypto.ts +0 -55
  499. package/src/secrets/index.ts +0 -355
  500. package/src/secrets/master-key.ts +0 -70
  501. package/src/secrets/secrets.test.ts +0 -354
  502. package/src/session-manager.migrate.test.ts +0 -59
  503. package/src/session-manager.ts +0 -433
  504. package/src/startup-bootstrap.test.ts +0 -226
  505. package/src/startup-bootstrap.ts +0 -207
  506. package/src/state-sqlite.ts +0 -182
  507. package/src/timezone.test.ts +0 -64
  508. package/src/timezone.ts +0 -37
  509. package/src/types.ts +0 -230
  510. package/src/web/auth.test.ts +0 -335
  511. package/src/web/auth.ts +0 -214
  512. package/src/web/discord-validate.test.ts +0 -77
  513. package/src/web/discord-validate.ts +0 -88
  514. package/src/web/hub-discovery.test.ts +0 -98
  515. package/src/web/hub-discovery.ts +0 -69
  516. package/src/web/routes/activity.ts +0 -106
  517. package/src/web/routes/agent-provider.test.ts +0 -282
  518. package/src/web/routes/agent-provider.ts +0 -309
  519. package/src/web/routes/approvals.ts +0 -185
  520. package/src/web/routes/apps.ts +0 -434
  521. package/src/web/routes/channels-mg-detail.test.ts +0 -324
  522. package/src/web/routes/channels-mga-detail.test.ts +0 -425
  523. package/src/web/routes/channels.ts +0 -489
  524. package/src/web/routes/oauth-providers.ts +0 -42
  525. package/src/web/routes/secrets.test.ts +0 -175
  526. package/src/web/routes/secrets.ts +0 -282
  527. package/src/web/routes/sessions.ts +0 -123
  528. package/src/web/routes/settings.test.ts +0 -106
  529. package/src/web/routes/settings.ts +0 -247
  530. package/src/web/routes/setup-status.ts +0 -205
  531. package/src/web/routes/vaults.test.ts +0 -389
  532. package/src/web/routes/vaults.ts +0 -225
  533. package/src/web/server-version.test.ts +0 -16
  534. package/src/web/server.ts +0 -1003
  535. package/src/web/services-manifest.test.ts +0 -120
  536. package/src/web/services-manifest.ts +0 -61
  537. package/src/web/static-serve.test.ts +0 -255
  538. package/src/web/static-serve.ts +0 -104
  539. package/src/web/telegram-validate.test.ts +0 -116
  540. package/src/web/telegram-validate.ts +0 -107
  541. package/src/web/vault-proxy.test.ts +0 -214
  542. package/src/web/vault-proxy.ts +0 -120
  543. package/src/web/wire-channel.ts +0 -181
  544. package/src/webhook-server.ts +0 -134
  545. package/vitest.config.ts +0 -18
  546. package/web/README.md +0 -63
  547. package/web/ui/index.html +0 -13
  548. package/web/ui/package.json +0 -35
  549. package/web/ui/pnpm-lock.yaml +0 -2164
  550. package/web/ui/scripts/verify-base.mjs +0 -31
  551. package/web/ui/src/App.tsx +0 -88
  552. package/web/ui/src/components/ActivityFeed.tsx +0 -444
  553. package/web/ui/src/components/AgentGroupPicker.tsx +0 -263
  554. package/web/ui/src/components/AgentProviderCards.tsx +0 -220
  555. package/web/ui/src/components/CredentialForm.tsx +0 -214
  556. package/web/ui/src/components/ScopeGrants.tsx +0 -74
  557. package/web/ui/src/components/StatusDot.tsx +0 -43
  558. package/web/ui/src/components/VaultPicker.tsx +0 -127
  559. package/web/ui/src/components/setup/AdapterInstallStep.tsx +0 -178
  560. package/web/ui/src/components/setup/AgentGroupStep.tsx +0 -43
  561. package/web/ui/src/components/setup/ChannelPickStep.tsx +0 -74
  562. package/web/ui/src/components/setup/DoneStep.tsx +0 -49
  563. package/web/ui/src/components/setup/PrereqStep.tsx +0 -129
  564. package/web/ui/src/components/setup/TestConnectionStep.tsx +0 -108
  565. package/web/ui/src/components/setup/TestMessageStep.tsx +0 -104
  566. package/web/ui/src/components/setup/WireChannelStep.tsx +0 -166
  567. package/web/ui/src/components/setup/types.ts +0 -105
  568. package/web/ui/src/lib/api.test.ts +0 -410
  569. package/web/ui/src/lib/api.ts +0 -1210
  570. package/web/ui/src/lib/auth.test.ts +0 -139
  571. package/web/ui/src/lib/auth.ts +0 -348
  572. package/web/ui/src/lib/channel-adapters.ts +0 -136
  573. package/web/ui/src/main.tsx +0 -19
  574. package/web/ui/src/routes/ApprovalsList.tsx +0 -294
  575. package/web/ui/src/routes/Apps.tsx +0 -613
  576. package/web/ui/src/routes/ChannelWireDetail.test.tsx +0 -233
  577. package/web/ui/src/routes/ChannelWireDetail.tsx +0 -403
  578. package/web/ui/src/routes/ChannelsList.tsx +0 -158
  579. package/web/ui/src/routes/GroupDetail.tsx +0 -755
  580. package/web/ui/src/routes/GroupList.tsx +0 -187
  581. package/web/ui/src/routes/MessagingGroupDetail.test.tsx +0 -233
  582. package/web/ui/src/routes/MessagingGroupDetail.tsx +0 -306
  583. package/web/ui/src/routes/NewGroupWizard.tsx +0 -390
  584. package/web/ui/src/routes/OAuthCallback.tsx +0 -56
  585. package/web/ui/src/routes/SecretsList.tsx +0 -921
  586. package/web/ui/src/routes/SessionsList.tsx +0 -220
  587. package/web/ui/src/routes/SettingsAgentProvider.tsx +0 -109
  588. package/web/ui/src/routes/SettingsApprovals.tsx +0 -234
  589. package/web/ui/src/routes/SetupWizard.tsx +0 -219
  590. package/web/ui/src/routes/VaultDetail.test.tsx +0 -361
  591. package/web/ui/src/routes/VaultDetail.tsx +0 -960
  592. package/web/ui/src/routes/VaultsList.tsx +0 -295
  593. package/web/ui/src/routes/WireChannelPage.tsx +0 -413
  594. package/web/ui/src/styles.css +0 -608
  595. package/web/ui/src/test/setup.ts +0 -23
  596. package/web/ui/src/vite-env.d.ts +0 -10
  597. package/web/ui/vite.config.ts +0 -34
  598. package/web/ui/vitest.config.ts +0 -25
@@ -0,0 +1,569 @@
1
+ /**
2
+ * SHARED spawn helpers — the sandbox/filesystem/env/spec-persistence primitives
3
+ * that BOTH live agent backends build on:
4
+ *
5
+ * - the PROGRAMMATIC backend (`src/backends/programmatic.ts`) — `claude -p` turns;
6
+ * - the PARKED interactive spawner (`src/_parked/interactive-spawn.ts`) — the
7
+ * retired tmux backend, kept for future terminal/process-mgmt (design
8
+ * 2026-06-19-retire-interactive-backend.md).
9
+ *
10
+ * What lives here:
11
+ * - {@link wrapArgvInSandbox} — the ONE place the sandbox/egress/filesystem policy
12
+ * is applied to a launch argv (every launch gets the same egress floor + scoped-
13
+ * read confinement);
14
+ * - {@link seedAgentHome} — the per-session writable HOME (the stability keystone);
15
+ * - {@link buildAgentChildEnv} — the scrubbed child env (NEVER `ANTHROPIC_API_KEY`;
16
+ * the session runs on the subscription via `CLAUDE_CODE_OAUTH_TOKEN`, §6);
17
+ * - {@link resolveAgentCwd} / {@link sessionWorkspace} / {@link persistSpec} /
18
+ * {@link readPersistedSpec} / {@link shellJoin} — the spec/path/quoting helpers.
19
+ *
20
+ * The interactive tmux SPAWNER itself (the `claude` argv, the launch script, the
21
+ * dev-channels-consent auto-answer, `spawnAgent`, the `TmuxLauncher`) was PARKED to
22
+ * `src/_parked/interactive-spawn.ts` when the interactive backend retired — it
23
+ * imports these helpers, it didn't fork them.
24
+ */
25
+
26
+ import { writeFileSync, mkdirSync, chmodSync, existsSync, readFileSync } from "node:fs";
27
+ import { homedir } from "node:os";
28
+ import { join } from "node:path";
29
+ import type { AgentSpec, BaseBinds } from "./sandbox/types.ts";
30
+ import { Sandbox, type SandboxEngine, type WrappedCommand } from "./sandbox/index.ts";
31
+ import type { EgressBaseInput } from "./sandbox/egress.ts";
32
+ import { DENYLISTED_ENV } from "./credentials.ts";
33
+
34
+ /**
35
+ * Slug guard for `spec.name`. The name is used UNESCAPED as a tmux session
36
+ * target (`-t`) and a path segment under `sessionsDir`, so it must be a strict
37
+ * slug — mirrors `scripts/launch-session.sh`'s existing check. Anything with
38
+ * `..`, `/`, or spaces would traverse the sessions dir or break tmux targeting.
39
+ * Phase 2 makes spawns API/MCP-triggered (the name becomes less-trusted input),
40
+ * so the guard is enforced now, before any fs/tmux side effect.
41
+ */
42
+ const AGENT_NAME_SLUG = /^[a-z0-9_-]+$/i;
43
+
44
+ /**
45
+ * Process-wide serialization for the sandbox-runtime singleton. `SandboxManager`
46
+ * is global (initialize → wrap → reset share one set of host proxies), so two
47
+ * concurrent `spawnAgent` calls would race the initialize→wrap window (a second
48
+ * `initialize` could clobber the first's config before its command is wrapped).
49
+ * Only that brief window needs the lock — the sandbox policy is baked into the
50
+ * argv at `wrapWithSandboxArgv`, after which the spawned process runs
51
+ * independently. This is a minimal FIFO async mutex: each acquirer chains onto
52
+ * the previous one's release.
53
+ */
54
+ let spawnLock: Promise<void> = Promise.resolve();
55
+ async function withSpawnLock<T>(fn: () => Promise<T>): Promise<T> {
56
+ const prior = spawnLock;
57
+ let release!: () => void;
58
+ spawnLock = new Promise<void>((r) => (release = r));
59
+ await prior;
60
+ try {
61
+ return await fn();
62
+ } finally {
63
+ release();
64
+ }
65
+ }
66
+
67
+ /**
68
+ * Inputs to {@link wrapArgvInSandbox} — the spec (carries network/filesystem/
69
+ * mounts/egress), the workspace + runtime read binds, the egress base origins, the
70
+ * argv to run, and the engine + ripgrep overrides.
71
+ */
72
+ export interface WrapArgvInSandboxInput {
73
+ /** The agent spec — its network/filesystem/egress/mounts drive the sandbox config. */
74
+ spec: AgentSpec;
75
+ /** Private per-session workspace (rw). */
76
+ workspace: string;
77
+ /** Read-only runtime/claude-config binds the session needs to run `claude`. */
78
+ runtimeReadOnly: string[];
79
+ /** Hub origin for the non-removable egress base. */
80
+ hubOrigin: string;
81
+ /** Vault origin for the egress base (if the spec binds a vault). */
82
+ vaultUrl?: string;
83
+ /** The argv to sandbox-wrap (e.g. the `claude …` invocation). */
84
+ argv: string[];
85
+ /** Sandbox engine override (tests inject a fake). */
86
+ sandboxEngine?: SandboxEngine;
87
+ /**
88
+ * Optional ripgrep override threaded to the sandbox (macOS deny-path scan needs a
89
+ * real `rg`; pass one when the host has none on PATH).
90
+ */
91
+ ripgrep?: { command: string; args?: string[] };
92
+ }
93
+
94
+ /**
95
+ * Sandbox-wrap an argv for one launch — the SHARED sandbox seam both the
96
+ * programmatic backend (`claude -p`) and the parked interactive spawner (tmux
97
+ * `claude`, `src/_parked/interactive-spawn.ts`) call. Extracted so the sandbox/
98
+ * egress/filesystem policy lives in
99
+ * exactly ONE place: every launch, regardless of backend, gets the same egress
100
+ * floor (§4.4) + scoped-read confinement (§4.5) baked into its argv.
101
+ *
102
+ * It owns the process-wide serialization of the sandbox-runtime singleton's
103
+ * initialize→wrap window (`withSpawnLock`): the engine is global (one set of host
104
+ * proxies), so two concurrent wraps would race the initialize→wrap window. Only
105
+ * that brief window holds the lock — the policy is baked into the returned argv at
106
+ * `wrap`, after which the spawned process runs independently.
107
+ */
108
+ export async function wrapArgvInSandbox(input: WrapArgvInSandboxInput): Promise<WrappedCommand> {
109
+ const baseBinds: BaseBinds = {
110
+ workspace: input.workspace,
111
+ runtimeReadOnly: input.runtimeReadOnly,
112
+ };
113
+ const egressBase: EgressBaseInput = {
114
+ hubOrigin: input.hubOrigin,
115
+ ...(input.vaultUrl ? { vaultOrigin: input.vaultUrl } : {}),
116
+ };
117
+ const sandbox = new Sandbox(input.sandboxEngine);
118
+ return withSpawnLock(() =>
119
+ sandbox.wrap({
120
+ spec: input.spec,
121
+ baseBinds,
122
+ egressBase,
123
+ command: shellJoin(input.argv),
124
+ ...(input.ripgrep ? { ripgrep: input.ripgrep } : {}),
125
+ }),
126
+ );
127
+ }
128
+
129
+ /**
130
+ * The SHARED, NON-tmux deps a real session launch needs (hub origin + manager
131
+ * bearer for minting, channel/vault URLs, the sessions dir, the runtime read binds,
132
+ * the per-channel credential/env resolvers, sandbox/ripgrep overrides). The
133
+ * programmatic backend reads its slice of these; `resolveSpawnDeps` builds them.
134
+ *
135
+ * The PARKED interactive spawner extends this with a `tmux` launcher
136
+ * (`SpawnAgentDeps` in `src/_parked/interactive-spawn.ts`); the live tree never
137
+ * carries a tmux launcher in its deps.
138
+ */
139
+ export interface SpawnAgentBaseDeps {
140
+ /** Hub origin + manager bearer for minting (§4.3). */
141
+ hubOrigin: string;
142
+ managerBearer: string;
143
+ /** Daemon base URL the channel MCP endpoints live under. */
144
+ channelUrl: string;
145
+ /** Vault base URL (if the spec binds a vault). Defaults to hubOrigin. */
146
+ vaultUrl?: string;
147
+ /** Base for session workspaces (e.g. `~/.parachute/agent/sessions`). */
148
+ sessionsDir: string;
149
+ /**
150
+ * Read-only runtime/config binds the sandbox always grants (the claude config
151
+ * dir, etc.). Workspace is derived per-session under `sessionsDir`.
152
+ */
153
+ runtimeReadOnly: string[];
154
+ /**
155
+ * Resolve the Claude OAuth token to inject as `CLAUDE_CODE_OAUTH_TOKEN`, given
156
+ * the spec's wake channel. Defaults to the real per-channel secret store
157
+ * (`credentials.ts` — channel override ?? default/operator ?? throw). The store
158
+ * throws `CredentialNotConfiguredError` when neither is set, which aborts the
159
+ * launch BEFORE any side effect (no session ever runs without auth).
160
+ */
161
+ resolveClaudeToken?: (channel: string) => string;
162
+ /**
163
+ * Resolve the per-channel ENV vars (the GH_TOKEN/CLOUDFLARE_* slice) to inject
164
+ * into the sandboxed child. Read at spawn time so a var set via the config API
165
+ * applies on the next spawn without a daemon restart. A missing/empty store
166
+ * resolves to `{}` (env injection is optional).
167
+ */
168
+ resolveChannelEnv?: (channel: string) => Record<string, string>;
169
+ /** Sandbox engine override (tests inject a fake). */
170
+ sandboxEngine?: SandboxEngine;
171
+ /** fetch override for the mint client (tests). */
172
+ fetchFn?: typeof fetch;
173
+ /** Parent env to scrub from. Defaults to process.env. */
174
+ parentEnv?: Record<string, string | undefined>;
175
+ /** claude binary. Defaults to "claude" (resolved by the shell at run, not us). */
176
+ claudeBin?: string;
177
+ /**
178
+ * Optional ripgrep override threaded to the sandbox (macOS deny-path scan needs
179
+ * a real `rg` binary; pass one when the host has none on PATH).
180
+ */
181
+ ripgrep?: { command: string; args?: string[] };
182
+ }
183
+
184
+ /** Per-session workspace dir under the sessions base. */
185
+ export function sessionWorkspace(sessionsDir: string, specName: string): string {
186
+ return join(sessionsDir, specName);
187
+ }
188
+
189
+ /**
190
+ * Resolve an agent's CWD (the working-directory axis, design
191
+ * 2026-06-16-agent-filesystem-and-sharing.md). When the spec sets `workspace`
192
+ * (the shared real dir the agent works from) the cwd is that dir; otherwise it's
193
+ * the agent's PRIVATE per-session dir (today's behavior, exactly).
194
+ *
195
+ * This is ONLY the cwd. The private dir always remains the home for `.mcp.json`,
196
+ * `spec.json`, `system-prompt.txt`, the seeded `CLAUDE_CONFIG_DIR`, and `tmp` —
197
+ * those are passed to `claude` by ABSOLUTE path (`--mcp-config`,
198
+ * `--system-prompt-file`, `CLAUDE_CONFIG_DIR`/`TMPDIR` env) so they're unaffected
199
+ * by the cwd change. The decoupling keeps the working dir shareable while the
200
+ * credential-bearing private home stays per-agent.
201
+ */
202
+ export function resolveAgentCwd(spec: AgentSpec, privateWorkspace: string): string {
203
+ return typeof spec.workspace === "string" && spec.workspace.length > 0
204
+ ? spec.workspace
205
+ : privateWorkspace;
206
+ }
207
+
208
+ /** Path to the persisted spawn-spec for a session (recovered by restart). */
209
+ export function specFilePath(workspace: string): string {
210
+ return join(workspace, "spec.json");
211
+ }
212
+
213
+ /**
214
+ * Persist the spawn {@link AgentSpec} alongside the session workspace so a
215
+ * per-session restart can faithfully reproduce the original launch (same channels,
216
+ * vault, network, mounts) WITHOUT re-asking the operator. The live tmux session
217
+ * carries none of this — `GET /api/agents` only knows name + attached — and the
218
+ * workspace's `.mcp.json` inlines minted tokens (not a clean spec), so the spec
219
+ * itself is the recoverable source of truth.
220
+ *
221
+ * The spec is NON-SECRET (channel names, access verbs, vault name, host paths) —
222
+ * the actual credentials live in credentials.json (Claude) / the env store and are
223
+ * re-resolved at each (re)spawn. We still write it 0600 (matching the workspace's
224
+ * secret-bearing `.mcp.json`): the per-session workspace dir is umask-inherited (no
225
+ * tighter than 0755), so 0600 on the file is the real guard — defense-in-depth that
226
+ * also keeps the perms honest if a future field ever does carry something sensitive.
227
+ * `chmod`-ed unconditionally since writeFileSync's `mode` only applies on create.
228
+ * Returns the path written.
229
+ */
230
+ export function persistSpec(workspace: string, spec: AgentSpec): string {
231
+ mkdirSync(workspace, { recursive: true });
232
+ const path = specFilePath(workspace);
233
+ writeFileSync(path, JSON.stringify(spec, null, 2) + "\n", { mode: 0o600 });
234
+ chmodSync(path, 0o600);
235
+ return path;
236
+ }
237
+
238
+ /** Read a persisted spawn-spec, or null if absent/unreadable. */
239
+ export function readPersistedSpec(workspace: string): AgentSpec | null {
240
+ const path = specFilePath(workspace);
241
+ if (!existsSync(path)) return null;
242
+ try {
243
+ return JSON.parse(readFileSync(path, "utf-8")) as AgentSpec;
244
+ } catch {
245
+ return null;
246
+ }
247
+ }
248
+
249
+ /**
250
+ * The ONLY env keys we accept FROM the sandbox engine's returned `wrapped.env`.
251
+ *
252
+ * CRITICAL ISOLATION CONTRACT. `@anthropic-ai/sandbox-runtime`'s
253
+ * `wrapWithSandboxArgv` returns `env: process.env` on macOS/Linux (the proxy/sandbox
254
+ * vars are baked into the wrapped COMMAND STRING via an `env VAR=… sandbox-exec …`
255
+ * prefix / bwrap `--setenv`, NOT into the returned env) and `{...process.env, ...proxy}`
256
+ * on Windows (where the proxy vars DO ride in the returned env). So `wrapped.env` is
257
+ * essentially the WHOLE daemon env. If we spread it over the scrubbed `childEnv`, the
258
+ * daemon's ambient `ANTHROPIC_API_KEY` / any other secret would OVERRIDE the scrub and
259
+ * reach the sandboxed turn — defeating `buildAgentChildEnv` entirely (the
260
+ * subscription-billing + no-secret-leak guarantee). So we ALLOWLIST: from `wrapped.env`
261
+ * we keep ONLY these known sandbox/proxy keys (the exact set the runtime's
262
+ * `generateProxyEnvVars` + the Linux bwrap `--setenv` markers emit — needed so the
263
+ * egress proxy works, esp. on Windows where they ride in the returned env), and drop
264
+ * everything else. {@link DENYLISTED_ENV} is re-applied to whatever we keep as belt-
265
+ * and-suspenders so the Claude-auth trio can NEVER enter via this seam.
266
+ *
267
+ * Source of truth: `@anthropic-ai/sandbox-runtime` `sandbox-utils.generateProxyEnvVars`
268
+ * (`SANDBOX_RUNTIME`, `TMPDIR`, `CA_TRUST_VARS`, `NO_PROXY`/proxy/socks/git-ssh/docker/
269
+ * cloudsdk/grpc vars) + `linux-sandbox-utils` (`CLAUDE_CODE_HOST_*_PROXY_PORT`). Raise
270
+ * this set alongside the pinned-engine upgrade gate if the runtime adds a launch var.
271
+ */
272
+ export const SANDBOX_ENV_ALLOWLIST: ReadonlySet<string> = new Set([
273
+ // Sandbox markers + the per-session temp dir. NB: TMPDIR is allowlisted for the
274
+ // WINDOWS path (where it rides in the returned env dict); on macOS/Linux it's baked
275
+ // into the command string. Either way `homeEnv` (seedAgentHome's per-workspace tmp)
276
+ // is layered LAST in mergeSandboxLaunchEnv, so the session's own TMPDIR wins by design.
277
+ "SANDBOX_RUNTIME",
278
+ "TMPDIR",
279
+ // CA trust stores (CA_TRUST_VARS) — when the proxy terminates TLS the child must
280
+ // trust the proxy-minted certs.
281
+ "NODE_EXTRA_CA_CERTS",
282
+ "SSL_CERT_FILE",
283
+ "CURL_CA_BUNDLE",
284
+ "REQUESTS_CA_BUNDLE",
285
+ "PIP_CERT",
286
+ "GIT_SSL_CAINFO",
287
+ "AWS_CA_BUNDLE",
288
+ "CARGO_HTTP_CAINFO",
289
+ "DENO_CERT",
290
+ // Proxy routing (upper + lower case) — the egress floor. Without these the
291
+ // sandboxed turn loses network on platforms that carry them in the returned env.
292
+ "NO_PROXY",
293
+ "no_proxy",
294
+ "HTTP_PROXY",
295
+ "http_proxy",
296
+ "HTTPS_PROXY",
297
+ "https_proxy",
298
+ "ALL_PROXY",
299
+ "all_proxy",
300
+ "FTP_PROXY",
301
+ "ftp_proxy",
302
+ "RSYNC_PROXY",
303
+ "GRPC_PROXY",
304
+ "grpc_proxy",
305
+ "DOCKER_HTTP_PROXY",
306
+ "DOCKER_HTTPS_PROXY",
307
+ "CLOUDSDK_PROXY_TYPE",
308
+ "CLOUDSDK_PROXY_ADDRESS",
309
+ "CLOUDSDK_PROXY_PORT",
310
+ // Git-over-SSH through the SOCKS/HTTP proxy.
311
+ "GIT_SSH_COMMAND",
312
+ // Linux bwrap host-proxy-port markers (debug/transparency).
313
+ "CLAUDE_CODE_HOST_HTTP_PROXY_PORT",
314
+ "CLAUDE_CODE_HOST_SOCKS_PROXY_PORT",
315
+ ]);
316
+
317
+ /**
318
+ * Compose the FINAL launch env for a sandboxed turn so the SCRUB WINS.
319
+ *
320
+ * Layering (lowest → highest precedence):
321
+ * 1. `childEnv` — the scrubbed allowlist from {@link buildAgentChildEnv} (authoritative).
322
+ * 2. the sandbox/proxy ALLOWLIST drawn from `wrappedEnv` ({@link SANDBOX_ENV_ALLOWLIST}),
323
+ * with {@link DENYLISTED_ENV} re-applied defensively — only known egress/sandbox
324
+ * vars layer on, never the daemon's ambient `process.env` (the old `...wrappedEnv`
325
+ * spread leaked it).
326
+ * 3. `homeEnv` — `seedAgentHome`'s CLAUDE_CONFIG_DIR/XDG/TMP overrides win last.
327
+ *
328
+ * `CLAUDE_CODE_OAUTH_TOKEN` (set last by `buildAgentChildEnv`) survives: it is not in the
329
+ * allowlist AND is denylisted, so step 2 can never overwrite it; step 3 doesn't set it.
330
+ */
331
+ export function mergeSandboxLaunchEnv(
332
+ childEnv: Record<string, string>,
333
+ wrappedEnv: Record<string, string | undefined>,
334
+ homeEnv: Record<string, string>,
335
+ ): Record<string, string | undefined> {
336
+ const out: Record<string, string | undefined> = { ...childEnv };
337
+ for (const [k, v] of Object.entries(wrappedEnv)) {
338
+ if (typeof v !== "string" || v.length === 0) continue;
339
+ if (!SANDBOX_ENV_ALLOWLIST.has(k)) continue; // drop the daemon's ambient env
340
+ if (DENYLISTED_ENV.has(k)) continue; // never re-admit the Claude-auth trio here
341
+ out[k] = v;
342
+ }
343
+ return { ...out, ...homeEnv };
344
+ }
345
+
346
+ /**
347
+ * Build the scrubbed child env for the sandboxed claude. Mirrors runner's
348
+ * passthrough allowlist MINUS `ANTHROPIC_API_KEY` (and the `ANTHROPIC_*`/`CLAUDE_*`
349
+ * wildcards, which would re-admit it) — the channel session runs on the
350
+ * interactive subscription, so an API key must never leak in (§6). The injected
351
+ * `CLAUDE_CODE_OAUTH_TOKEN` is the session's auth.
352
+ *
353
+ * The sandbox engine's own env (proxy vars, sandbox markers) is layered on TOP of
354
+ * this by {@link mergeSandboxLaunchEnv} — but as an ALLOWLIST
355
+ * ({@link SANDBOX_ENV_ALLOWLIST}), NOT the whole returned `wrapped.env` (which is the
356
+ * daemon's `process.env` and would re-admit the scrubbed secrets). This scrubbed env
357
+ * is authoritative; only known sandbox/proxy keys + the home overrides layer on top.
358
+ *
359
+ * SECURITY POSTURE — the per-channel env injection (`channelEnv`):
360
+ *
361
+ * The operator scopes a channel's spawned agent extra credentials/vars
362
+ * (`GH_TOKEN`, `CLOUDFLARE_API_TOKEN`, …) via the env store (credentials.ts).
363
+ * They are resolved at SPAWN time (issuance-time scoping: the sandbox only ever
364
+ * sees the minimal set the operator configured for THAT channel, never the
365
+ * daemon's own ambient process env), then merged here. The layering is precise
366
+ * so the injection can only ADD capability, never subvert the two guarantees:
367
+ *
368
+ * 1. `channelEnv` is applied FIRST (as the base), THEN the structural
369
+ * passthrough (PATH/HOME/locale) and FINALLY `CLAUDE_CODE_OAUTH_TOKEN` —
370
+ * so a channel-set var can never clobber the Claude auth token or a
371
+ * structural fundamental. (seedAgentHome's CLAUDE_CONFIG_DIR/XDG/TMP layer
372
+ * even later, in spawnAgent, so those win too.)
373
+ * 2. Denylisted keys (ANTHROPIC_API_KEY / CLAUDE_API_KEY / CLAUDE_CODE_OAUTH_TOKEN)
374
+ * are dropped defensively with a warning — the setter already blocks them
375
+ * and `resolveChannelEnv` already strips them, so this is belt-and-suspenders
376
+ * for a hand-edited credentials.json: the subscription-billing + managed-auth
377
+ * guarantee holds even if the store is tampered with.
378
+ */
379
+ export function buildAgentChildEnv(
380
+ parentEnv: Record<string, string | undefined>,
381
+ claudeOauthToken: string,
382
+ channelEnv: Record<string, string> = {},
383
+ ): Record<string, string> {
384
+ const out: Record<string, string> = {};
385
+
386
+ // 1. The operator-scoped per-channel env goes in FIRST (lowest precedence) so the
387
+ // structural passthrough + the Claude auth token below always win. Drop any
388
+ // denylisted key defensively (the store already blocks them; this guards a
389
+ // hand-edited file from smuggling an API key / a swapped OAuth token in).
390
+ for (const [k, v] of Object.entries(channelEnv)) {
391
+ if (typeof v !== "string" || v.length === 0) continue;
392
+ if (DENYLISTED_ENV.has(k)) {
393
+ console.warn(
394
+ `parachute-agent: refusing to inject denylisted env var "${k}" from the channel env store ` +
395
+ `(it controls Claude auth/billing) — skipping. Remove it from credentials.json.`,
396
+ );
397
+ continue;
398
+ }
399
+ out[k] = v;
400
+ }
401
+
402
+ // Fundamentals + locale, like runner — but NOT ANTHROPIC_API_KEY / CLAUDE_API_KEY.
403
+ const passthrough = [
404
+ "PATH",
405
+ "HOME",
406
+ "USER",
407
+ "LOGNAME",
408
+ "SHELL",
409
+ "TERM",
410
+ "LANG",
411
+ "TZ",
412
+ "CLAUDE_CONFIG_DIR",
413
+ "XDG_CONFIG_HOME",
414
+ "XDG_DATA_HOME",
415
+ "XDG_CACHE_HOME",
416
+ "XDG_STATE_HOME",
417
+ "XDG_RUNTIME_DIR",
418
+ ];
419
+ for (const k of passthrough) {
420
+ const v = parentEnv[k];
421
+ if (typeof v === "string" && v.length > 0) out[k] = v;
422
+ }
423
+ // Pass through LC_* locale vars only. Deliberately NOT the broad ANTHROPIC_*/
424
+ // CLAUDE_* wildcards runner uses — those would re-admit ANTHROPIC_API_KEY and
425
+ // route the session onto metered API billing instead of the subscription.
426
+ for (const [k, v] of Object.entries(parentEnv)) {
427
+ if (typeof v === "string" && v.length > 0 && k.startsWith("LC_")) out[k] = v;
428
+ }
429
+ if (!out.PATH) out.PATH = "/usr/local/bin:/usr/bin:/bin";
430
+
431
+ // The interactive subscription credential (design §6). Explicitly the ONLY
432
+ // Claude auth var set; ANTHROPIC_API_KEY is intentionally absent. Set LAST so no
433
+ // channel-injected var can ever override the session's managed auth.
434
+ out.CLAUDE_CODE_OAUTH_TOKEN = claudeOauthToken;
435
+ return out;
436
+ }
437
+
438
+ /**
439
+ * Create the agent's PRIVATE, WRITABLE HOME inside its workspace and seed it so
440
+ * claude starts straight into a usable session — no onboarding flow, no per-folder
441
+ * trust prompt — and so ALL of claude's config/cache/log/lock/temp writes land
442
+ * here instead of EPERM-ing against the operator's (read-only, shared) real home.
443
+ *
444
+ * This is the keystone of a STABLE sandbox: claude always has a home it can fully
445
+ * read AND write, decoupled from the operator's ~/.claude (so concurrent agents
446
+ * never race/corrupt it).
447
+ *
448
+ * The seed is based on the operator's REAL `~/.claude.json` so the agent inherits
449
+ * a fully-COMPLETED first run — onboarding, theme, and every version-migration
450
+ * flag — which is robust to claude's evolving first-run sub-steps (chasing them
451
+ * one-by-one is exactly the fragility this avoids). We then strip the heavy /
452
+ * private bits: `projects` is REPLACED with just this workspace (pre-trusted), and
453
+ * `oauthAccount` is dropped (the agent authenticates via CLAUDE_CODE_OAUTH_TOKEN).
454
+ * If the operator has no config, fall back to the two flags that gate the prompts.
455
+ *
456
+ * Returns the env overrides (CLAUDE_CONFIG_DIR + XDG_* + the temp vars — NOT HOME,
457
+ * which is deliberately left as the operator's so claude finds its real install) to
458
+ * layer LAST over the launch env so they win over the inherited + engine env.
459
+ * Idempotent: an existing seed is left as-is (claude owns it after first boot).
460
+ * `operatorConfigPath` is injectable for tests.
461
+ */
462
+ export function seedAgentHome(
463
+ workspace: string,
464
+ opts: { mcpServers?: string[]; operatorConfigPath?: string; projectRoot?: string } = {},
465
+ ): Record<string, string> {
466
+ const mcpServerNames = opts.mcpServers ?? [];
467
+ const operatorConfigPath = opts.operatorConfigPath ?? join(homedir(), ".claude.json");
468
+ // The project root claude pre-trusts in the seed. Defaults to the private
469
+ // workspace (today's behavior), but when the agent's CWD is a shared working dir
470
+ // (the spec's `workspace`), the CALLER passes that path here so claude's project
471
+ // (= its cwd) is pre-trusted + its MCP servers pre-approved — otherwise the agent
472
+ // would hit the per-folder trust / "new MCP server" prompts for the shared dir.
473
+ // The seeded HOME/config/tmp still live UNDER the private `workspace` regardless.
474
+ const projectRoot = opts.projectRoot ?? workspace;
475
+ const home = join(workspace, "home");
476
+ const claudeDir = join(home, ".claude");
477
+ const tmp = join(workspace, "tmp");
478
+ mkdirSync(claudeDir, { recursive: true });
479
+ mkdirSync(tmp, { recursive: true });
480
+ // claude reads its primary config from `$CLAUDE_CONFIG_DIR/.claude.json` when
481
+ // CLAUDE_CONFIG_DIR is set (which we set below, to claudeDir) — NOT
482
+ // `$HOME/.claude.json`. Seed THERE. Only seed if absent — after first boot
483
+ // claude owns this file.
484
+ const seedPath = join(claudeDir, ".claude.json");
485
+ if (!existsSync(seedPath)) {
486
+ let base: Record<string, unknown> = {};
487
+ try {
488
+ if (existsSync(operatorConfigPath)) {
489
+ base = JSON.parse(readFileSync(operatorConfigPath, "utf-8")) as Record<string, unknown>;
490
+ }
491
+ } catch {
492
+ base = {}; // unreadable/garbage operator config → minimal seed
493
+ }
494
+ delete base.oauthAccount; // don't copy the operator's account into the agent home
495
+ const seed = {
496
+ ...base,
497
+ hasCompletedOnboarding: true,
498
+ // Replace the operator's project history with ONLY the agent's project root
499
+ // (its cwd — the private workspace by default, or the shared working dir when
500
+ // the spec sets one), pre-trusted AND with our own configured MCP servers
501
+ // pre-approved (claude otherwise prompts "New MCP server found in this
502
+ // project" / the per-folder trust dialog — these are operator-configured, not
503
+ // foreign, so pre-approve them).
504
+ projects: {
505
+ [projectRoot]: {
506
+ hasTrustDialogAccepted: true,
507
+ hasCompletedProjectOnboarding: true,
508
+ enabledMcpjsonServers: mcpServerNames,
509
+ enableAllProjectMcpServers: true,
510
+ },
511
+ },
512
+ };
513
+ writeFileSync(seedPath, JSON.stringify(seed, null, 2) + "\n", { mode: 0o600 });
514
+ }
515
+ // settings.json: pre-suppress the "are you sure?" meta-prompt that
516
+ // `--dangerously-skip-permissions` shows on first use (the operator's own config
517
+ // sets this too). Without it, skip-permissions just trades one prompt for another.
518
+ const settingsPath = join(claudeDir, "settings.json");
519
+ if (!existsSync(settingsPath)) {
520
+ writeFileSync(
521
+ settingsPath,
522
+ JSON.stringify({ skipDangerousModePermissionPrompt: true }, null, 2) + "\n",
523
+ { mode: 0o600 },
524
+ );
525
+ }
526
+ // NOTE: we deliberately do NOT override HOME. claude resolves its own install
527
+ // relative to $HOME (`$HOME/.local/...`); leaving HOME as the operator's means
528
+ // claude finds its real install (no "setup issue", no per-spawn self-reinstall).
529
+ // All of claude's WRITES are redirected to the per-session dirs below
530
+ // (CLAUDE_CONFIG_DIR + XDG + temp), so it never EPERMs on the operator's
531
+ // read-only home and concurrent agents don't share mutable config.
532
+ return {
533
+ CLAUDE_CONFIG_DIR: claudeDir,
534
+ XDG_CONFIG_HOME: join(home, ".config"),
535
+ XDG_DATA_HOME: join(home, ".local", "share"),
536
+ XDG_CACHE_HOME: join(home, ".cache"),
537
+ XDG_STATE_HOME: join(home, ".local", "state"),
538
+ XDG_RUNTIME_DIR: join(home, ".run"),
539
+ // claude's `/tmp/claude-<uid>` scratch dir follows CLAUDE_CODE_TMPDIR; TMPDIR/
540
+ // TMP/TEMP cover everything else. All inside the writable workspace, so claude
541
+ // never EPERMs on temp (the "could not start" death) regardless of read scope.
542
+ TMPDIR: tmp,
543
+ CLAUDE_CODE_TMPDIR: tmp,
544
+ TMP: tmp,
545
+ TEMP: tmp,
546
+ // An ephemeral sandboxed agent shouldn't auto-update itself — it would download
547
+ // a fresh claude into the per-session data dir on every spawn (bandwidth + disk
548
+ // for nothing; the agent is gone when the session ends). This narrow flag
549
+ // disables ONLY the updater — unlike CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC,
550
+ // which also disables the channels feature we depend on.
551
+ DISABLE_AUTOUPDATER: "1",
552
+ };
553
+ }
554
+
555
+ /**
556
+ * Minimal POSIX shell-quote for joining argv into the single command string the
557
+ * sandbox engine wraps (`wrapWithSandboxArgv` takes a command string). Quotes any
558
+ * arg containing shell-significant chars; safe for the controlled argv we build
559
+ * (claude bin, flags, a workspace-local config path).
560
+ */
561
+ export function shellJoin(argv: string[]): string {
562
+ return argv.map(shellQuote).join(" ");
563
+ }
564
+
565
+ function shellQuote(arg: string): string {
566
+ if (arg.length > 0 && /^[A-Za-z0-9_@%+=:,./-]+$/.test(arg)) return arg;
567
+ // Single-quote, escaping embedded single quotes the POSIX way.
568
+ return `'${arg.replace(/'/g, `'\\''`)}'`;
569
+ }
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Tests for `resolveSpawnDeps` (`src/spawn-deps.ts`) — the real-dep resolver
3
+ * shared by the CLI and the web spawn endpoint.
4
+ *
5
+ * The load-bearing regression guard here is the claude config binding: the
6
+ * sandboxed `claude` MUST get `~/.claude.json` bound read-only, or it runs
7
+ * first-run onboarding whose connectivity check is FATAL under the restricted
8
+ * egress proxy and the tmux session dies instantly ("An unknown error occurred").
9
+ * That bug shipped once; this test ensures the binding stays.
10
+ */
11
+
12
+ import { describe, test, expect, afterEach } from "bun:test";
13
+ import { mkdtempSync, writeFileSync, rmSync } from "node:fs";
14
+ import { tmpdir, homedir } from "node:os";
15
+ import { join, resolve } from "node:path";
16
+ import { resolveSpawnDeps, SpawnDepsError } from "./spawn-deps.ts";
17
+
18
+ const savedHome = process.env.PARACHUTE_HOME;
19
+ let tmp: string | undefined;
20
+
21
+ afterEach(() => {
22
+ if (savedHome === undefined) delete process.env.PARACHUTE_HOME;
23
+ else process.env.PARACHUTE_HOME = savedHome;
24
+ if (tmp) {
25
+ try { rmSync(tmp, { recursive: true, force: true }); } catch {}
26
+ tmp = undefined;
27
+ }
28
+ });
29
+
30
+ describe("resolveSpawnDeps", () => {
31
+ test("throws SpawnDepsError when there's no operator token", () => {
32
+ tmp = mkdtempSync(join(tmpdir(), "spawn-deps-empty-"));
33
+ process.env.PARACHUTE_HOME = tmp; // no operator.token inside
34
+ expect(() => resolveSpawnDeps()).toThrow(SpawnDepsError);
35
+ });
36
+
37
+ test("binds the claude binary (confined reads) but NOT the operator's ~/.claude", () => {
38
+ tmp = mkdtempSync(join(tmpdir(), "spawn-deps-"));
39
+ process.env.PARACHUTE_HOME = tmp;
40
+ writeFileSync(join(tmp, "operator.token"), "fake-operator-bearer");
41
+ const deps = resolveSpawnDeps();
42
+ // The agent's config/onboarding now lives in its own per-session HOME
43
+ // (seedAgentHome), so we no longer expose the operator's real config.
44
+ expect(deps.runtimeReadOnly).not.toContain(resolve(homedir(), ".claude.json"));
45
+ expect(deps.runtimeReadOnly).not.toContain(resolve(homedir(), ".claude"));
46
+ // The claude BINARY is still bound (needed under confined/scoped reads) when
47
+ // resolvable on PATH — and claudeBin is set to its absolute path.
48
+ const bin = Bun.which("claude");
49
+ if (bin) {
50
+ expect(deps.claudeBin).toBe(bin);
51
+ expect(deps.runtimeReadOnly).toContain(bin);
52
+ }
53
+ });
54
+ });