@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.
- package/.parachute/module.json +124 -8
- package/LICENSE +2 -16
- package/README.md +118 -166
- package/package.json +35 -42
- 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/dist/assets/index-C-iWdFFV.css +1 -0
- package/web/ui/dist/assets/index-VFETBk0a.js +60 -0
- package/web/ui/dist/index.html +15 -0
- 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,274 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-channel Claude OAuth credential store (design §6).
|
|
3
|
+
*
|
|
4
|
+
* Covers: store/retrieve round-trip, 0600 on the secret file, redaction (the
|
|
5
|
+
* raw token never appears in the inspection helper / serialized output), and
|
|
6
|
+
* default-vs-override resolution (override wins, falls back to default, errors
|
|
7
|
+
* when neither). All hermetic under a throwaway state dir.
|
|
8
|
+
*/
|
|
9
|
+
import { describe, test, expect, beforeEach, afterEach } from "bun:test";
|
|
10
|
+
import { mkdtempSync, rmSync, existsSync, readFileSync, statSync } from "fs";
|
|
11
|
+
import { join } from "path";
|
|
12
|
+
import { tmpdir } from "os";
|
|
13
|
+
import {
|
|
14
|
+
setDefaultClaudeCredential,
|
|
15
|
+
setChannelClaudeCredential,
|
|
16
|
+
removeChannelClaudeCredential,
|
|
17
|
+
resolveClaudeCredential,
|
|
18
|
+
describeClaudeCredentials,
|
|
19
|
+
readCredentialsFile,
|
|
20
|
+
credentialsFilePath,
|
|
21
|
+
CredentialNotConfiguredError,
|
|
22
|
+
setChannelEnvVar,
|
|
23
|
+
removeChannelEnvVar,
|
|
24
|
+
resolveChannelEnv,
|
|
25
|
+
describeChannelEnv,
|
|
26
|
+
DenylistedEnvError,
|
|
27
|
+
DENYLISTED_ENV,
|
|
28
|
+
} from "./credentials.ts";
|
|
29
|
+
|
|
30
|
+
const DEFAULT_TOKEN = "oat_DEFAULT-OPERATOR-TOKEN-SECRET";
|
|
31
|
+
const OVERRIDE_TOKEN = "oat_PER-CHANNEL-OVERRIDE-SECRET";
|
|
32
|
+
|
|
33
|
+
let dir: string;
|
|
34
|
+
beforeEach(() => {
|
|
35
|
+
dir = mkdtempSync(join(tmpdir(), "channel-creds-"));
|
|
36
|
+
});
|
|
37
|
+
afterEach(() => {
|
|
38
|
+
rmSync(dir, { recursive: true, force: true });
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
describe("store / retrieve round-trip", () => {
|
|
42
|
+
test("default token: set then resolve returns it", () => {
|
|
43
|
+
setDefaultClaudeCredential(DEFAULT_TOKEN, dir);
|
|
44
|
+
expect(resolveClaudeCredential("any-channel", dir)).toBe(DEFAULT_TOKEN);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test("per-channel override: set then resolve returns it for that channel", () => {
|
|
48
|
+
setChannelClaudeCredential("aaron-dev", OVERRIDE_TOKEN, dir);
|
|
49
|
+
expect(resolveClaudeCredential("aaron-dev", dir)).toBe(OVERRIDE_TOKEN);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test("setting one slice preserves the other (read-modify-write)", () => {
|
|
53
|
+
setDefaultClaudeCredential(DEFAULT_TOKEN, dir);
|
|
54
|
+
setChannelClaudeCredential("aaron-dev", OVERRIDE_TOKEN, dir);
|
|
55
|
+
setChannelClaudeCredential("ops", "oat_OPS", dir);
|
|
56
|
+
const file = readCredentialsFile(dir);
|
|
57
|
+
expect(file.claude!.default).toBe(DEFAULT_TOKEN);
|
|
58
|
+
expect(file.claude!.channels!["aaron-dev"]).toBe(OVERRIDE_TOKEN);
|
|
59
|
+
expect(file.claude!.channels!["ops"]).toBe("oat_OPS");
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test("empty token is rejected (never persists a blank credential)", () => {
|
|
63
|
+
expect(() => setDefaultClaudeCredential("", dir)).toThrow(/non-empty token/);
|
|
64
|
+
expect(() => setChannelClaudeCredential("c", "", dir)).toThrow(/non-empty token/);
|
|
65
|
+
expect(existsSync(credentialsFilePath(dir))).toBe(false);
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
describe("0600 on the secret file", () => {
|
|
70
|
+
test("the credentials file is written 0600 (holds a secret)", () => {
|
|
71
|
+
setDefaultClaudeCredential(DEFAULT_TOKEN, dir);
|
|
72
|
+
const file = credentialsFilePath(dir);
|
|
73
|
+
expect(existsSync(file)).toBe(true);
|
|
74
|
+
expect(statSync(file).mode & 0o777).toBe(0o600);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test("a subsequent write keeps it 0600 (chmod is unconditional)", () => {
|
|
78
|
+
setDefaultClaudeCredential(DEFAULT_TOKEN, dir);
|
|
79
|
+
// Loosen perms behind the store's back, then write again → must re-tighten.
|
|
80
|
+
const fs = require("fs") as typeof import("fs");
|
|
81
|
+
fs.chmodSync(credentialsFilePath(dir), 0o644);
|
|
82
|
+
setChannelClaudeCredential("c", OVERRIDE_TOKEN, dir);
|
|
83
|
+
expect(statSync(credentialsFilePath(dir)).mode & 0o777).toBe(0o600);
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
describe("redaction — the raw token never leaks via the inspection helper", () => {
|
|
88
|
+
test("describeClaudeCredentials reports presence + channel names, NOT the token", () => {
|
|
89
|
+
setDefaultClaudeCredential(DEFAULT_TOKEN, dir);
|
|
90
|
+
setChannelClaudeCredential("aaron-dev", OVERRIDE_TOKEN, dir);
|
|
91
|
+
setChannelClaudeCredential("ops", "oat_OPS", dir);
|
|
92
|
+
const desc = describeClaudeCredentials(dir);
|
|
93
|
+
expect(desc.defaultSet).toBe(true);
|
|
94
|
+
expect(desc.channels).toEqual(["aaron-dev", "ops"]); // sorted, names only
|
|
95
|
+
const serialized = JSON.stringify(desc);
|
|
96
|
+
expect(serialized).not.toContain(DEFAULT_TOKEN);
|
|
97
|
+
expect(serialized).not.toContain(OVERRIDE_TOKEN);
|
|
98
|
+
expect(serialized).not.toContain("oat_OPS");
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
test("describe on an empty store: defaultSet false, no channels", () => {
|
|
102
|
+
const desc = describeClaudeCredentials(dir);
|
|
103
|
+
expect(desc).toEqual({ defaultSet: false, channels: [] });
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
describe("default-vs-override resolution", () => {
|
|
108
|
+
test("override WINS over the default for its channel", () => {
|
|
109
|
+
setDefaultClaudeCredential(DEFAULT_TOKEN, dir);
|
|
110
|
+
setChannelClaudeCredential("aaron-dev", OVERRIDE_TOKEN, dir);
|
|
111
|
+
expect(resolveClaudeCredential("aaron-dev", dir)).toBe(OVERRIDE_TOKEN);
|
|
112
|
+
// A different channel with no override falls back to the default.
|
|
113
|
+
expect(resolveClaudeCredential("other", dir)).toBe(DEFAULT_TOKEN);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
test("falls back to the default when the channel has no override", () => {
|
|
117
|
+
setDefaultClaudeCredential(DEFAULT_TOKEN, dir);
|
|
118
|
+
expect(resolveClaudeCredential("never-configured", dir)).toBe(DEFAULT_TOKEN);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
test("ERRORS when neither an override nor a default is set", () => {
|
|
122
|
+
expect(() => resolveClaudeCredential("ghost", dir)).toThrow(CredentialNotConfiguredError);
|
|
123
|
+
expect(() => resolveClaudeCredential("ghost", dir)).toThrow(/no Claude credential for channel "ghost"/);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
test("removing an override falls back to the default; removing a missing one is a no-op", () => {
|
|
127
|
+
setDefaultClaudeCredential(DEFAULT_TOKEN, dir);
|
|
128
|
+
setChannelClaudeCredential("aaron-dev", OVERRIDE_TOKEN, dir);
|
|
129
|
+
expect(removeChannelClaudeCredential("aaron-dev", dir)).toBe(true);
|
|
130
|
+
expect(resolveClaudeCredential("aaron-dev", dir)).toBe(DEFAULT_TOKEN); // back to default
|
|
131
|
+
expect(removeChannelClaudeCredential("aaron-dev", dir)).toBe(false); // already gone
|
|
132
|
+
// The default is untouched by an override removal.
|
|
133
|
+
expect(readCredentialsFile(dir).claude!.default).toBe(DEFAULT_TOKEN);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
test("resolution is read dynamically — a rotate takes effect on the next resolve", () => {
|
|
137
|
+
setDefaultClaudeCredential(DEFAULT_TOKEN, dir);
|
|
138
|
+
expect(resolveClaudeCredential("c", dir)).toBe(DEFAULT_TOKEN);
|
|
139
|
+
setDefaultClaudeCredential("oat_ROTATED", dir);
|
|
140
|
+
expect(resolveClaudeCredential("c", dir)).toBe("oat_ROTATED");
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
// ===========================================================================
|
|
145
|
+
// Generic per-channel env store (GH_TOKEN / CLOUDFLARE_API_TOKEN / …)
|
|
146
|
+
// ===========================================================================
|
|
147
|
+
const GH = "ghp_GITHUB-TOKEN-SECRET";
|
|
148
|
+
const CF = "cf_CLOUDFLARE-TOKEN-SECRET";
|
|
149
|
+
|
|
150
|
+
describe("env store — set / resolve / channel-over-default merge", () => {
|
|
151
|
+
test("default var: set with null channel, resolves for any channel", () => {
|
|
152
|
+
setChannelEnvVar(null, "GH_TOKEN", GH, dir);
|
|
153
|
+
expect(resolveChannelEnv("anything", dir)).toEqual({ GH_TOKEN: GH });
|
|
154
|
+
// An empty-string channel also targets the default layer.
|
|
155
|
+
setChannelEnvVar("", "CF_TOKEN", CF, dir);
|
|
156
|
+
expect(resolveChannelEnv("anything", dir)).toEqual({ GH_TOKEN: GH, CF_TOKEN: CF });
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
test("per-channel override WINS over the default for that channel; others see only the default", () => {
|
|
160
|
+
setChannelEnvVar(null, "GH_TOKEN", "ghp_DEFAULT", dir);
|
|
161
|
+
setChannelEnvVar("aaron-dev", "GH_TOKEN", "ghp_AARON", dir);
|
|
162
|
+
setChannelEnvVar("aaron-dev", "CLOUDFLARE_API_TOKEN", CF, dir);
|
|
163
|
+
// channel layer wins on GH_TOKEN, plus its own CF token, plus inherits nothing extra.
|
|
164
|
+
expect(resolveChannelEnv("aaron-dev", dir)).toEqual({ GH_TOKEN: "ghp_AARON", CLOUDFLARE_API_TOKEN: CF });
|
|
165
|
+
// a different channel falls back to the default only.
|
|
166
|
+
expect(resolveChannelEnv("other", dir)).toEqual({ GH_TOKEN: "ghp_DEFAULT" });
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
test("resolves to {} when nothing is configured (env injection is optional)", () => {
|
|
170
|
+
expect(resolveChannelEnv("ghost", dir)).toEqual({});
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
test("setting an env var preserves the Claude slice (independent namespaces)", () => {
|
|
174
|
+
setDefaultClaudeCredential(DEFAULT_TOKEN, dir);
|
|
175
|
+
setChannelEnvVar(null, "GH_TOKEN", GH, dir);
|
|
176
|
+
const file = readCredentialsFile(dir);
|
|
177
|
+
expect(file.claude!.default).toBe(DEFAULT_TOKEN); // untouched
|
|
178
|
+
expect(file.env!.default!.GH_TOKEN).toBe(GH);
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
test("read dynamically — a value change takes effect on the next resolve", () => {
|
|
182
|
+
setChannelEnvVar("c", "GH_TOKEN", "ghp_OLD", dir);
|
|
183
|
+
expect(resolveChannelEnv("c", dir).GH_TOKEN).toBe("ghp_OLD");
|
|
184
|
+
setChannelEnvVar("c", "GH_TOKEN", "ghp_NEW", dir);
|
|
185
|
+
expect(resolveChannelEnv("c", dir).GH_TOKEN).toBe("ghp_NEW");
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
test("the env store file is written 0600 (holds secrets)", () => {
|
|
189
|
+
setChannelEnvVar(null, "GH_TOKEN", GH, dir);
|
|
190
|
+
expect(statSync(credentialsFilePath(dir)).mode & 0o777).toBe(0o600);
|
|
191
|
+
});
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
describe("env store — remove", () => {
|
|
195
|
+
test("remove a default var; remove a missing one is a no-op (false)", () => {
|
|
196
|
+
setChannelEnvVar(null, "GH_TOKEN", GH, dir);
|
|
197
|
+
setChannelEnvVar(null, "CF_TOKEN", CF, dir);
|
|
198
|
+
expect(removeChannelEnvVar(null, "GH_TOKEN", dir)).toBe(true);
|
|
199
|
+
expect(resolveChannelEnv("any", dir)).toEqual({ CF_TOKEN: CF });
|
|
200
|
+
expect(removeChannelEnvVar(null, "GH_TOKEN", dir)).toBe(false); // already gone
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
test("remove a channel override; the default for that name re-emerges", () => {
|
|
204
|
+
setChannelEnvVar(null, "GH_TOKEN", "ghp_DEFAULT", dir);
|
|
205
|
+
setChannelEnvVar("aaron-dev", "GH_TOKEN", "ghp_AARON", dir);
|
|
206
|
+
expect(removeChannelEnvVar("aaron-dev", "GH_TOKEN", dir)).toBe(true);
|
|
207
|
+
expect(resolveChannelEnv("aaron-dev", dir)).toEqual({ GH_TOKEN: "ghp_DEFAULT" }); // back to default
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
test("removing the last var of a channel prunes the empty channel map", () => {
|
|
211
|
+
setChannelEnvVar("c", "GH_TOKEN", GH, dir);
|
|
212
|
+
removeChannelEnvVar("c", "GH_TOKEN", dir);
|
|
213
|
+
const file = readCredentialsFile(dir);
|
|
214
|
+
// The channel (and the now-empty channels map) is pruned, not left as {}.
|
|
215
|
+
expect(file.env?.channels).toBeUndefined();
|
|
216
|
+
});
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
describe("env store — redaction (describeChannelEnv returns NAMES only)", () => {
|
|
220
|
+
test("describe reports names per layer, never the values", () => {
|
|
221
|
+
setChannelEnvVar(null, "GH_TOKEN", GH, dir);
|
|
222
|
+
setChannelEnvVar("aaron-dev", "CLOUDFLARE_API_TOKEN", CF, dir);
|
|
223
|
+
setChannelEnvVar("aaron-dev", "GH_TOKEN", "ghp_AARON", dir);
|
|
224
|
+
const desc = describeChannelEnv(dir);
|
|
225
|
+
expect(desc.default).toEqual(["GH_TOKEN"]);
|
|
226
|
+
expect(desc.channels["aaron-dev"]).toEqual(["CLOUDFLARE_API_TOKEN", "GH_TOKEN"]); // sorted
|
|
227
|
+
const serialized = JSON.stringify(desc);
|
|
228
|
+
expect(serialized).not.toContain(GH);
|
|
229
|
+
expect(serialized).not.toContain(CF);
|
|
230
|
+
expect(serialized).not.toContain("ghp_AARON");
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
test("describe on an empty store: no default, no channels", () => {
|
|
234
|
+
expect(describeChannelEnv(dir)).toEqual({ default: [], channels: {} });
|
|
235
|
+
});
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
describe("env store — denylist (the Claude-auth trio is never settable)", () => {
|
|
239
|
+
test("the denylist is exactly the Claude-auth vars", () => {
|
|
240
|
+
expect([...DENYLISTED_ENV].sort()).toEqual(
|
|
241
|
+
["ANTHROPIC_API_KEY", "CLAUDE_API_KEY", "CLAUDE_CODE_OAUTH_TOKEN"].sort(),
|
|
242
|
+
);
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
test("setter REJECTS each denylisted name (default + channel), nothing persisted", () => {
|
|
246
|
+
for (const name of DENYLISTED_ENV) {
|
|
247
|
+
expect(() => setChannelEnvVar(null, name, "x", dir)).toThrow(DenylistedEnvError);
|
|
248
|
+
expect(() => setChannelEnvVar("c", name, "x", dir)).toThrow(DenylistedEnvError);
|
|
249
|
+
}
|
|
250
|
+
expect(existsSync(credentialsFilePath(dir))).toBe(false);
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
test("setter rejects a malformed name + an empty value", () => {
|
|
254
|
+
expect(() => setChannelEnvVar(null, "9BAD", "x", dir)).toThrow(/invalid/);
|
|
255
|
+
expect(() => setChannelEnvVar(null, "has space", "x", dir)).toThrow(/invalid/);
|
|
256
|
+
expect(() => setChannelEnvVar(null, "GH_TOKEN", "", dir)).toThrow(/non-empty/);
|
|
257
|
+
expect(existsSync(credentialsFilePath(dir))).toBe(false);
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
test("resolve defensively STRIPS a denylisted key planted by a hand-edited file", () => {
|
|
261
|
+
// Plant a denylisted key directly on disk (bypassing the setter), then prove
|
|
262
|
+
// resolveChannelEnv never returns it — the injection defense's first line.
|
|
263
|
+
setChannelEnvVar(null, "GH_TOKEN", GH, dir);
|
|
264
|
+
const fs = require("fs") as typeof import("fs");
|
|
265
|
+
const file = JSON.parse(fs.readFileSync(credentialsFilePath(dir), "utf8")) as {
|
|
266
|
+
env: { default: Record<string, string> };
|
|
267
|
+
};
|
|
268
|
+
file.env.default.ANTHROPIC_API_KEY = "sk-ant-SMUGGLED";
|
|
269
|
+
fs.writeFileSync(credentialsFilePath(dir), JSON.stringify(file));
|
|
270
|
+
const resolved = resolveChannelEnv("any", dir);
|
|
271
|
+
expect(resolved.GH_TOKEN).toBe(GH);
|
|
272
|
+
expect(resolved.ANTHROPIC_API_KEY).toBeUndefined();
|
|
273
|
+
});
|
|
274
|
+
});
|
|
@@ -0,0 +1,380 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-channel Claude OAuth credential store (design §6).
|
|
3
|
+
*
|
|
4
|
+
* The Claude `CLAUDE_CODE_OAUTH_TOKEN` (from `claude setup-token`, the documented
|
|
5
|
+
* 1-year headless/CI auth path) is the credential a launched agent session runs
|
|
6
|
+
* on — injected into the sandbox at launch as the session's auth (NEVER
|
|
7
|
+
* `ANTHROPIC_API_KEY`, which would silently route onto API billing; see
|
|
8
|
+
* `spawn-agent.ts`). This module persists that secret, following the SAME
|
|
9
|
+
* file-store discipline `registry.ts` uses for per-channel transport tokens:
|
|
10
|
+
* a read-modify-write JSON file, written 0600 and `chmod`-ed 0600 unconditionally
|
|
11
|
+
* (so an existing file created under a looser umask is tightened on every write).
|
|
12
|
+
*
|
|
13
|
+
* Two principal levels (design §6 — "default one operator token; per-channel
|
|
14
|
+
* override"):
|
|
15
|
+
*
|
|
16
|
+
* - a **default / operator-level** token, used when a channel has no override,
|
|
17
|
+
* - a **per-channel override**, the multi-principal seam (multi-user isn't a
|
|
18
|
+
* rewrite — just populating per-channel, eventually per-principal, tokens).
|
|
19
|
+
*
|
|
20
|
+
* Resolution (`resolveClaudeCredential`): channel override ?? default ?? error.
|
|
21
|
+
*
|
|
22
|
+
* The secret lives in its OWN file (`credentials.json`), separate from
|
|
23
|
+
* `channels.json`: the default/operator token isn't tied to any single channel,
|
|
24
|
+
* and the credential lifecycle (set the operator token once, override per
|
|
25
|
+
* channel) is distinct from the channel-registry lifecycle. The file is
|
|
26
|
+
* NAMESPACED by credential type (`{ claude: { ... } }`) so a future credential
|
|
27
|
+
* type can coexist without a schema migration.
|
|
28
|
+
*
|
|
29
|
+
* Redaction discipline: the raw token is NEVER returned by the listing/inspection
|
|
30
|
+
* helper (`describeClaudeCredentials`) and NEVER logged — exactly the posture the
|
|
31
|
+
* config API + transports already keep for `config.token` / `webhookSecret`.
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
import { readFileSync, writeFileSync, existsSync, chmodSync, mkdirSync } from "fs";
|
|
35
|
+
import { join } from "path";
|
|
36
|
+
import { defaultStateDir } from "./registry.ts";
|
|
37
|
+
|
|
38
|
+
/** The Claude-credential slice of the store. */
|
|
39
|
+
export interface ClaudeCredentialStore {
|
|
40
|
+
/** Default / operator-level OAuth token, used when a channel has no override. */
|
|
41
|
+
default?: string;
|
|
42
|
+
/** Per-channel overrides, keyed by channel name. */
|
|
43
|
+
channels?: Record<string, string>;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* The generic per-channel ENVIRONMENT-VARIABLE slice (`env`). Same two-principal
|
|
48
|
+
* shape as the Claude slice (an operator-level default layer + per-channel
|
|
49
|
+
* overrides), but each layer is a NAME→VALUE map rather than a single token: an
|
|
50
|
+
* operator scopes a channel's spawned agent a `GH_TOKEN`, `CLOUDFLARE_API_TOKEN`,
|
|
51
|
+
* etc. {@link resolveChannelEnv} flattens the two layers into one map (channel
|
|
52
|
+
* wins) at spawn time, and {@link buildAgentChildEnv} (spawn-agent.ts) merges that
|
|
53
|
+
* into the sandboxed child's env so the agent's `gh`/`git`/build tooling sees the
|
|
54
|
+
* tokens — while Claude's own auth (`CLAUDE_CODE_OAUTH_TOKEN`) stays untouched.
|
|
55
|
+
*/
|
|
56
|
+
export interface ChannelEnvStore {
|
|
57
|
+
/** Operator-level default env vars, used by every channel (lowest precedence). */
|
|
58
|
+
default?: Record<string, string>;
|
|
59
|
+
/** Per-channel env overrides, keyed by channel name (wins over the default). */
|
|
60
|
+
channels?: Record<string, Record<string, string>>;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** The on-disk `credentials.json` shape (namespaced by credential type). */
|
|
64
|
+
export interface CredentialsFile {
|
|
65
|
+
claude?: ClaudeCredentialStore;
|
|
66
|
+
/** Generic per-channel env-var injection (the GH_TOKEN/CLOUDFLARE_* slice). */
|
|
67
|
+
env?: ChannelEnvStore;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Env-var names that MUST NEVER be settable through the env store — they'd break
|
|
72
|
+
* the module's two load-bearing guarantees:
|
|
73
|
+
*
|
|
74
|
+
* - `ANTHROPIC_API_KEY` / `CLAUDE_API_KEY` would route the spawned session onto
|
|
75
|
+
* METERED API billing instead of the interactive subscription (the exact thing
|
|
76
|
+
* `buildAgentChildEnv` deliberately scrubs — see spawn-agent.ts §6).
|
|
77
|
+
* - `CLAUDE_CODE_OAUTH_TOKEN` is the session's MANAGED auth, resolved per-channel
|
|
78
|
+
* from the Claude slice; letting the generic env store override it would let an
|
|
79
|
+
* operator (or a future less-trusted caller) silently swap the session's
|
|
80
|
+
* identity out from under the credential resolver.
|
|
81
|
+
*
|
|
82
|
+
* The setters REJECT these (throw {@link DenylistedEnvError}); the injection step
|
|
83
|
+
* (`buildAgentChildEnv`) ALSO drops them defensively, so even a hand-edited
|
|
84
|
+
* credentials.json can't smuggle one through.
|
|
85
|
+
*
|
|
86
|
+
* `PATH`/`HOME` are deliberately NOT denylisted: `buildAgentChildEnv` layers the
|
|
87
|
+
* resolved channel env UNDER its own structural passthrough + the seeded-HOME
|
|
88
|
+
* overrides, so a channel-set PATH/HOME can't clobber the sandbox's own (the
|
|
89
|
+
* passthrough copies the real PATH/HOME after, and seedAgentHome's CLAUDE_CONFIG_DIR
|
|
90
|
+
* /XDG/TMP win last). Rejecting them would only deny a harmless no-op; allowing
|
|
91
|
+
* them keeps the denylist focused on the keys that actually matter (the Claude-auth
|
|
92
|
+
* trio). See the layering comment in `buildAgentChildEnv`.
|
|
93
|
+
*/
|
|
94
|
+
export const DENYLISTED_ENV: ReadonlySet<string> = new Set([
|
|
95
|
+
"ANTHROPIC_API_KEY",
|
|
96
|
+
"CLAUDE_API_KEY",
|
|
97
|
+
"CLAUDE_CODE_OAUTH_TOKEN",
|
|
98
|
+
]);
|
|
99
|
+
|
|
100
|
+
/** A basic POSIX-ish env-var name guard (letters/digits/underscore, no leading digit). */
|
|
101
|
+
const ENV_NAME_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
102
|
+
|
|
103
|
+
/** Thrown when a setter is asked to set/override a denylisted env-var name. */
|
|
104
|
+
export class DenylistedEnvError extends Error {
|
|
105
|
+
constructor(name: string) {
|
|
106
|
+
super(
|
|
107
|
+
`env var "${name}" is not settable here: it controls Claude auth / billing ` +
|
|
108
|
+
`(ANTHROPIC_API_KEY, CLAUDE_API_KEY, CLAUDE_CODE_OAUTH_TOKEN are reserved by ` +
|
|
109
|
+
`the managed subscription-billing path). Set the Claude credential via ` +
|
|
110
|
+
`POST /api/credentials/claude instead.`,
|
|
111
|
+
);
|
|
112
|
+
this.name = "DenylistedEnvError";
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** The default credential reference an unspecified spec resolves against. */
|
|
117
|
+
export const DEFAULT_CREDENTIAL_REF = "operator" as const;
|
|
118
|
+
|
|
119
|
+
/** Absolute path to the credentials.json store in a state dir. */
|
|
120
|
+
export function credentialsFilePath(stateDir?: string): string {
|
|
121
|
+
return join(stateDir ?? defaultStateDir(), "credentials.json");
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Read `credentials.json` as a plain `CredentialsFile`. Returns an empty `{}` if
|
|
126
|
+
* the file is absent. Mirrors `registry.readChannelsFile` — the read half of the
|
|
127
|
+
* read-modify-write the setters use.
|
|
128
|
+
*/
|
|
129
|
+
export function readCredentialsFile(stateDir?: string): CredentialsFile {
|
|
130
|
+
const file = credentialsFilePath(stateDir);
|
|
131
|
+
if (!existsSync(file)) return {};
|
|
132
|
+
const parsed = JSON.parse(readFileSync(file, "utf8")) as CredentialsFile;
|
|
133
|
+
if (!parsed || typeof parsed !== "object") {
|
|
134
|
+
throw new Error(`credentials: ${file} must be a JSON object`);
|
|
135
|
+
}
|
|
136
|
+
return parsed;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Persist the store back to `credentials.json` with 0600 perms — the file holds
|
|
141
|
+
* the Claude OAuth secret. Creates the state dir if needed. `chmod`s 0600
|
|
142
|
+
* unconditionally (writeFileSync's `mode` only applies on CREATE, so an existing
|
|
143
|
+
* file created under a looser umask is tightened on every write) — the exact
|
|
144
|
+
* discipline `registry.upsertChannelEntry` keeps for the secret-bearing
|
|
145
|
+
* channels.json.
|
|
146
|
+
*/
|
|
147
|
+
function writeCredentialsFile(file: CredentialsFile, stateDir?: string): void {
|
|
148
|
+
const dir = stateDir ?? defaultStateDir();
|
|
149
|
+
mkdirSync(dir, { recursive: true });
|
|
150
|
+
const path = credentialsFilePath(dir);
|
|
151
|
+
writeFileSync(path, JSON.stringify(file, null, 2) + "\n", { mode: 0o600 });
|
|
152
|
+
chmodSync(path, 0o600);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Set the default / operator-level Claude OAuth token. Used by any channel that
|
|
157
|
+
* has no per-channel override. Read-modify-write so existing per-channel
|
|
158
|
+
* overrides are preserved.
|
|
159
|
+
*/
|
|
160
|
+
export function setDefaultClaudeCredential(token: string, stateDir?: string): void {
|
|
161
|
+
if (typeof token !== "string" || token.length === 0) {
|
|
162
|
+
throw new Error("credentials: a non-empty token is required");
|
|
163
|
+
}
|
|
164
|
+
const file = readCredentialsFile(stateDir);
|
|
165
|
+
const claude = file.claude ?? {};
|
|
166
|
+
claude.default = token;
|
|
167
|
+
file.claude = claude;
|
|
168
|
+
writeCredentialsFile(file, stateDir);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Set a per-channel Claude OAuth override. Wins over the default for that channel.
|
|
173
|
+
* Read-modify-write so the default + other channels' overrides are preserved.
|
|
174
|
+
*/
|
|
175
|
+
export function setChannelClaudeCredential(
|
|
176
|
+
channel: string,
|
|
177
|
+
token: string,
|
|
178
|
+
stateDir?: string,
|
|
179
|
+
): void {
|
|
180
|
+
if (typeof channel !== "string" || channel.length === 0) {
|
|
181
|
+
throw new Error("credentials: a channel name is required");
|
|
182
|
+
}
|
|
183
|
+
if (typeof token !== "string" || token.length === 0) {
|
|
184
|
+
throw new Error("credentials: a non-empty token is required");
|
|
185
|
+
}
|
|
186
|
+
const file = readCredentialsFile(stateDir);
|
|
187
|
+
const claude = file.claude ?? {};
|
|
188
|
+
const channels = claude.channels ?? {};
|
|
189
|
+
channels[channel] = token;
|
|
190
|
+
claude.channels = channels;
|
|
191
|
+
file.claude = claude;
|
|
192
|
+
writeCredentialsFile(file, stateDir);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Remove a per-channel override (the channel falls back to the default after
|
|
197
|
+
* this). Returns true if an override existed, false if there was nothing to
|
|
198
|
+
* remove. The default token is untouched.
|
|
199
|
+
*/
|
|
200
|
+
export function removeChannelClaudeCredential(channel: string, stateDir?: string): boolean {
|
|
201
|
+
const file = readCredentialsFile(stateDir);
|
|
202
|
+
const channels = file.claude?.channels;
|
|
203
|
+
if (!channels || !(channel in channels)) return false;
|
|
204
|
+
delete channels[channel];
|
|
205
|
+
writeCredentialsFile(file, stateDir);
|
|
206
|
+
return true;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/** Thrown when neither a per-channel override nor a default token is configured. */
|
|
210
|
+
export class CredentialNotConfiguredError extends Error {
|
|
211
|
+
constructor(channel: string) {
|
|
212
|
+
super(
|
|
213
|
+
`no Claude credential for channel "${channel}": set a per-channel override or the ` +
|
|
214
|
+
`default/operator token (POST /api/credentials/claude). Get one with ` +
|
|
215
|
+
`\`claude setup-token\`.`,
|
|
216
|
+
);
|
|
217
|
+
this.name = "CredentialNotConfiguredError";
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Resolve the Claude OAuth token a session on `channel` should run on:
|
|
223
|
+
*
|
|
224
|
+
* channel override ?? default ?? throw CredentialNotConfiguredError
|
|
225
|
+
*
|
|
226
|
+
* Read at resolve time (not cached) so a token set/rotated via the config API
|
|
227
|
+
* takes effect on the next spawn without a daemon restart — the dynamic-read
|
|
228
|
+
* discipline. Throwing (rather than returning empty) means a misconfigured
|
|
229
|
+
* install fails loud BEFORE a session launches with no auth.
|
|
230
|
+
*/
|
|
231
|
+
export function resolveClaudeCredential(channel: string, stateDir?: string): string {
|
|
232
|
+
const claude = readCredentialsFile(stateDir).claude;
|
|
233
|
+
const override = claude?.channels?.[channel];
|
|
234
|
+
if (override) return override;
|
|
235
|
+
const fallback = claude?.default;
|
|
236
|
+
if (fallback) return fallback;
|
|
237
|
+
throw new CredentialNotConfiguredError(channel);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Describe the credential store for an operator-facing read WITHOUT leaking the
|
|
242
|
+
* secret: whether a default is set, and which channels carry an override (names
|
|
243
|
+
* only). The raw token is never returned — same redaction posture the config
|
|
244
|
+
* API keeps for transport tokens. (`GET /api/credentials/claude`.)
|
|
245
|
+
*/
|
|
246
|
+
export function describeClaudeCredentials(
|
|
247
|
+
stateDir?: string,
|
|
248
|
+
): { defaultSet: boolean; channels: string[] } {
|
|
249
|
+
const claude = readCredentialsFile(stateDir).claude;
|
|
250
|
+
return {
|
|
251
|
+
defaultSet: Boolean(claude?.default),
|
|
252
|
+
channels: Object.keys(claude?.channels ?? {}).sort(),
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// ---------------------------------------------------------------------------
|
|
257
|
+
// Generic per-channel env-var store (the GH_TOKEN / CLOUDFLARE_API_TOKEN slice).
|
|
258
|
+
//
|
|
259
|
+
// Mirrors the Claude helpers exactly: read-modify-write JSON, 0600 + unconditional
|
|
260
|
+
// chmod, sibling-preserving, dynamic-read-at-resolve. A `null`/`undefined` channel
|
|
261
|
+
// targets the operator-level DEFAULT layer; a channel name targets that channel's
|
|
262
|
+
// override layer. Every setter enforces DENYLISTED_ENV (the Claude-auth trio) so
|
|
263
|
+
// the subscription-billing guarantee can't be subverted via this surface.
|
|
264
|
+
// ---------------------------------------------------------------------------
|
|
265
|
+
|
|
266
|
+
/** Validate an env-var NAME for the setters: non-denylisted + a sane shape. */
|
|
267
|
+
function assertSettableEnvName(name: string): void {
|
|
268
|
+
if (typeof name !== "string" || name.length === 0) {
|
|
269
|
+
throw new Error("credentials: an env var name is required");
|
|
270
|
+
}
|
|
271
|
+
if (DENYLISTED_ENV.has(name)) throw new DenylistedEnvError(name);
|
|
272
|
+
if (!ENV_NAME_RE.test(name)) {
|
|
273
|
+
throw new Error(
|
|
274
|
+
`credentials: env var name "${name}" is invalid (letters, digits, underscore; no leading digit)`,
|
|
275
|
+
);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Set ONE env var on the operator-level default layer (`channel` is null/undefined)
|
|
281
|
+
* or on a specific channel's override layer. Read-modify-write so the Claude slice,
|
|
282
|
+
* the other layer, and other vars are preserved. Rejects denylisted names
|
|
283
|
+
* ({@link DenylistedEnvError}) and an empty value.
|
|
284
|
+
*/
|
|
285
|
+
export function setChannelEnvVar(
|
|
286
|
+
channel: string | null | undefined,
|
|
287
|
+
name: string,
|
|
288
|
+
value: string,
|
|
289
|
+
stateDir?: string,
|
|
290
|
+
): void {
|
|
291
|
+
assertSettableEnvName(name);
|
|
292
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
293
|
+
throw new Error("credentials: a non-empty env var value is required");
|
|
294
|
+
}
|
|
295
|
+
const file = readCredentialsFile(stateDir);
|
|
296
|
+
const env = file.env ?? {};
|
|
297
|
+
if (channel === null || channel === undefined || channel === "") {
|
|
298
|
+
const def = env.default ?? {};
|
|
299
|
+
def[name] = value;
|
|
300
|
+
env.default = def;
|
|
301
|
+
} else {
|
|
302
|
+
const channels = env.channels ?? {};
|
|
303
|
+
const forChannel = channels[channel] ?? {};
|
|
304
|
+
forChannel[name] = value;
|
|
305
|
+
channels[channel] = forChannel;
|
|
306
|
+
env.channels = channels;
|
|
307
|
+
}
|
|
308
|
+
file.env = env;
|
|
309
|
+
writeCredentialsFile(file, stateDir);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* Remove ONE env var from the operator-level default layer (`channel` null/undefined)
|
|
314
|
+
* or a channel's override layer. Returns true if it existed, false if there was
|
|
315
|
+
* nothing to remove. Prunes an emptied channel map so a removed-everything channel
|
|
316
|
+
* doesn't linger as `{}`. Read-modify-write; the Claude slice + other vars untouched.
|
|
317
|
+
*/
|
|
318
|
+
export function removeChannelEnvVar(
|
|
319
|
+
channel: string | null | undefined,
|
|
320
|
+
name: string,
|
|
321
|
+
stateDir?: string,
|
|
322
|
+
): boolean {
|
|
323
|
+
const file = readCredentialsFile(stateDir);
|
|
324
|
+
const env = file.env;
|
|
325
|
+
if (!env) return false;
|
|
326
|
+
if (channel === null || channel === undefined || channel === "") {
|
|
327
|
+
if (!env.default || !(name in env.default)) return false;
|
|
328
|
+
delete env.default[name];
|
|
329
|
+
if (Object.keys(env.default).length === 0) delete env.default;
|
|
330
|
+
} else {
|
|
331
|
+
const forChannel = env.channels?.[channel];
|
|
332
|
+
if (!forChannel || !(name in forChannel)) return false;
|
|
333
|
+
delete forChannel[name];
|
|
334
|
+
if (Object.keys(forChannel).length === 0) delete env.channels![channel];
|
|
335
|
+
if (env.channels && Object.keys(env.channels).length === 0) delete env.channels;
|
|
336
|
+
}
|
|
337
|
+
writeCredentialsFile(file, stateDir);
|
|
338
|
+
return true;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* Resolve the FLATTENED env a session on `channel` should run with:
|
|
343
|
+
*
|
|
344
|
+
* { ...env.default, ...env.channels[channel] } (the channel layer wins)
|
|
345
|
+
*
|
|
346
|
+
* Read at resolve time (not cached), like the Claude resolver — so a var set via the
|
|
347
|
+
* config API takes effect on the next spawn (or per-session restart) without a daemon
|
|
348
|
+
* restart. Defensively SKIPS any denylisted key that somehow landed on disk (a
|
|
349
|
+
* hand-edited file): the setter blocks them, but the resolver never returns one
|
|
350
|
+
* either, so `buildAgentChildEnv`'s own denylist drop is a belt to this suspenders.
|
|
351
|
+
* Returns an empty map when nothing is configured (a channel with no env is fine).
|
|
352
|
+
*/
|
|
353
|
+
export function resolveChannelEnv(channel: string, stateDir?: string): Record<string, string> {
|
|
354
|
+
const env = readCredentialsFile(stateDir).env;
|
|
355
|
+
const merged: Record<string, string> = { ...(env?.default ?? {}), ...(env?.channels?.[channel] ?? {}) };
|
|
356
|
+
for (const k of Object.keys(merged)) {
|
|
357
|
+
if (DENYLISTED_ENV.has(k)) delete merged[k];
|
|
358
|
+
}
|
|
359
|
+
return merged;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
/**
|
|
363
|
+
* Describe the env store for an operator-facing read WITHOUT leaking values: the
|
|
364
|
+
* NAMES set on the default layer, and the names set per channel. The raw values are
|
|
365
|
+
* NEVER returned (`GET /api/credentials/env`) — same redaction posture as
|
|
366
|
+
* `describeClaudeCredentials`.
|
|
367
|
+
*/
|
|
368
|
+
export function describeChannelEnv(
|
|
369
|
+
stateDir?: string,
|
|
370
|
+
): { default: string[]; channels: Record<string, string[]> } {
|
|
371
|
+
const env = readCredentialsFile(stateDir).env;
|
|
372
|
+
const channels: Record<string, string[]> = {};
|
|
373
|
+
for (const [ch, vars] of Object.entries(env?.channels ?? {})) {
|
|
374
|
+
channels[ch] = Object.keys(vars).sort();
|
|
375
|
+
}
|
|
376
|
+
return {
|
|
377
|
+
default: Object.keys(env?.default ?? {}).sort(),
|
|
378
|
+
channels,
|
|
379
|
+
};
|
|
380
|
+
}
|