@openparachute/agent 0.1.2 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.parachute/module.json +124 -8
- package/LICENSE +2 -16
- package/README.md +118 -166
- package/package.json +32 -43
- package/scripts/spawn-agent.ts +371 -0
- package/src/_parked/interactive-spawn.test.ts +324 -0
- package/src/_parked/interactive-spawn.ts +701 -0
- package/src/agent-defs.test.ts +1504 -0
- package/src/agent-defs.ts +1702 -0
- package/src/agent-mcp-config.test.ts +115 -0
- package/src/agent-mcp-config.ts +115 -0
- package/src/agents.test.ts +360 -0
- package/src/agents.ts +379 -0
- package/src/auth.test.ts +46 -0
- package/src/auth.ts +140 -0
- package/src/backends/attached-queue.test.ts +376 -0
- package/src/backends/attached-queue.ts +372 -0
- package/src/backends/programmatic.test.ts +1715 -0
- package/src/backends/programmatic.ts +927 -0
- package/src/backends/registry.test.ts +1494 -0
- package/src/backends/registry.ts +1202 -0
- package/src/backends/stream-json.test.ts +570 -0
- package/src/backends/stream-json.ts +392 -0
- package/src/backends/types.ts +223 -0
- package/src/bridge.ts +417 -0
- package/src/channel-backend-wiring.test.ts +237 -0
- package/src/credentials.test.ts +274 -0
- package/src/credentials.ts +380 -0
- package/src/cron.test.ts +342 -0
- package/src/cron.ts +380 -0
- package/src/daemon-agent-def-api.test.ts +166 -0
- package/src/daemon-agent-defs-api.test.ts +953 -0
- package/src/daemon-agent-env-api.test.ts +338 -0
- package/src/daemon-attached-queue-store.test.ts +65 -0
- package/src/daemon-config-api.test.ts +962 -0
- package/src/daemon-jobs-api.test.ts +271 -0
- package/src/daemon-vault-chat.test.ts +250 -0
- package/src/daemon.test.ts +746 -0
- package/src/daemon.ts +3314 -0
- package/src/def-vaults.test.ts +136 -0
- package/src/def-vaults.ts +165 -0
- package/src/delivery-state.test.ts +110 -0
- package/src/delivery-state.ts +154 -0
- package/src/effective-env.test.ts +114 -0
- package/src/effective-env.ts +184 -0
- package/src/env-compat.ts +39 -0
- package/src/grants.test.ts +638 -0
- package/src/grants.ts +675 -0
- package/src/hub-jwt.test.ts +161 -0
- package/src/hub-jwt.ts +182 -0
- package/src/jobs.test.ts +245 -0
- package/src/jobs.ts +266 -0
- package/src/mcp-http.test.ts +265 -0
- package/src/mcp-http.ts +771 -0
- package/src/mint-token.test.ts +152 -0
- package/src/mint-token.ts +139 -0
- package/src/module-manifest.test.ts +158 -0
- package/src/oauth-discovery.ts +134 -0
- package/src/programmatic-wiring.test.ts +838 -0
- package/src/registry.test.ts +227 -0
- package/src/registry.ts +228 -0
- package/src/resolve-port.test.ts +64 -0
- package/src/routing.test.ts +184 -0
- package/src/routing.ts +76 -0
- package/src/runner.test.ts +506 -0
- package/src/runner.ts +255 -0
- package/src/sandbox/config.test.ts +150 -0
- package/src/sandbox/config.ts +102 -0
- package/src/sandbox/egress.test.ts +113 -0
- package/src/sandbox/egress.ts +123 -0
- package/src/sandbox/index.ts +180 -0
- package/src/sandbox/live-seatbelt.test.ts +277 -0
- package/src/sandbox/mounts.test.ts +154 -0
- package/src/sandbox/mounts.ts +133 -0
- package/src/sandbox/sandbox.test.ts +168 -0
- package/src/sandbox/types.ts +382 -0
- package/src/services-manifest.test.ts +106 -0
- package/src/services-manifest.ts +95 -0
- package/src/spa-serve.test.ts +116 -0
- package/src/spa-serve.ts +116 -0
- package/src/spawn-agent-cli.test.ts +172 -0
- package/src/spawn-agent.test.ts +1218 -0
- package/src/spawn-agent.ts +569 -0
- package/src/spawn-deps.test.ts +54 -0
- package/src/spawn-deps.ts +166 -0
- package/src/telegram/api.ts +153 -0
- package/src/terminal-assets.test.ts +50 -0
- package/src/terminal-assets.ts +79 -0
- package/src/terminal-ui.ts +305 -0
- package/src/terminal.test.ts +530 -0
- package/src/terminal.ts +458 -0
- package/src/transport.ts +270 -0
- package/src/transports/http-ui.test.ts +455 -0
- package/src/transports/http-ui.ts +201 -0
- package/src/transports/telegram.test.ts +174 -0
- package/src/transports/telegram.ts +426 -0
- package/src/transports/vault.test.ts +2011 -0
- package/src/transports/vault.ts +1790 -0
- package/src/ui-kit.test.ts +178 -0
- package/src/ui-kit.ts +402 -0
- package/tsconfig.json +8 -14
- package/web/ui/tsconfig.json +2 -1
- package/.claude/scheduled_tasks.lock +0 -1
- package/.claude/settings.json +0 -5
- package/.claude/skills/add-atomic-chat-tool/SKILL.md +0 -243
- package/.claude/skills/add-atomic-chat-tool/atomic-chat-mcp-stdio.ts +0 -229
- package/.claude/skills/add-codex/SKILL.md +0 -161
- package/.claude/skills/add-dashboard/SKILL.md +0 -138
- package/.claude/skills/add-dashboard/resources/dashboard-pusher.ts +0 -495
- package/.claude/skills/add-emacs/SKILL.md +0 -296
- package/.claude/skills/add-gcal-tool/SKILL.md +0 -210
- package/.claude/skills/add-gchat/REMOVE.md +0 -6
- package/.claude/skills/add-gchat/SKILL.md +0 -92
- package/.claude/skills/add-gchat/VERIFY.md +0 -3
- package/.claude/skills/add-github/REMOVE.md +0 -6
- package/.claude/skills/add-github/SKILL.md +0 -148
- package/.claude/skills/add-github/VERIFY.md +0 -3
- package/.claude/skills/add-gmail-tool/SKILL.md +0 -229
- package/.claude/skills/add-imessage/REMOVE.md +0 -6
- package/.claude/skills/add-imessage/SKILL.md +0 -113
- package/.claude/skills/add-imessage/VERIFY.md +0 -3
- package/.claude/skills/add-karpathy-llm-wiki/SKILL.md +0 -110
- package/.claude/skills/add-karpathy-llm-wiki/llm-wiki.md +0 -75
- package/.claude/skills/add-linear/REMOVE.md +0 -6
- package/.claude/skills/add-linear/SKILL.md +0 -168
- package/.claude/skills/add-linear/VERIFY.md +0 -3
- package/.claude/skills/add-macos-statusbar/SKILL.md +0 -133
- package/.claude/skills/add-macos-statusbar/add/src/statusbar.swift +0 -147
- package/.claude/skills/add-matrix/REMOVE.md +0 -6
- package/.claude/skills/add-matrix/SKILL.md +0 -148
- package/.claude/skills/add-matrix/VERIFY.md +0 -3
- package/.claude/skills/add-ollama-provider/SKILL.md +0 -179
- package/.claude/skills/add-ollama-tool/SKILL.md +0 -193
- package/.claude/skills/add-opencode/SKILL.md +0 -229
- package/.claude/skills/add-parallel/SKILL.md +0 -290
- package/.claude/skills/add-resend/REMOVE.md +0 -6
- package/.claude/skills/add-resend/SKILL.md +0 -93
- package/.claude/skills/add-resend/VERIFY.md +0 -3
- package/.claude/skills/add-signal/REMOVE.md +0 -13
- package/.claude/skills/add-signal/SKILL.md +0 -318
- package/.claude/skills/add-signal/VERIFY.md +0 -5
- package/.claude/skills/add-slack/REMOVE.md +0 -6
- package/.claude/skills/add-slack/SKILL.md +0 -112
- package/.claude/skills/add-slack/VERIFY.md +0 -3
- package/.claude/skills/add-teams/REMOVE.md +0 -6
- package/.claude/skills/add-teams/SKILL.md +0 -207
- package/.claude/skills/add-teams/VERIFY.md +0 -3
- package/.claude/skills/add-vercel/SKILL.md +0 -147
- package/.claude/skills/add-vercel/container-skills/vercel-cli/SKILL.md +0 -103
- package/.claude/skills/add-webex/REMOVE.md +0 -6
- package/.claude/skills/add-webex/SKILL.md +0 -88
- package/.claude/skills/add-webex/VERIFY.md +0 -3
- package/.claude/skills/add-wechat/REMOVE.md +0 -49
- package/.claude/skills/add-wechat/SKILL.md +0 -170
- package/.claude/skills/add-wechat/scripts/wire-dm.ts +0 -172
- package/.claude/skills/add-whatsapp/SKILL.md +0 -264
- package/.claude/skills/add-whatsapp-cloud/REMOVE.md +0 -6
- package/.claude/skills/add-whatsapp-cloud/SKILL.md +0 -95
- package/.claude/skills/add-whatsapp-cloud/VERIFY.md +0 -3
- package/.claude/skills/claw/SKILL.md +0 -131
- package/.claude/skills/claw/scripts/claw +0 -374
- package/.claude/skills/convert-to-apple-container/SKILL.md +0 -212
- package/.claude/skills/customize/SKILL.md +0 -110
- package/.claude/skills/debug/SKILL.md +0 -349
- package/.claude/skills/get-qodo-rules/SKILL.md +0 -122
- package/.claude/skills/get-qodo-rules/references/output-format.md +0 -41
- package/.claude/skills/get-qodo-rules/references/pagination.md +0 -33
- package/.claude/skills/get-qodo-rules/references/repository-scope.md +0 -26
- package/.claude/skills/init-first-agent/SKILL.md +0 -120
- package/.claude/skills/init-onecli/SKILL.md +0 -270
- package/.claude/skills/manage-channels/SKILL.md +0 -87
- package/.claude/skills/manage-mounts/SKILL.md +0 -47
- package/.claude/skills/migrate-from-openclaw/MIGRATE_CRONS.md +0 -100
- package/.claude/skills/migrate-from-openclaw/SKILL.md +0 -447
- package/.claude/skills/migrate-from-openclaw/scripts/discover-openclaw.ts +0 -734
- package/.claude/skills/migrate-from-openclaw/scripts/extract-channel-credentials.ts +0 -476
- package/.claude/skills/migrate-nanoclaw/SKILL.md +0 -484
- package/.claude/skills/migrate-nanoclaw/diagnostics.md +0 -51
- package/.claude/skills/qodo-pr-resolver/SKILL.md +0 -326
- package/.claude/skills/qodo-pr-resolver/resources/providers.md +0 -329
- package/.claude/skills/update-nanoclaw/SKILL.md +0 -243
- package/.claude/skills/update-nanoclaw/diagnostics.md +0 -48
- package/.claude/skills/update-skills/SKILL.md +0 -130
- package/.claude/skills/use-native-credential-proxy/SKILL.md +0 -167
- package/.claude/skills/x-integration/SKILL.md +0 -417
- package/.claude/skills/x-integration/agent.ts +0 -243
- package/.claude/skills/x-integration/host.ts +0 -155
- package/.claude/skills/x-integration/lib/browser.ts +0 -148
- package/.claude/skills/x-integration/lib/config.ts +0 -62
- package/.claude/skills/x-integration/scripts/like.ts +0 -56
- package/.claude/skills/x-integration/scripts/post.ts +0 -66
- package/.claude/skills/x-integration/scripts/quote.ts +0 -80
- package/.claude/skills/x-integration/scripts/reply.ts +0 -74
- package/.claude/skills/x-integration/scripts/retweet.ts +0 -62
- package/.claude/skills/x-integration/scripts/setup.ts +0 -87
- package/.github/CODEOWNERS +0 -10
- package/.github/PULL_REQUEST_TEMPLATE.md +0 -18
- package/.github/workflows/bump-version.yml +0 -35
- package/.github/workflows/ci.yml +0 -39
- package/.github/workflows/label-pr.yml +0 -40
- package/.github/workflows/update-tokens.yml +0 -43
- package/.husky/pre-commit +0 -1
- package/.mcp.json +0 -3
- package/.nvmrc +0 -1
- package/.prettierrc +0 -4
- package/CHANGELOG.md +0 -263
- package/CLAUDE.md +0 -307
- package/CODE_OF_CONDUCT.md +0 -128
- package/CONTRIBUTING.md +0 -159
- package/CONTRIBUTORS.md +0 -26
- package/LICENSE-NANOCLAW-MIT +0 -21
- package/README_ja.md +0 -194
- package/README_zh.md +0 -194
- package/assets/nanoclaw-favicon.png +0 -0
- package/assets/nanoclaw-icon.png +0 -0
- package/assets/nanoclaw-logo-dark.png +0 -0
- package/assets/nanoclaw-logo.png +0 -0
- package/assets/nanoclaw-profile.jpeg +0 -0
- package/assets/nanoclaw-sales.png +0 -0
- package/assets/social-preview.jpg +0 -0
- package/config-examples/mount-allowlist.json +0 -25
- package/container/.dockerignore +0 -2
- package/container/CLAUDE.md +0 -21
- package/container/Dockerfile +0 -121
- package/container/agent-runner/bun.lock +0 -243
- package/container/agent-runner/package.json +0 -22
- package/container/agent-runner/scripts/sdk-signal-probe.ts +0 -169
- package/container/agent-runner/src/config.ts +0 -55
- package/container/agent-runner/src/db/connection.ts +0 -267
- package/container/agent-runner/src/db/index.ts +0 -20
- package/container/agent-runner/src/db/messages-in.ts +0 -138
- package/container/agent-runner/src/db/messages-out.ts +0 -143
- package/container/agent-runner/src/db/session-routing.ts +0 -30
- package/container/agent-runner/src/db/session-state.test.ts +0 -100
- package/container/agent-runner/src/db/session-state.ts +0 -79
- package/container/agent-runner/src/destinations.ts +0 -135
- package/container/agent-runner/src/formatter.test.ts +0 -167
- package/container/agent-runner/src/formatter.ts +0 -260
- package/container/agent-runner/src/index.ts +0 -110
- package/container/agent-runner/src/integration.test.ts +0 -121
- package/container/agent-runner/src/mcp-tools/agents.instructions.md +0 -26
- package/container/agent-runner/src/mcp-tools/agents.ts +0 -66
- package/container/agent-runner/src/mcp-tools/core.instructions.md +0 -27
- package/container/agent-runner/src/mcp-tools/core.ts +0 -262
- package/container/agent-runner/src/mcp-tools/index.ts +0 -22
- package/container/agent-runner/src/mcp-tools/interactive.instructions.md +0 -22
- package/container/agent-runner/src/mcp-tools/interactive.ts +0 -169
- package/container/agent-runner/src/mcp-tools/scheduling.instructions.md +0 -40
- package/container/agent-runner/src/mcp-tools/scheduling.ts +0 -299
- package/container/agent-runner/src/mcp-tools/self-mod.instructions.md +0 -25
- package/container/agent-runner/src/mcp-tools/self-mod.ts +0 -120
- package/container/agent-runner/src/mcp-tools/server.ts +0 -54
- package/container/agent-runner/src/mcp-tools/types.ts +0 -6
- package/container/agent-runner/src/poll-loop.test.ts +0 -248
- package/container/agent-runner/src/poll-loop.ts +0 -437
- package/container/agent-runner/src/providers/claude.ts +0 -379
- package/container/agent-runner/src/providers/factory.test.ts +0 -19
- package/container/agent-runner/src/providers/factory.ts +0 -13
- package/container/agent-runner/src/providers/index.ts +0 -6
- package/container/agent-runner/src/providers/mock.ts +0 -77
- package/container/agent-runner/src/providers/provider-registry.ts +0 -33
- package/container/agent-runner/src/providers/types.ts +0 -82
- package/container/agent-runner/src/scheduling/task-script.ts +0 -121
- package/container/agent-runner/src/timezone.test.ts +0 -93
- package/container/agent-runner/src/timezone.ts +0 -107
- package/container/agent-runner/tsconfig.json +0 -14
- package/container/build.sh +0 -48
- package/container/entrypoint.sh +0 -16
- package/container/skills/agent-browser/SKILL.md +0 -159
- package/container/skills/frontend-engineer/SKILL.md +0 -157
- package/container/skills/self-customize/SKILL.md +0 -87
- package/container/skills/slack-formatting/SKILL.md +0 -94
- package/container/skills/vercel-cli/SKILL.md +0 -111
- package/container/skills/welcome/SKILL.md +0 -85
- package/docs/APPLE-CONTAINER-NETWORKING.md +0 -90
- package/docs/BRANCH-FORK-MAINTENANCE.md +0 -81
- package/docs/README.md +0 -25
- package/docs/SDK_DEEP_DIVE.md +0 -643
- package/docs/SECURITY.md +0 -162
- package/docs/agent-runner-details.md +0 -749
- package/docs/api-details.md +0 -365
- package/docs/architecture-diagram.html +0 -422
- package/docs/architecture-diagram.md +0 -215
- package/docs/architecture.md +0 -751
- package/docs/audit/2026-04-30-channel-endpoint-audit.md +0 -36
- package/docs/build-and-runtime.md +0 -80
- package/docs/cross-mount-stress/README.md +0 -112
- package/docs/cross-mount-stress/container-writer-retry.mjs +0 -55
- package/docs/cross-mount-stress/container-writer-slow.mjs +0 -42
- package/docs/cross-mount-stress/container-writer.mjs +0 -47
- package/docs/cross-mount-stress/host-writer-retry.mjs +0 -55
- package/docs/cross-mount-stress/host-writer-slow.mjs +0 -43
- package/docs/cross-mount-stress/host-writer.mjs +0 -47
- package/docs/db-central.md +0 -316
- package/docs/db-session.md +0 -183
- package/docs/db.md +0 -119
- package/docs/design/2026-04-29-vault-management-ui.md +0 -231
- package/docs/design/2026-04-30-channel-wiring-rework.md +0 -234
- package/docs/design/2026-05-01-channel-wiring-approvals-deep-dive.md +0 -272
- package/docs/design/2026-05-02-channel-policy-and-approval-routing.md +0 -250
- package/docs/docker-sandboxes.md +0 -359
- package/docs/isolation-model.md +0 -88
- package/docs/ollama.md +0 -79
- package/docs/parachute-integration.md +0 -109
- package/docs/post-night-rebirth-reflections.md +0 -151
- package/eslint.config.js +0 -32
- package/pnpm-workspace.yaml +0 -8
- package/repo-tokens/README.md +0 -113
- package/repo-tokens/action.yml +0 -186
- package/repo-tokens/badge.svg +0 -23
- package/repo-tokens/examples/green.svg +0 -14
- package/repo-tokens/examples/red.svg +0 -14
- package/repo-tokens/examples/yellow-green.svg +0 -14
- package/repo-tokens/examples/yellow.svg +0 -14
- package/scripts/chat.ts +0 -101
- package/scripts/cleanup-sessions.sh +0 -150
- package/scripts/init-cli-agent.ts +0 -172
- package/scripts/init-first-agent.ts +0 -378
- package/scripts/parachute.ts +0 -158
- package/scripts/run-migrations.ts +0 -105
- package/scripts/sanity-live-poll.ts +0 -95
- package/scripts/seed-discord.ts +0 -80
- package/scripts/test-v2-agent.ts +0 -106
- package/scripts/test-v2-channel-e2e.ts +0 -265
- package/scripts/test-v2-host.ts +0 -184
- package/src/channels/adapter.ts +0 -214
- package/src/channels/api-translator.test.ts +0 -306
- package/src/channels/api-translator.ts +0 -214
- package/src/channels/ask-question.ts +0 -46
- package/src/channels/channel-registry.test.ts +0 -421
- package/src/channels/channel-registry.ts +0 -313
- package/src/channels/chat-sdk-bridge.test.ts +0 -84
- package/src/channels/chat-sdk-bridge.ts +0 -652
- package/src/channels/cli.ts +0 -276
- package/src/channels/discord.ts +0 -90
- package/src/channels/index.ts +0 -17
- package/src/channels/telegram-markdown-sanitize.test.ts +0 -78
- package/src/channels/telegram-markdown-sanitize.ts +0 -55
- package/src/channels/telegram-pairing.test.ts +0 -254
- package/src/channels/telegram-pairing.ts +0 -339
- package/src/channels/telegram.ts +0 -279
- package/src/channels/trust-hint.test.ts +0 -48
- package/src/channels/trust-hint.ts +0 -75
- package/src/claude-md-compose.migrate.test.ts +0 -64
- package/src/claude-md-compose.ts +0 -205
- package/src/command-gate.ts +0 -63
- package/src/config.test.ts +0 -93
- package/src/config.ts +0 -128
- package/src/container-config.ts +0 -167
- package/src/container-runner.test.ts +0 -32
- package/src/container-runner.ts +0 -576
- package/src/container-runtime.test.ts +0 -269
- package/src/container-runtime.ts +0 -167
- package/src/db/_bun-sqlite-shim.ts +0 -88
- package/src/db/agent-activity.test.ts +0 -155
- package/src/db/agent-activity.ts +0 -121
- package/src/db/agent-groups.ts +0 -77
- package/src/db/connection.migrate.test.ts +0 -176
- package/src/db/connection.ts +0 -259
- package/src/db/db-v2.test.ts +0 -440
- package/src/db/dropped-messages.ts +0 -44
- package/src/db/index.ts +0 -40
- package/src/db/messaging-groups.ts +0 -252
- package/src/db/migrations/001-initial.ts +0 -112
- package/src/db/migrations/002-chat-sdk-state.ts +0 -36
- package/src/db/migrations/008-dropped-messages.ts +0 -27
- package/src/db/migrations/009-drop-pending-credentials.ts +0 -13
- package/src/db/migrations/010-engage-modes.ts +0 -103
- package/src/db/migrations/011-pending-sender-approvals.ts +0 -40
- package/src/db/migrations/012-channel-registration.ts +0 -48
- package/src/db/migrations/013-approval-render-metadata.ts +0 -27
- package/src/db/migrations/014-secrets.ts +0 -44
- package/src/db/migrations/015-secrets-drop-host-pattern.ts +0 -18
- package/src/db/migrations/016-secret-assignments.ts +0 -30
- package/src/db/migrations/017-agent-activity.ts +0 -40
- package/src/db/migrations/018-oauth-app-configs.ts +0 -34
- package/src/db/migrations/019-oauth-app-connections.ts +0 -48
- package/src/db/migrations/020-agent-app-connections.ts +0 -28
- package/src/db/migrations/021-pending-oauth-states.ts +0 -35
- package/src/db/migrations/022-app-connections-provider.ts +0 -25
- package/src/db/migrations/023-agent-group-secret-mode.test.ts +0 -124
- package/src/db/migrations/023-agent-group-secret-mode.ts +0 -65
- package/src/db/migrations/024-collapse-approvals.test.ts +0 -249
- package/src/db/migrations/024-collapse-approvals.ts +0 -182
- package/src/db/migrations/025-secret-mode-check.test.ts +0 -155
- package/src/db/migrations/025-secret-mode-check.ts +0 -49
- package/src/db/migrations/026-user-dms-bot-id.test.ts +0 -116
- package/src/db/migrations/026-user-dms-bot-id.ts +0 -54
- package/src/db/migrations/027-provider-credentials.ts +0 -41
- package/src/db/migrations/_test-helpers.ts +0 -41
- package/src/db/migrations/index.ts +0 -127
- package/src/db/migrations/module-agent-to-agent-destinations.ts +0 -84
- package/src/db/migrations/module-approvals-pending-approvals.ts +0 -42
- package/src/db/migrations/module-approvals-title-options.ts +0 -40
- package/src/db/schema.ts +0 -258
- package/src/db/session-db.test.ts +0 -93
- package/src/db/session-db.ts +0 -325
- package/src/db/sessions.ts +0 -241
- package/src/delivery.test.ts +0 -148
- package/src/delivery.ts +0 -445
- package/src/env.ts +0 -74
- package/src/group-folder.test.ts +0 -35
- package/src/group-folder.ts +0 -44
- package/src/group-init.ts +0 -92
- package/src/host-core.test.ts +0 -456
- package/src/host-sweep.test.ts +0 -146
- package/src/host-sweep.ts +0 -287
- package/src/index.ts +0 -232
- package/src/install-slug.ts +0 -33
- package/src/log.test.ts +0 -81
- package/src/log.ts +0 -117
- package/src/mcp/http.ts +0 -72
- package/src/mcp/server.ts +0 -92
- package/src/mcp/stdio.ts +0 -51
- package/src/mcp/tools/activity.ts +0 -88
- package/src/mcp/tools/agent-groups.ts +0 -183
- package/src/mcp/tools/approvals.ts +0 -122
- package/src/mcp/tools/channels.test.ts +0 -126
- package/src/mcp/tools/channels.ts +0 -134
- package/src/mcp/tools/index.ts +0 -27
- package/src/mcp/tools/oauth.ts +0 -48
- package/src/mcp/tools/secrets.ts +0 -169
- package/src/mcp/tools/sessions.ts +0 -135
- package/src/mcp/types.ts +0 -51
- package/src/modules/agent-to-agent/agent-route.test.ts +0 -46
- package/src/modules/agent-to-agent/agent-route.ts +0 -223
- package/src/modules/agent-to-agent/create-agent.ts +0 -127
- package/src/modules/agent-to-agent/db/agent-destinations.ts +0 -135
- package/src/modules/agent-to-agent/index.ts +0 -22
- package/src/modules/agent-to-agent/write-destinations.ts +0 -59
- package/src/modules/approvals/agent.md +0 -45
- package/src/modules/approvals/index.ts +0 -21
- package/src/modules/approvals/picks.test.ts +0 -291
- package/src/modules/approvals/primitive.ts +0 -279
- package/src/modules/approvals/project.md +0 -27
- package/src/modules/approvals/response-handler.ts +0 -87
- package/src/modules/index.ts +0 -24
- package/src/modules/interactive/agent.md +0 -21
- package/src/modules/interactive/index.ts +0 -69
- package/src/modules/interactive/project.md +0 -12
- package/src/modules/mount-security/expand-path.test.ts +0 -82
- package/src/modules/mount-security/index.ts +0 -459
- package/src/modules/mount-security/migrate.test.ts +0 -91
- package/src/modules/permissions/access.ts +0 -28
- package/src/modules/permissions/channel-approval.test.ts +0 -389
- package/src/modules/permissions/channel-approval.ts +0 -188
- package/src/modules/permissions/db/agent-group-members.ts +0 -44
- package/src/modules/permissions/db/pending-channel-approvals.test.ts +0 -86
- package/src/modules/permissions/db/pending-channel-approvals.ts +0 -66
- package/src/modules/permissions/db/pending-sender-approvals.ts +0 -60
- package/src/modules/permissions/db/user-dms.ts +0 -58
- package/src/modules/permissions/db/user-roles.ts +0 -85
- package/src/modules/permissions/db/users.ts +0 -38
- package/src/modules/permissions/index.ts +0 -421
- package/src/modules/permissions/permissions.test.ts +0 -358
- package/src/modules/permissions/sender-approval.test.ts +0 -641
- package/src/modules/permissions/sender-approval.ts +0 -165
- package/src/modules/permissions/user-dm.ts +0 -200
- package/src/modules/provider-credentials/db.ts +0 -121
- package/src/modules/provider-credentials/index.ts +0 -12
- package/src/modules/provider-credentials/spawn.test.ts +0 -206
- package/src/modules/provider-credentials/spawn.ts +0 -114
- package/src/modules/scheduling/actions.ts +0 -113
- package/src/modules/scheduling/db.test.ts +0 -282
- package/src/modules/scheduling/db.ts +0 -148
- package/src/modules/scheduling/index.ts +0 -34
- package/src/modules/scheduling/recurrence.test.ts +0 -98
- package/src/modules/scheduling/recurrence.ts +0 -54
- package/src/modules/self-mod/agent.md +0 -30
- package/src/modules/self-mod/apply.ts +0 -85
- package/src/modules/self-mod/index.ts +0 -30
- package/src/modules/self-mod/project.md +0 -39
- package/src/modules/self-mod/request.ts +0 -91
- package/src/modules/typing/index.ts +0 -165
- package/src/oauth/agent-app-connections.ts +0 -103
- package/src/oauth/app-configs.test.ts +0 -64
- package/src/oauth/app-configs.ts +0 -114
- package/src/oauth/app-connections.test.ts +0 -109
- package/src/oauth/app-connections.ts +0 -178
- package/src/oauth/crypto.ts +0 -56
- package/src/oauth/flow.ts +0 -104
- package/src/oauth/providers/google.test.ts +0 -38
- package/src/oauth/providers/google.ts +0 -46
- package/src/oauth/providers/index.ts +0 -48
- package/src/oauth/state-store.test.ts +0 -54
- package/src/oauth/state-store.ts +0 -93
- package/src/parachute/README.md +0 -27
- package/src/parachute/create-agent.test.ts +0 -83
- package/src/parachute/create-agent.ts +0 -122
- package/src/parachute/group-status.test.ts +0 -165
- package/src/parachute/group-status.ts +0 -136
- package/src/parachute/types.ts +0 -41
- package/src/parachute/vault-mcp.test.ts +0 -251
- package/src/parachute/vault-mcp.ts +0 -232
- package/src/platform-id.test.ts +0 -104
- package/src/platform-id.ts +0 -109
- package/src/providers/index.ts +0 -6
- package/src/providers/provider-container-registry.ts +0 -58
- package/src/response-registry.ts +0 -45
- package/src/router.ts +0 -530
- package/src/secrets/crypto.test.ts +0 -45
- package/src/secrets/crypto.ts +0 -55
- package/src/secrets/index.ts +0 -461
- package/src/secrets/master-key.ts +0 -70
- package/src/secrets/secrets.test.ts +0 -651
- package/src/session-manager.attachments.test.ts +0 -171
- package/src/session-manager.dup-skip.test.ts +0 -173
- package/src/session-manager.migrate.test.ts +0 -59
- package/src/session-manager.ts +0 -451
- package/src/startup-bootstrap.test.ts +0 -226
- package/src/startup-bootstrap.ts +0 -207
- package/src/state-sqlite.ts +0 -182
- package/src/timezone.test.ts +0 -64
- package/src/timezone.ts +0 -37
- package/src/types.ts +0 -233
- package/src/web/auth.test.ts +0 -335
- package/src/web/auth.ts +0 -214
- package/src/web/discord-validate.test.ts +0 -77
- package/src/web/discord-validate.ts +0 -88
- package/src/web/hub-discovery.test.ts +0 -98
- package/src/web/hub-discovery.ts +0 -69
- package/src/web/routes/activity.ts +0 -106
- package/src/web/routes/agent-provider.test.ts +0 -282
- package/src/web/routes/agent-provider.ts +0 -309
- package/src/web/routes/approvals.ts +0 -185
- package/src/web/routes/apps.ts +0 -434
- package/src/web/routes/channels-mg-detail.test.ts +0 -324
- package/src/web/routes/channels-mga-detail.test.ts +0 -472
- package/src/web/routes/channels.ts +0 -311
- package/src/web/routes/oauth-providers.ts +0 -42
- package/src/web/routes/secrets.test.ts +0 -220
- package/src/web/routes/secrets.ts +0 -317
- package/src/web/routes/sessions.ts +0 -123
- package/src/web/routes/settings.test.ts +0 -106
- package/src/web/routes/settings.ts +0 -247
- package/src/web/routes/setup-status.ts +0 -205
- package/src/web/routes/vaults.test.ts +0 -389
- package/src/web/routes/vaults.ts +0 -225
- package/src/web/server-version.test.ts +0 -16
- package/src/web/server.ts +0 -1024
- package/src/web/services-manifest.test.ts +0 -148
- package/src/web/services-manifest.ts +0 -66
- package/src/web/static-serve.test.ts +0 -255
- package/src/web/static-serve.ts +0 -104
- package/src/web/telegram-validate.test.ts +0 -116
- package/src/web/telegram-validate.ts +0 -107
- package/src/web/vault-proxy.test.ts +0 -214
- package/src/web/vault-proxy.ts +0 -120
- package/src/web/wire-channel.ts +0 -181
- package/src/webhook-server.ts +0 -134
- package/vitest.config.ts +0 -18
- package/web/README.md +0 -63
- package/web/ui/index.html +0 -13
- package/web/ui/package.json +0 -35
- package/web/ui/pnpm-lock.yaml +0 -2164
- package/web/ui/scripts/verify-base.mjs +0 -31
- package/web/ui/src/App.tsx +0 -88
- package/web/ui/src/components/ActivityFeed.tsx +0 -444
- package/web/ui/src/components/AgentGroupPicker.tsx +0 -263
- package/web/ui/src/components/AgentProviderCards.tsx +0 -220
- package/web/ui/src/components/CredentialForm.tsx +0 -214
- package/web/ui/src/components/ScopeGrants.tsx +0 -74
- package/web/ui/src/components/StatusDot.tsx +0 -43
- package/web/ui/src/components/VaultPicker.tsx +0 -127
- package/web/ui/src/components/setup/AdapterInstallStep.tsx +0 -178
- package/web/ui/src/components/setup/AgentGroupStep.tsx +0 -43
- package/web/ui/src/components/setup/ChannelPickStep.tsx +0 -74
- package/web/ui/src/components/setup/DoneStep.tsx +0 -49
- package/web/ui/src/components/setup/PrereqStep.tsx +0 -129
- package/web/ui/src/components/setup/TestConnectionStep.tsx +0 -108
- package/web/ui/src/components/setup/TestMessageStep.tsx +0 -104
- package/web/ui/src/components/setup/WireChannelStep.tsx +0 -166
- package/web/ui/src/components/setup/types.ts +0 -105
- package/web/ui/src/lib/api.test.ts +0 -410
- package/web/ui/src/lib/api.ts +0 -1248
- package/web/ui/src/lib/auth.test.ts +0 -352
- package/web/ui/src/lib/auth.ts +0 -405
- package/web/ui/src/lib/channel-adapters.ts +0 -136
- package/web/ui/src/main.tsx +0 -19
- package/web/ui/src/routes/ApprovalsList.tsx +0 -294
- package/web/ui/src/routes/Apps.tsx +0 -613
- package/web/ui/src/routes/ChannelWireDetail.test.tsx +0 -233
- package/web/ui/src/routes/ChannelWireDetail.tsx +0 -403
- package/web/ui/src/routes/ChannelsList.tsx +0 -158
- package/web/ui/src/routes/GroupDetail.test.tsx +0 -206
- package/web/ui/src/routes/GroupDetail.tsx +0 -880
- package/web/ui/src/routes/GroupList.tsx +0 -187
- package/web/ui/src/routes/MessagingGroupDetail.test.tsx +0 -233
- package/web/ui/src/routes/MessagingGroupDetail.tsx +0 -306
- package/web/ui/src/routes/NewGroupWizard.tsx +0 -390
- package/web/ui/src/routes/OAuthCallback.tsx +0 -56
- package/web/ui/src/routes/SecretsList.tsx +0 -942
- package/web/ui/src/routes/SessionsList.tsx +0 -220
- package/web/ui/src/routes/SettingsAgentProvider.tsx +0 -109
- package/web/ui/src/routes/SettingsApprovals.tsx +0 -234
- package/web/ui/src/routes/SetupWizard.tsx +0 -219
- package/web/ui/src/routes/VaultDetail.test.tsx +0 -363
- package/web/ui/src/routes/VaultDetail.tsx +0 -960
- package/web/ui/src/routes/VaultsList.tsx +0 -295
- package/web/ui/src/routes/WireChannelPage.tsx +0 -413
- package/web/ui/src/styles.css +0 -608
- package/web/ui/src/test/setup.ts +0 -23
- package/web/ui/src/vite-env.d.ts +0 -10
- package/web/ui/vite.config.ts +0 -34
- 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
|
+
});
|