@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,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
+ });