@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
package/src/grants.ts
ADDED
|
@@ -0,0 +1,675 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agent connectors — approval-gated cross-resource GRANTS (design
|
|
3
|
+
* 2026-06-17-agent-connectors-4b.md, slice 4b-1).
|
|
4
|
+
*
|
|
5
|
+
* 4a gave an agent its OWN def-vault. 4b lets a `#agent/definition` note DECLARE
|
|
6
|
+
* what it wants to reach BEYOND that — other local vaults, external services
|
|
7
|
+
* (GitHub, Cloudflare), and (parsed-but-deferred to 4b-2) remote MCP/OAuth servers.
|
|
8
|
+
* Every extra reach is OPERATOR-APPROVED in the hub; every secret stays in the hub's
|
|
9
|
+
* grant store; the agent module is the CONSUMER that fetches approved material at
|
|
10
|
+
* spawn + injects it into the ephemeral per-spawn `.mcp.json` + env.
|
|
11
|
+
*
|
|
12
|
+
* THE ONE INVARIANT (design §"The one invariant"): a vault note can only REQUEST, it
|
|
13
|
+
* can never GRANT. This module:
|
|
14
|
+
* - parses the note's `wants:` into structured {@link ConnectionSpec}s ({@link parseWants});
|
|
15
|
+
* - REGISTERS each as a PENDING grant with the hub (`PUT /admin/grants`) — that is
|
|
16
|
+
* the request, not a grant; worst case it sits `pending`;
|
|
17
|
+
* - at spawn, fetches only APPROVED grants' MATERIAL (`GET …/material`) and injects
|
|
18
|
+
* it. Unapproved/pending/error connections are simply ABSENT — never a failure.
|
|
19
|
+
*
|
|
20
|
+
* THE WIRE CONTRACT (parachute-hub PR #668 + #96 — consume, do not redesign):
|
|
21
|
+
* - PUT <hub>/admin/grants { agent, connection } → { id, agent, connection, status, reason? }
|
|
22
|
+
* - GET <hub>/admin/grants?agent=<> → { grants: [{ id, agent, connection, status, reason?, approvedAt? }] }
|
|
23
|
+
* - GET <hub>/admin/grants/<id>/material → APPROVED only:
|
|
24
|
+
* vault → { kind:"vault", token, mcpUrl }
|
|
25
|
+
* service → { kind:"service", token, inject }
|
|
26
|
+
* (404 unknown id / 409 not-approved)
|
|
27
|
+
* - POST <hub>/admin/grants/reconcile { agent, liveConnections } → { pruned, prunedIds } (#96
|
|
28
|
+
* grant-GC): the hub re-derives each key with ITS connectionKey and tears down +
|
|
29
|
+
* REMOVEs every grant for `agent` whose key is NOT among the live specs (empty
|
|
30
|
+
* liveConnections = the def is gone → prune ALL). Stops a removed
|
|
31
|
+
* want / a deleted def from orphaning a live approved grant. Pruning only ever
|
|
32
|
+
* REMOVES access, so it shares the host-admin Bearer (never an operator cookie).
|
|
33
|
+
* - Auth: all of these need a `parachute:host:admin` Bearer — we REUSE the module's
|
|
34
|
+
* existing host-admin-capable MANAGER BEARER (the operator token it mints vault
|
|
35
|
+
* tokens with; see mint-token.ts / spawn-deps.ts). NO new auth path.
|
|
36
|
+
* - approve/revoke are operator-only via the hub UI — the module NEVER calls those.
|
|
37
|
+
*
|
|
38
|
+
* SECURITY: grant material is SECRET (tokens). It only ever lands in the ephemeral,
|
|
39
|
+
* 0600 per-spawn `.mcp.json` + the child env — NEVER in a vault note. Material is
|
|
40
|
+
* fetched FRESH each spawn (design: revocation takes effect on the next spawn), so we
|
|
41
|
+
* deliberately do NOT cache it.
|
|
42
|
+
*/
|
|
43
|
+
|
|
44
|
+
import { DENYLISTED_ENV } from "./credentials.ts";
|
|
45
|
+
|
|
46
|
+
// ---------------------------------------------------------------------------
|
|
47
|
+
// Connection spec — the structured form of one `wants:` entry
|
|
48
|
+
// ---------------------------------------------------------------------------
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* A declared connection beyond the def-vault. Matches the hub's `connection` spec
|
|
52
|
+
* shape exactly (design §"The hub grants API", connection spec):
|
|
53
|
+
* `{ kind, target, access?, tags?, inject? }`
|
|
54
|
+
* — `access`/`tags` are vault-only; `inject` is service-only (`("env"|"mcp")[]`).
|
|
55
|
+
*/
|
|
56
|
+
export interface ConnectionSpec {
|
|
57
|
+
/** Resource kind. `vault`/`service` are wired in 4b-1; `mcp` is parsed-but-deferred. */
|
|
58
|
+
kind: "vault" | "service" | "mcp";
|
|
59
|
+
/**
|
|
60
|
+
* The resource target — a vault name (`research`), a service name (`github`),
|
|
61
|
+
* or, for `kind:"mcp"`, the remote MCP https URL.
|
|
62
|
+
*/
|
|
63
|
+
target: string;
|
|
64
|
+
/** Vault access verb. Vault-only. */
|
|
65
|
+
access?: "read" | "write";
|
|
66
|
+
/** Vault tag-scope (one or more `#tag`). Vault-only. */
|
|
67
|
+
tags?: string[];
|
|
68
|
+
/** Injection shape(s) for a service credential. Service-only. */
|
|
69
|
+
inject?: ("env" | "mcp")[];
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** A malformed `wants:` entry — the whole def is an error (no half-instantiate). */
|
|
73
|
+
export class WantsParseError extends Error {
|
|
74
|
+
constructor(message: string) {
|
|
75
|
+
super(message);
|
|
76
|
+
this.name = "WantsParseError";
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* A STABLE, canonical key for a connection — the (agent, connection) grant key the
|
|
82
|
+
* status `pending:[…]` list reports + the hub upserts on. Derived purely from the
|
|
83
|
+
* spec so a re-parse of the same `wants:` yields the same key (idempotent upsert).
|
|
84
|
+
*
|
|
85
|
+
* vault → `vault:<target>:<access>[#tag…]` (tags sorted for stability)
|
|
86
|
+
* service → `<inject-joined>:<target>` e.g. `env+mcp:github`
|
|
87
|
+
* mcp → `mcp:<url>`
|
|
88
|
+
*/
|
|
89
|
+
export function connectionKey(c: ConnectionSpec): string {
|
|
90
|
+
if (c.kind === "vault") {
|
|
91
|
+
const tags = c.tags && c.tags.length > 0 ? [...c.tags].sort().join("") : "";
|
|
92
|
+
return `vault:${c.target}:${c.access ?? "read"}${tags}`;
|
|
93
|
+
}
|
|
94
|
+
if (c.kind === "service") {
|
|
95
|
+
const inject = (c.inject && c.inject.length > 0 ? [...c.inject].sort() : ["env"]).join("+");
|
|
96
|
+
return `${inject}:${c.target}`;
|
|
97
|
+
}
|
|
98
|
+
return `mcp:${c.target}`;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** A vault name slug — the `<name>` segment in `vault:<name>:<verb>`. */
|
|
102
|
+
const VAULT_NAME_SLUG = /^[a-zA-Z0-9_-]+$/;
|
|
103
|
+
/** A service name slug — `github`, `cloudflare`, … */
|
|
104
|
+
const SERVICE_NAME_SLUG = /^[a-zA-Z0-9_-]+$/;
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Parse the `wants:` metadata field — a comma-separated list of connection specs —
|
|
108
|
+
* into structured {@link ConnectionSpec}s. PURE, no I/O.
|
|
109
|
+
*
|
|
110
|
+
* Spec forms (design §"The connection declaration"):
|
|
111
|
+
* - `vault:<name>:<read|write>` with optional `#tag` suffix(es):
|
|
112
|
+
* `vault:research:read` → {kind:"vault", target:"research", access:"read"}
|
|
113
|
+
* `vault:research:read#published#wip`→ {…, tags:["#published","#wip"]}
|
|
114
|
+
* (the agent's OWN def-vault is implicit — never in `wants:`; the def-vault binding
|
|
115
|
+
* drives `spec.vault`, not this.)
|
|
116
|
+
* - `env:<service>` → {kind:"service", target:"<service>", inject:["env"]}
|
|
117
|
+
* `mcp:<service>` → {kind:"service", target:"<service>", inject:["mcp"]}
|
|
118
|
+
* BOTH for the same service MERGE → inject:["env","mcp"].
|
|
119
|
+
* - `mcp:<https-url>`→ {kind:"mcp", target:"<url>"} (parsed; deferred to 4b-2).
|
|
120
|
+
*
|
|
121
|
+
* Disambiguation of `mcp:<x>`: an `<x>` that starts with `http://` or `https://` is
|
|
122
|
+
* a remote MCP (`kind:"mcp"`); otherwise it's a service MCP-injection (`mcp:github`).
|
|
123
|
+
*
|
|
124
|
+
* Accepts a real array OR the comma/space-joined string the vault stringifies arrays
|
|
125
|
+
* into. A MALFORMED entry throws {@link WantsParseError} — the caller stamps the def
|
|
126
|
+
* `status:error` rather than half-instantiating (design §1 "a malformed `wants:` →
|
|
127
|
+
* the def is an error").
|
|
128
|
+
*/
|
|
129
|
+
export function parseWants(raw: unknown): ConnectionSpec[] {
|
|
130
|
+
const entries = toEntries(raw);
|
|
131
|
+
if (entries.length === 0) return [];
|
|
132
|
+
|
|
133
|
+
// Service connections accumulate by target so `env:github` + `mcp:github` merge to
|
|
134
|
+
// one connection with inject:["env","mcp"] (design §1). Keyed by service target.
|
|
135
|
+
const services = new Map<string, Set<"env" | "mcp">>();
|
|
136
|
+
// Insertion order of services (Map preserves it, but we re-emit at the end so the
|
|
137
|
+
// overall ordering = first-seen across all kinds).
|
|
138
|
+
const out: ConnectionSpec[] = [];
|
|
139
|
+
// Placeholder index per service so the merged service lands at its first position.
|
|
140
|
+
const servicePos = new Map<string, number>();
|
|
141
|
+
|
|
142
|
+
for (const entry of entries) {
|
|
143
|
+
const spec = parseOneWant(entry);
|
|
144
|
+
if (spec.kind === "service") {
|
|
145
|
+
const modes = services.get(spec.target);
|
|
146
|
+
if (modes) {
|
|
147
|
+
for (const m of spec.inject ?? []) modes.add(m);
|
|
148
|
+
continue; // already placeheld at first position
|
|
149
|
+
}
|
|
150
|
+
const set = new Set<"env" | "mcp">(spec.inject ?? []);
|
|
151
|
+
services.set(spec.target, set);
|
|
152
|
+
servicePos.set(spec.target, out.length);
|
|
153
|
+
out.push(spec); // placeholder; finalized below with the merged inject
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
out.push(spec);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Finalize each service connection's merged inject (stable order: env before mcp).
|
|
160
|
+
for (const [target, modes] of services) {
|
|
161
|
+
const pos = servicePos.get(target)!;
|
|
162
|
+
const inject = (["env", "mcp"] as const).filter((m) => modes.has(m));
|
|
163
|
+
out[pos] = { kind: "service", target, inject };
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return out;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/** Coerce a `wants:` metadata value (array or comma/space string) → clean entries. */
|
|
170
|
+
function toEntries(raw: unknown): string[] {
|
|
171
|
+
let parts: string[] = [];
|
|
172
|
+
if (Array.isArray(raw)) {
|
|
173
|
+
parts = raw.map((x) => (typeof x === "string" ? x : String(x)));
|
|
174
|
+
} else if (typeof raw === "string") {
|
|
175
|
+
parts = raw.split(/[,\s]+/);
|
|
176
|
+
} else if (raw === undefined || raw === null) {
|
|
177
|
+
return [];
|
|
178
|
+
} else {
|
|
179
|
+
parts = [String(raw)];
|
|
180
|
+
}
|
|
181
|
+
return parts.map((s) => s.trim()).filter((s) => s.length > 0);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/** Parse ONE `wants:` entry string. Throws {@link WantsParseError} on a malformed one. */
|
|
185
|
+
function parseOneWant(entry: string): ConnectionSpec {
|
|
186
|
+
const colon = entry.indexOf(":");
|
|
187
|
+
if (colon < 0) {
|
|
188
|
+
throw new WantsParseError(
|
|
189
|
+
`wants: "${entry}" is malformed — expected "<kind>:<target>…" ` +
|
|
190
|
+
`(e.g. "vault:research:read", "env:github", "mcp:https://…").`,
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
const prefix = entry.slice(0, colon);
|
|
194
|
+
const rest = entry.slice(colon + 1);
|
|
195
|
+
|
|
196
|
+
switch (prefix) {
|
|
197
|
+
case "vault":
|
|
198
|
+
return parseVaultWant(entry, rest);
|
|
199
|
+
case "env":
|
|
200
|
+
return parseServiceWant(entry, rest, "env");
|
|
201
|
+
case "mcp":
|
|
202
|
+
// `mcp:` is overloaded: a remote-MCP URL (kind:"mcp", 4b-2) vs a service MCP
|
|
203
|
+
// injection (kind:"service", inject:["mcp"], 4b-1). An http(s) target is the URL form.
|
|
204
|
+
if (/^https?:\/\//i.test(rest)) return parseMcpUrlWant(entry, rest);
|
|
205
|
+
return parseServiceWant(entry, rest, "mcp");
|
|
206
|
+
default:
|
|
207
|
+
throw new WantsParseError(
|
|
208
|
+
`wants: "${entry}" has unknown kind "${prefix}" — expected one of ` +
|
|
209
|
+
`vault | env | mcp.`,
|
|
210
|
+
);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/** Parse `vault:<name>:<read|write>[#tag…]`. */
|
|
215
|
+
function parseVaultWant(entry: string, rest: string): ConnectionSpec {
|
|
216
|
+
// rest = "<name>:<verb>[#tag…]". Split on the FIRST colon (name has no colon).
|
|
217
|
+
const colon = rest.indexOf(":");
|
|
218
|
+
if (colon < 0) {
|
|
219
|
+
throw new WantsParseError(
|
|
220
|
+
`wants: "${entry}" is malformed — a vault connection needs a verb: ` +
|
|
221
|
+
`"vault:<name>:<read|write>".`,
|
|
222
|
+
);
|
|
223
|
+
}
|
|
224
|
+
const name = rest.slice(0, colon);
|
|
225
|
+
let verbAndTags = rest.slice(colon + 1);
|
|
226
|
+
if (!VAULT_NAME_SLUG.test(name)) {
|
|
227
|
+
throw new WantsParseError(
|
|
228
|
+
`wants: "${entry}" — vault name "${name}" must be a slug (alphanumeric, dash, underscore).`,
|
|
229
|
+
);
|
|
230
|
+
}
|
|
231
|
+
// Tag suffix: everything from the first `#` onward, split into individual `#tag`s.
|
|
232
|
+
let tags: string[] | undefined;
|
|
233
|
+
const hash = verbAndTags.indexOf("#");
|
|
234
|
+
if (hash >= 0) {
|
|
235
|
+
const tagStr = verbAndTags.slice(hash);
|
|
236
|
+
verbAndTags = verbAndTags.slice(0, hash);
|
|
237
|
+
tags = tagStr
|
|
238
|
+
.split("#")
|
|
239
|
+
.map((t) => t.trim())
|
|
240
|
+
.filter((t) => t.length > 0)
|
|
241
|
+
.map((t) => `#${t}`);
|
|
242
|
+
if (tags.length === 0) tags = undefined;
|
|
243
|
+
}
|
|
244
|
+
const verb = verbAndTags.trim();
|
|
245
|
+
if (verb !== "read" && verb !== "write") {
|
|
246
|
+
throw new WantsParseError(
|
|
247
|
+
`wants: "${entry}" — vault access must be "read" or "write" (got "${verb}").`,
|
|
248
|
+
);
|
|
249
|
+
}
|
|
250
|
+
return { kind: "vault", target: name, access: verb, ...(tags ? { tags } : {}) };
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/** Parse `env:<service>` / `mcp:<service>` into one service connection. */
|
|
254
|
+
function parseServiceWant(entry: string, service: string, mode: "env" | "mcp"): ConnectionSpec {
|
|
255
|
+
const target = service.trim();
|
|
256
|
+
if (!SERVICE_NAME_SLUG.test(target)) {
|
|
257
|
+
throw new WantsParseError(
|
|
258
|
+
`wants: "${entry}" — service name "${target}" must be a slug (alphanumeric, dash, underscore).`,
|
|
259
|
+
);
|
|
260
|
+
}
|
|
261
|
+
// Reject a service whose env-var would collide with the Claude-auth denylist
|
|
262
|
+
// (e.g. a service named `claude-code-oauth` → CLAUDE_CODE_OAUTH_TOKEN). The
|
|
263
|
+
// spawn-time denylist already drops it (security intact), but surface it HERE at
|
|
264
|
+
// define-time so the operator sees the problem rather than a silent spawn-warn.
|
|
265
|
+
if (DENYLISTED_ENV.has(serviceEnvVar(target))) {
|
|
266
|
+
throw new WantsParseError(
|
|
267
|
+
`wants: "${entry}" — service "${target}" maps to the protected env var ${serviceEnvVar(target)}, ` +
|
|
268
|
+
`which a grant can never set (it's the session's managed Claude auth).`,
|
|
269
|
+
);
|
|
270
|
+
}
|
|
271
|
+
return { kind: "service", target, inject: [mode] };
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/** Parse `mcp:<https-url>` — a remote MCP (parsed; deferred to 4b-2). */
|
|
275
|
+
function parseMcpUrlWant(entry: string, url: string): ConnectionSpec {
|
|
276
|
+
let parsed: URL;
|
|
277
|
+
try {
|
|
278
|
+
parsed = new URL(url);
|
|
279
|
+
} catch {
|
|
280
|
+
throw new WantsParseError(`wants: "${entry}" — "${url}" is not a valid URL.`);
|
|
281
|
+
}
|
|
282
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
|
283
|
+
throw new WantsParseError(`wants: "${entry}" — remote MCP URL must be http(s) (got "${parsed.protocol}").`);
|
|
284
|
+
}
|
|
285
|
+
return { kind: "mcp", target: url };
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// ---------------------------------------------------------------------------
|
|
289
|
+
// The hub grants-API client (consume parachute-hub #668)
|
|
290
|
+
// ---------------------------------------------------------------------------
|
|
291
|
+
|
|
292
|
+
/** A grant record as the hub returns it (PUT result / GET list element). No secrets. */
|
|
293
|
+
export interface GrantRecord {
|
|
294
|
+
/** The hub-assigned grant id (used to fetch material). */
|
|
295
|
+
id: string;
|
|
296
|
+
/** The agent name the grant belongs to. */
|
|
297
|
+
agent: string;
|
|
298
|
+
/** The connection spec (echoed back). */
|
|
299
|
+
connection: ConnectionSpec;
|
|
300
|
+
/** Lifecycle: `pending` (registered, not approved), `approved`, `revoked`, `error`. */
|
|
301
|
+
status: string;
|
|
302
|
+
/** Optional human reason (e.g. why it errored). */
|
|
303
|
+
reason?: string;
|
|
304
|
+
/** ISO timestamp the operator approved it (approved grants). */
|
|
305
|
+
approvedAt?: string;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* Approved-grant material — APPROVED only. A discriminated union by `kind`.
|
|
310
|
+
* - `vault` → a Bearer + the granted vault's MCP URL (inject as an MCP server).
|
|
311
|
+
* - `service` → a Bearer + the inject shape(s) (env var and/or service MCP server).
|
|
312
|
+
* - `mcp` → a remote-MCP grant (4b-2): a Bearer + the remote MCP URL. The wire
|
|
313
|
+
* shape is byte-identical to `vault` (`{ token, mcpUrl }`) — the hub auto-refreshes
|
|
314
|
+
* OAuth tokens behind `/material` and projects only `{ kind, token, mcpUrl }`, so
|
|
315
|
+
* the consumer injects it the SAME way as a granted vault.
|
|
316
|
+
*/
|
|
317
|
+
export type GrantMaterial =
|
|
318
|
+
| { kind: "vault"; token: string; mcpUrl: string }
|
|
319
|
+
| { kind: "service"; token: string; inject: ("env" | "mcp")[] }
|
|
320
|
+
| { kind: "mcp"; token: string; mcpUrl: string };
|
|
321
|
+
|
|
322
|
+
/** A failed grants-API call — carries the HTTP status for the caller to branch on. */
|
|
323
|
+
export class GrantsApiError extends Error {
|
|
324
|
+
constructor(
|
|
325
|
+
message: string,
|
|
326
|
+
readonly status: number,
|
|
327
|
+
) {
|
|
328
|
+
super(message);
|
|
329
|
+
this.name = "GrantsApiError";
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
export interface GrantsClientDeps {
|
|
334
|
+
/** Hub public origin (the grants API lives on the hub, not the vault). */
|
|
335
|
+
hubOrigin: string;
|
|
336
|
+
/**
|
|
337
|
+
* The module's host-admin-capable MANAGER BEARER — the SAME operator token it
|
|
338
|
+
* mints vault tokens with (mint-token.ts). All three grants endpoints require a
|
|
339
|
+
* `parachute:host:admin` Bearer; we reuse this credential, no new auth path.
|
|
340
|
+
*/
|
|
341
|
+
managerBearer: string;
|
|
342
|
+
/** Inject fetch for tests. Defaults to global fetch. */
|
|
343
|
+
fetchFn?: typeof fetch;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
function stripTrailingSlash(url: string): string {
|
|
347
|
+
return url.replace(/\/$/, "");
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/**
|
|
351
|
+
* Thin client for the hub's grants API (parachute-hub #668 + #96). The calls the module
|
|
352
|
+
* makes — register (PUT), list (GET), fetch-material (GET …/material), and reconcile
|
|
353
|
+
* (POST …/reconcile, the grant-GC of #96). It NEVER approves/revokes (operator-only via
|
|
354
|
+
* the hub UI). All requests carry the manager bearer (`parachute:host:admin`).
|
|
355
|
+
*/
|
|
356
|
+
export class GrantsClient {
|
|
357
|
+
private readonly base: string;
|
|
358
|
+
private readonly managerBearer: string;
|
|
359
|
+
private readonly fetchFn: typeof fetch;
|
|
360
|
+
|
|
361
|
+
constructor(deps: GrantsClientDeps) {
|
|
362
|
+
if (!deps.hubOrigin) throw new Error("GrantsClient: hubOrigin is required");
|
|
363
|
+
if (!deps.managerBearer) throw new Error("GrantsClient: managerBearer is required");
|
|
364
|
+
this.base = stripTrailingSlash(deps.hubOrigin);
|
|
365
|
+
this.managerBearer = deps.managerBearer;
|
|
366
|
+
this.fetchFn = deps.fetchFn ?? fetch;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
private authHeaders(extra?: Record<string, string>): Record<string, string> {
|
|
370
|
+
return { authorization: `Bearer ${this.managerBearer}`, ...(extra ?? {}) };
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* Register (idempotent upsert) a PENDING grant request for `(agent, connection)`.
|
|
375
|
+
* `PUT /admin/grants { agent, connection }` → the grant record (status, usually
|
|
376
|
+
* `pending` on first register; an already-approved grant returns its current
|
|
377
|
+
* status). Throws {@link GrantsApiError} on a non-ok response.
|
|
378
|
+
*/
|
|
379
|
+
async registerGrant(agent: string, connection: ConnectionSpec): Promise<GrantRecord> {
|
|
380
|
+
const url = `${this.base}/admin/grants`;
|
|
381
|
+
const res = await this.fetchFn(url, {
|
|
382
|
+
method: "PUT",
|
|
383
|
+
headers: this.authHeaders({ "content-type": "application/json" }),
|
|
384
|
+
body: JSON.stringify({ agent, connection }),
|
|
385
|
+
});
|
|
386
|
+
if (!res.ok) {
|
|
387
|
+
const detail = await res.text().catch(() => "");
|
|
388
|
+
throw new GrantsApiError(`register grant failed (${res.status}) ${detail}`.trim(), res.status);
|
|
389
|
+
}
|
|
390
|
+
return (await res.json()) as GrantRecord;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
/**
|
|
394
|
+
* List the grants for an agent — `GET /admin/grants?agent=<name>` → `{ grants }`.
|
|
395
|
+
* No secrets (status only). Throws on a non-ok response.
|
|
396
|
+
*/
|
|
397
|
+
async listGrants(agent: string): Promise<GrantRecord[]> {
|
|
398
|
+
const url = `${this.base}/admin/grants?agent=${encodeURIComponent(agent)}`;
|
|
399
|
+
const res = await this.fetchFn(url, { headers: this.authHeaders() });
|
|
400
|
+
if (!res.ok) {
|
|
401
|
+
const detail = await res.text().catch(() => "");
|
|
402
|
+
throw new GrantsApiError(`list grants failed (${res.status}) ${detail}`.trim(), res.status);
|
|
403
|
+
}
|
|
404
|
+
const parsed = (await res.json()) as { grants?: GrantRecord[] };
|
|
405
|
+
return Array.isArray(parsed.grants) ? parsed.grants : [];
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
/**
|
|
409
|
+
* Fetch a grant's MATERIAL — `GET /admin/grants/<id>/material`. APPROVED only:
|
|
410
|
+
* the hub 404s an unknown id and 409s a not-yet-approved grant — both return null
|
|
411
|
+
* (the connection is simply absent this spawn, never a failure). Any OTHER non-ok
|
|
412
|
+
* throws {@link GrantsApiError} (a real fault the caller should log). The result
|
|
413
|
+
* is SECRET (a token) — fetched fresh each spawn, never cached.
|
|
414
|
+
*/
|
|
415
|
+
async getMaterial(id: string): Promise<GrantMaterial | null> {
|
|
416
|
+
const url = `${this.base}/admin/grants/${encodeURIComponent(id)}/material`;
|
|
417
|
+
const res = await this.fetchFn(url, { headers: this.authHeaders() });
|
|
418
|
+
if (res.status === 404 || res.status === 409) return null;
|
|
419
|
+
if (!res.ok) {
|
|
420
|
+
const detail = await res.text().catch(() => "");
|
|
421
|
+
throw new GrantsApiError(`get grant material failed (${res.status}) ${detail}`.trim(), res.status);
|
|
422
|
+
}
|
|
423
|
+
return (await res.json()) as GrantMaterial;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
/**
|
|
427
|
+
* GARBAGE-COLLECT an agent's now-stale grants (parachute-hub #96). `POST
|
|
428
|
+
* /admin/grants/reconcile { agent, liveConnections }` → `{ pruned, prunedIds }`. The
|
|
429
|
+
* hub re-derives each key with ITS OWN connectionKey and tears down + REMOVES every
|
|
430
|
+
* grant for `agent` whose key is NOT among the live specs (an empty array prunes ALL
|
|
431
|
+
* of the agent's grants — the def is gone). This is how a removed connection (or a
|
|
432
|
+
* deleted `#agent/definition` note) stops orphaning a live `approved` grant row.
|
|
433
|
+
*
|
|
434
|
+
* `liveConnections` is the agent's CURRENTLY-declared connection SPECS (`def.wants`).
|
|
435
|
+
* We send SPECS, not keys, so there's no dependency on this module's connectionKey
|
|
436
|
+
* matching the hub's — the hub keys them the same way it stored them.
|
|
437
|
+
*
|
|
438
|
+
* SAFETY: only ever call this from a CONFIDENT live set — a clean successful def load
|
|
439
|
+
* (real `liveConnections`) or a confirmed removal (empty array). NEVER from a parse/load
|
|
440
|
+
* failure: a transient error must not present an empty/partial set that nukes
|
|
441
|
+
* approved grants. Pruning only ever REMOVES access (never escalates), so the host-admin
|
|
442
|
+
* Bearer is the right auth (mirrors PUT/GET /admin/grants) — the same one the module
|
|
443
|
+
* uses for register/list/material. Throws {@link GrantsApiError} on a non-ok response;
|
|
444
|
+
* the caller logs + continues (best-effort — a GC fault must never crash a load).
|
|
445
|
+
*/
|
|
446
|
+
async reconcileGrants(
|
|
447
|
+
agent: string,
|
|
448
|
+
liveConnections: ConnectionSpec[],
|
|
449
|
+
): Promise<{ pruned: number; prunedIds?: string[] }> {
|
|
450
|
+
// Send the live connection SPECS, not pre-computed keys: the hub re-derives
|
|
451
|
+
// each key with its OWN connectionKey (the one it stored them under). Sending
|
|
452
|
+
// keys we computed here would couple to the hub's separate connectionKey impl,
|
|
453
|
+
// which diverges for service / tagged-vault / mixed-case-mcp grants and would
|
|
454
|
+
// wrongly prune still-wanted grants (caught by live verification 2026-06-18).
|
|
455
|
+
const url = `${this.base}/admin/grants/reconcile`;
|
|
456
|
+
const res = await this.fetchFn(url, {
|
|
457
|
+
method: "POST",
|
|
458
|
+
headers: this.authHeaders({ "content-type": "application/json" }),
|
|
459
|
+
body: JSON.stringify({ agent, liveConnections }),
|
|
460
|
+
});
|
|
461
|
+
if (!res.ok) {
|
|
462
|
+
const detail = await res.text().catch(() => "");
|
|
463
|
+
throw new GrantsApiError(`reconcile grants failed (${res.status}) ${detail}`.trim(), res.status);
|
|
464
|
+
}
|
|
465
|
+
const parsed = (await res.json().catch(() => ({}))) as { pruned?: number; prunedIds?: string[] };
|
|
466
|
+
return {
|
|
467
|
+
pruned: typeof parsed.pruned === "number" ? parsed.pruned : 0,
|
|
468
|
+
...(Array.isArray(parsed.prunedIds) ? { prunedIds: parsed.prunedIds } : {}),
|
|
469
|
+
};
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
// ---------------------------------------------------------------------------
|
|
474
|
+
// Status resolution — enabled vs pending from registered grants
|
|
475
|
+
// ---------------------------------------------------------------------------
|
|
476
|
+
|
|
477
|
+
/** The resolved status for a def after its connections are registered. */
|
|
478
|
+
export interface ConnectionStatus {
|
|
479
|
+
/** `enabled` iff every declared connection is `approved`; else `pending`. */
|
|
480
|
+
status: "enabled" | "pending";
|
|
481
|
+
/** The connection keys NOT yet approved (only when `pending`). */
|
|
482
|
+
pending?: string[];
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
/**
|
|
486
|
+
* Resolve the def's status from its declared connections + each one's registered
|
|
487
|
+
* grant status (design §2). `enabled` ONLY if EVERY connection is `approved`; else
|
|
488
|
+
* `pending` listing the unapproved connection keys. No connections → `enabled`.
|
|
489
|
+
*
|
|
490
|
+
* `grantStatusByKey` maps a {@link connectionKey} → the hub's grant status. A
|
|
491
|
+
* connection with no entry (registration failed / not found) counts as NOT approved.
|
|
492
|
+
*/
|
|
493
|
+
export function resolveConnectionStatus(
|
|
494
|
+
connections: ConnectionSpec[],
|
|
495
|
+
grantStatusByKey: Map<string, string>,
|
|
496
|
+
): ConnectionStatus {
|
|
497
|
+
if (connections.length === 0) return { status: "enabled" };
|
|
498
|
+
const pending: string[] = [];
|
|
499
|
+
for (const c of connections) {
|
|
500
|
+
const key = connectionKey(c);
|
|
501
|
+
if (grantStatusByKey.get(key) !== "approved") pending.push(key);
|
|
502
|
+
}
|
|
503
|
+
if (pending.length === 0) return { status: "enabled" };
|
|
504
|
+
return { status: "pending", pending };
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
// ---------------------------------------------------------------------------
|
|
508
|
+
// Spawn-time injection — approved grant material → MCP-config entries + env vars
|
|
509
|
+
// ---------------------------------------------------------------------------
|
|
510
|
+
|
|
511
|
+
/** Known service → the env var name its token injects as. Default: `<TARGET>_TOKEN`. */
|
|
512
|
+
const SERVICE_ENV_VAR: Record<string, string> = {
|
|
513
|
+
github: "GITHUB_TOKEN",
|
|
514
|
+
cloudflare: "CLOUDFLARE_API_TOKEN",
|
|
515
|
+
};
|
|
516
|
+
|
|
517
|
+
/** Known service → its remote MCP server URL (for the `inject:["mcp"]` shape). */
|
|
518
|
+
const SERVICE_MCP_URL: Record<string, string> = {
|
|
519
|
+
github: "https://api.githubcopilot.com/mcp/",
|
|
520
|
+
};
|
|
521
|
+
|
|
522
|
+
/** The env var name a service's token injects as (known map ?? `<TARGET>_TOKEN`). */
|
|
523
|
+
export function serviceEnvVar(service: string): string {
|
|
524
|
+
return SERVICE_ENV_VAR[service] ?? `${service.toUpperCase().replace(/[^A-Z0-9]+/g, "_")}_TOKEN`;
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
/** The known remote-MCP URL for a service, or undefined (no MCP injection for it). */
|
|
528
|
+
export function serviceMcpUrl(service: string): string | undefined {
|
|
529
|
+
return SERVICE_MCP_URL[service];
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
/** One MCP server entry to ADD to the agent's `--mcp-config` from a grant. */
|
|
533
|
+
export interface InjectedMcpEntry {
|
|
534
|
+
/** Entry key in `mcpServers` (unique per granted resource). */
|
|
535
|
+
name: string;
|
|
536
|
+
/** Streamable-HTTP MCP URL. */
|
|
537
|
+
url: string;
|
|
538
|
+
/** Bearer token for the Authorization header. */
|
|
539
|
+
token: string;
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
/** The result of resolving an agent's approved grants into spawn-injectable bits. */
|
|
543
|
+
export interface InjectedGrants {
|
|
544
|
+
/** MCP servers to ADD to the existing per-spawn `.mcp.json` (vault + service-mcp). */
|
|
545
|
+
mcpEntries: InjectedMcpEntry[];
|
|
546
|
+
/** Env vars to set for the agent's shell tools (service env injections). */
|
|
547
|
+
env: Record<string, string>;
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
/**
|
|
551
|
+
* Resolve an agent's APPROVED grants into spawn-injectable MCP entries + env vars
|
|
552
|
+
* (design §3). Fetches the agent's grant LIST, then for each `approved` grant fetches
|
|
553
|
+
* its MATERIAL FRESH (never cached — revocation takes effect next spawn) and maps it:
|
|
554
|
+
*
|
|
555
|
+
* - vault material (`{token, mcpUrl}`) → an MCP server entry (the agent
|
|
556
|
+
* reaches the OTHER vault alongside its own).
|
|
557
|
+
* - service material, inject includes `"env"` → an env var (github→GITHUB_TOKEN,
|
|
558
|
+
* cloudflare→CLOUDFLARE_API_TOKEN, default `<TARGET>_TOKEN`).
|
|
559
|
+
* - service material, inject includes `"mcp"` → the service's MCP server entry
|
|
560
|
+
* (known-service→URL map; a service with no known MCP logs + SKIPS the mcp
|
|
561
|
+
* inject, keeping the env one).
|
|
562
|
+
* - mcp material (`{token, mcpUrl}`, 4b-2) → an MCP server entry (the agent
|
|
563
|
+
* reaches the remote MCP / OAuth resource). An UNAPPROVED mcp grant has no
|
|
564
|
+
* material — `getMaterial` returns null (404/409), so it's simply absent.
|
|
565
|
+
*
|
|
566
|
+
* The MCP-entry KEYS are namespaced (`grant-vault-<name>`, `grant-service-<svc>`,
|
|
567
|
+
* `grant-mcp-<grant-id>`) so they never collide with the agent's own def-vault entry
|
|
568
|
+
* (`parachute-vault-<name>`).
|
|
569
|
+
*
|
|
570
|
+
* Best-effort + isolated: the grant LIST failing throws (the caller logs + spawns
|
|
571
|
+
* WITHOUT injected grants — own-vault still works); a SINGLE material fetch failing
|
|
572
|
+
* is logged + skipped (that one connection is absent; the rest inject). Secrets only
|
|
573
|
+
* flow into the returned struct → the ephemeral 0600 spawn config; never logged.
|
|
574
|
+
*/
|
|
575
|
+
export async function resolveInjectedGrants(
|
|
576
|
+
client: GrantsClient,
|
|
577
|
+
agent: string,
|
|
578
|
+
): Promise<InjectedGrants> {
|
|
579
|
+
const mcpEntries: InjectedMcpEntry[] = [];
|
|
580
|
+
const env: Record<string, string> = {};
|
|
581
|
+
|
|
582
|
+
const grants = await client.listGrants(agent); // throws → caller spawns without grants
|
|
583
|
+
for (const g of grants) {
|
|
584
|
+
if (g.status !== "approved") continue; // only approved grants have material
|
|
585
|
+
let material: GrantMaterial | null;
|
|
586
|
+
try {
|
|
587
|
+
material = await client.getMaterial(g.id);
|
|
588
|
+
} catch (err) {
|
|
589
|
+
// A single material fetch fault must not sink the others — that connection is
|
|
590
|
+
// simply absent this spawn. Never log the token (there is none in the error).
|
|
591
|
+
console.warn(
|
|
592
|
+
`parachute-agent: fetching grant material for "${agent}" (${connectionKey(g.connection)}) ` +
|
|
593
|
+
`failed (skipping this connection): ${(err as Error).message}`,
|
|
594
|
+
);
|
|
595
|
+
continue;
|
|
596
|
+
}
|
|
597
|
+
if (!material) continue; // 404/409 — not actually approved/available right now
|
|
598
|
+
|
|
599
|
+
if (material.kind === "vault") {
|
|
600
|
+
mcpEntries.push({
|
|
601
|
+
name: grantVaultEntryKey(g.connection.target),
|
|
602
|
+
url: material.mcpUrl,
|
|
603
|
+
token: material.token,
|
|
604
|
+
});
|
|
605
|
+
continue;
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
if (material.kind === "mcp") {
|
|
609
|
+
// Remote-MCP grant (4b-2): the /material wire shape is byte-identical to vault's
|
|
610
|
+
// (`{token, mcpUrl}`); inject it the SAME way. Key on the grant ID (not the URL)
|
|
611
|
+
// so two distinct remote MCPs never collide on the entry name.
|
|
612
|
+
mcpEntries.push({
|
|
613
|
+
name: grantMcpEntryKey(g.id),
|
|
614
|
+
url: material.mcpUrl,
|
|
615
|
+
token: material.token,
|
|
616
|
+
});
|
|
617
|
+
continue;
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
if (material.kind === "service") {
|
|
621
|
+
// service material — inject env and/or mcp per the material's `inject` list.
|
|
622
|
+
const service = g.connection.target;
|
|
623
|
+
const inject = material.inject ?? [];
|
|
624
|
+
if (inject.includes("env")) {
|
|
625
|
+
env[serviceEnvVar(service)] = material.token;
|
|
626
|
+
}
|
|
627
|
+
if (inject.includes("mcp")) {
|
|
628
|
+
const url = serviceMcpUrl(service);
|
|
629
|
+
if (url) {
|
|
630
|
+
mcpEntries.push({
|
|
631
|
+
name: grantServiceEntryKey(service),
|
|
632
|
+
url,
|
|
633
|
+
token: material.token,
|
|
634
|
+
});
|
|
635
|
+
} else {
|
|
636
|
+
// No known MCP URL for this service — keep the env inject, skip the mcp one.
|
|
637
|
+
console.warn(
|
|
638
|
+
`parachute-agent: service "${service}" granted with inject:"mcp" but no known MCP URL — ` +
|
|
639
|
+
`skipping the MCP injection (the env injection, if any, still applies).`,
|
|
640
|
+
);
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
continue;
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
// Exhaustiveness guard (future-safety): every known material kind `continue`s
|
|
647
|
+
// above, so `material` is `never` here today. If a future kind is added to the
|
|
648
|
+
// union without a branch, it lands here + is skipped LOUDLY rather than silently
|
|
649
|
+
// falling into the service path. Never log the token (the struct, not the value).
|
|
650
|
+
console.warn(
|
|
651
|
+
`parachute-agent: grant material for "${agent}" has an unhandled kind ` +
|
|
652
|
+
`"${(material as { kind?: string }).kind}" — skipping (no injection).`,
|
|
653
|
+
);
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
return { mcpEntries, env };
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
/** MCP entry key for a GRANTED vault — namespaced so it never collides with the
|
|
660
|
+
* agent's OWN def-vault entry (`parachute-vault-<name>`). */
|
|
661
|
+
export function grantVaultEntryKey(vault: string): string {
|
|
662
|
+
return `grant-vault-${vault}`;
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
/** MCP entry key for a GRANTED service MCP server. */
|
|
666
|
+
export function grantServiceEntryKey(service: string): string {
|
|
667
|
+
return `grant-service-${service}`;
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
/** MCP entry key for a GRANTED remote MCP (4b-2) — keyed by the grant id (stable +
|
|
671
|
+
* collision-free) and namespaced so it never collides with `grant-vault-*` /
|
|
672
|
+
* `grant-service-*` / the agent's OWN def-vault entry (`parachute-vault-<name>`). */
|
|
673
|
+
export function grantMcpEntryKey(slug: string): string {
|
|
674
|
+
return `grant-mcp-${slug}`;
|
|
675
|
+
}
|