@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/mcp-http.ts
ADDED
|
@@ -0,0 +1,771 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stateful HTTP MCP endpoint for parachute-agent.
|
|
3
|
+
*
|
|
4
|
+
* A second, additive way for a Claude Code session to connect to a channel:
|
|
5
|
+
* instead of spawning the stdio `bridge.ts` and consuming the daemon's SSE
|
|
6
|
+
* `/events`, the session adds the channel as a *pure HTTP MCP server* (URL +
|
|
7
|
+
* OAuth) — exactly like the vault. No local file, works on any machine.
|
|
8
|
+
*
|
|
9
|
+
* Why STATEFUL (not stateless like vault's mcp-http.ts): the live WAKE
|
|
10
|
+
* (`pushToChannel`) PUSHes a `notifications/claude/agent` onto a connected
|
|
11
|
+
* session's standalone GET stream — the programmatic backend's "watch it work"
|
|
12
|
+
* interim-text streaming + the live inbound wake for a subscribed session.
|
|
13
|
+
* Stateful Streamable HTTP (a `sessionIdGenerator` + `enableJsonResponse:false`)
|
|
14
|
+
* gives each session an SSE GET stream the server can push onto. This file is the
|
|
15
|
+
* productionized form: per-channel session registry, the push surface, the
|
|
16
|
+
* ATTACHED-backend pull surface (`pending`/`next-message`/`reply`/`release` —
|
|
17
|
+
* design 2026-06-18), and read-vs-write scope enforcement.
|
|
18
|
+
*
|
|
19
|
+
* NOTE — the deaf-on-restart BACKLOG REPLAY (the connect-hook that replayed
|
|
20
|
+
* messages a session missed while idle) was RETIRED with the interactive backend
|
|
21
|
+
* (design 2026-06-19-retire-interactive-backend.md): the programmatic path runs
|
|
22
|
+
* synchronously and the channel path uses the durable note-claim queue, so there's
|
|
23
|
+
* no missed-while-idle backlog to replay onto a reconnecting session.
|
|
24
|
+
*
|
|
25
|
+
* Lifecycle:
|
|
26
|
+
* - POST /mcp/<channel> with no mcp-session-id → a NEW session: build a
|
|
27
|
+
* Server + stateful transport, connect, register under <channel> on
|
|
28
|
+
* onsessioninitialized.
|
|
29
|
+
* - POST/GET /mcp/<channel> with a known mcp-session-id → route to that
|
|
30
|
+
* session's transport (GET opens the SSE push stream).
|
|
31
|
+
* - DELETE /mcp/<channel> (or transport.onclose) → tear the session down and
|
|
32
|
+
* drop it from the channel's set.
|
|
33
|
+
*
|
|
34
|
+
* Auth: the daemon validates `agent:read` BEFORE calling handleMcp (a session
|
|
35
|
+
* needs read to connect + receive the wake). The validated scopes are threaded
|
|
36
|
+
* in and stored ON the session, so the reply/react/edit/download tool handlers
|
|
37
|
+
* can additionally require `agent:write` — a read-only token connects and is
|
|
38
|
+
* woken but cannot send.
|
|
39
|
+
*/
|
|
40
|
+
|
|
41
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
42
|
+
import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js";
|
|
43
|
+
import {
|
|
44
|
+
ListToolsRequestSchema,
|
|
45
|
+
CallToolRequestSchema,
|
|
46
|
+
} from "@modelcontextprotocol/sdk/types.js";
|
|
47
|
+
import type {
|
|
48
|
+
Transport,
|
|
49
|
+
ReplyArgs,
|
|
50
|
+
ReactArgs,
|
|
51
|
+
EditArgs,
|
|
52
|
+
DownloadArgs,
|
|
53
|
+
} from "./transport.ts";
|
|
54
|
+
import { SCOPE_WRITE, grantsScope } from "./auth.ts";
|
|
55
|
+
import type { AttachedQueueRegistry } from "./backends/attached-queue.ts";
|
|
56
|
+
|
|
57
|
+
// ---------------------------------------------------------------------------
|
|
58
|
+
// Per-channel session registry
|
|
59
|
+
// ---------------------------------------------------------------------------
|
|
60
|
+
|
|
61
|
+
/** A live HTTP MCP session = a Server + its stateful transport + caller scopes. */
|
|
62
|
+
interface McpSession {
|
|
63
|
+
server: Server;
|
|
64
|
+
transport: WebStandardStreamableHTTPServerTransport;
|
|
65
|
+
/** The scopes the connecting token carried — write-tools check these. */
|
|
66
|
+
scopes: string[];
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* channel name → its set of live MCP sessions. A push on channel A reaches only
|
|
71
|
+
* the sessions registered under A. Distinct from the SSE ClientRegistry (which
|
|
72
|
+
* serves stdio bridges over `/events`); the two run side by side.
|
|
73
|
+
*/
|
|
74
|
+
const sessionsByChannel = new Map<string, Set<McpSession>>();
|
|
75
|
+
|
|
76
|
+
/** mcp-session-id → session, so a follow-up POST/GET/DELETE finds its transport. */
|
|
77
|
+
const sessionsById = new Map<string, McpSession>();
|
|
78
|
+
|
|
79
|
+
function registerSession(channel: string, id: string, session: McpSession): void {
|
|
80
|
+
let set = sessionsByChannel.get(channel);
|
|
81
|
+
if (!set) {
|
|
82
|
+
set = new Set();
|
|
83
|
+
sessionsByChannel.set(channel, set);
|
|
84
|
+
}
|
|
85
|
+
set.add(session);
|
|
86
|
+
sessionsById.set(id, session);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Whether a session has a LIVE standalone GET SSE push stream — the stream the
|
|
91
|
+
* SDK writes `notifications/claude/agent` onto. A session that has only POSTed
|
|
92
|
+
* `initialize` (registered) but not yet opened — or has since dropped — its GET
|
|
93
|
+
* stream is NOT deliverable: `transport.send()` silently no-ops for it (no event
|
|
94
|
+
* store is configured, so the message is dropped, not buffered). We read the SAME
|
|
95
|
+
* internal map the SDK's send() consults (`_streamMapping['_GET_stream']`), so our
|
|
96
|
+
* notion of "deliverable" can never disagree with whether the SDK actually writes.
|
|
97
|
+
*/
|
|
98
|
+
function sessionHasLivePushStream(session: McpSession): boolean {
|
|
99
|
+
const t = session.transport as unknown as
|
|
100
|
+
| { _streamMapping?: Map<string, unknown> }
|
|
101
|
+
| undefined;
|
|
102
|
+
return !!t && t._streamMapping?.get("_GET_stream") !== undefined;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function unregisterSession(channel: string, id: string): void {
|
|
106
|
+
const session = sessionsById.get(id);
|
|
107
|
+
sessionsById.delete(id);
|
|
108
|
+
const set = sessionsByChannel.get(channel);
|
|
109
|
+
if (set && session) {
|
|
110
|
+
set.delete(session);
|
|
111
|
+
if (set.size === 0) sessionsByChannel.delete(channel);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** Count of live MCP sessions on a channel (for /health). */
|
|
116
|
+
export function mcpSessionCount(channel: string): number {
|
|
117
|
+
return sessionsByChannel.get(channel)?.size ?? 0;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Boot-time guard for the ONE SDK internal `sessionHasLivePushStream` depends on:
|
|
122
|
+
* the standalone GET stream is keyed by `_standaloneSseStreamId === "_GET_stream"`
|
|
123
|
+
* inside the transport's `_streamMapping`. We read that private field (the SDK
|
|
124
|
+
* exposes no public "is the push stream open?" API), so an SDK upgrade that renamed
|
|
125
|
+
* it would make `sessionHasLivePushStream` return false forever — silently breaking
|
|
126
|
+
* the HTTP-MCP live wake (`pushToChannel`), which gates on it. The caret pin (`^1.x`)
|
|
127
|
+
* lets a `bun update` pull such a version, so we verify
|
|
128
|
+
* the contract LOUDLY at boot rather than discover it as silent message loss in
|
|
129
|
+
* production. Verified against @modelcontextprotocol/sdk 1.29.x. Returns true when
|
|
130
|
+
* the contract holds; logs a screaming error and returns false otherwise.
|
|
131
|
+
*/
|
|
132
|
+
export function assertMcpSdkStreamContract(): boolean {
|
|
133
|
+
try {
|
|
134
|
+
const probe = new WebStandardStreamableHTTPServerTransport({
|
|
135
|
+
sessionIdGenerator: () => "contract-probe",
|
|
136
|
+
enableJsonResponse: false,
|
|
137
|
+
});
|
|
138
|
+
const id = (probe as unknown as { _standaloneSseStreamId?: unknown })._standaloneSseStreamId;
|
|
139
|
+
const hasMap =
|
|
140
|
+
(probe as unknown as { _streamMapping?: unknown })._streamMapping instanceof Map;
|
|
141
|
+
if (id === "_GET_stream" && hasMap) return true;
|
|
142
|
+
console.error(
|
|
143
|
+
"parachute-agent: FATAL CONTRACT DRIFT — the MCP SDK's standalone-GET-stream " +
|
|
144
|
+
`internals changed (expected _standaloneSseStreamId="_GET_stream" + a _streamMapping Map; ` +
|
|
145
|
+
`got id=${JSON.stringify(id)}, map=${hasMap}). sessionHasLivePushStream() can no longer ` +
|
|
146
|
+
"detect a live push stream, so the HTTP-MCP live wake (pushToChannel) is " +
|
|
147
|
+
"BROKEN. Pin @modelcontextprotocol/sdk back, or update mcp-http.ts to the new internals.",
|
|
148
|
+
);
|
|
149
|
+
return false;
|
|
150
|
+
} catch (err) {
|
|
151
|
+
console.error(
|
|
152
|
+
`parachute-agent: could not verify MCP SDK stream contract (${(err as Error).message}); ` +
|
|
153
|
+
"HTTP-MCP delivery may be unreliable.",
|
|
154
|
+
);
|
|
155
|
+
return false;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** Test/teardown helper — drop every registered session without touching transports. */
|
|
160
|
+
export function _resetSessionsForTest(): void {
|
|
161
|
+
sessionsByChannel.clear();
|
|
162
|
+
sessionsById.clear();
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Test-only: register a fake session under a channel without booting a real
|
|
167
|
+
* transport. The `server` only needs a `.notification` method for the push
|
|
168
|
+
* tests; pass scopes to model a connection's grant.
|
|
169
|
+
*/
|
|
170
|
+
export function _registerSessionForTest(
|
|
171
|
+
channel: string,
|
|
172
|
+
id: string,
|
|
173
|
+
server: Server,
|
|
174
|
+
scopes: string[],
|
|
175
|
+
opts?: { streamless?: boolean },
|
|
176
|
+
): void {
|
|
177
|
+
// Model the real transport's deliverability: a connected session whose GET
|
|
178
|
+
// stream is open carries `_streamMapping['_GET_stream']` — the same key
|
|
179
|
+
// sessionHasLivePushStream + the SDK's send() consult. `streamless: true` models
|
|
180
|
+
// a session that registered (POSTed initialize) but never opened — or has since
|
|
181
|
+
// dropped — its GET stream: registered but NOT deliverable.
|
|
182
|
+
const transport = opts?.streamless
|
|
183
|
+
? (undefined as never)
|
|
184
|
+
: ({ _streamMapping: new Map<string, unknown>([["_GET_stream", {}]]) } as never);
|
|
185
|
+
const session: McpSession = { server, transport, scopes };
|
|
186
|
+
registerSession(channel, id, session);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/** Test-only: remove a registered session — exercises the empty-set cleanup path. */
|
|
190
|
+
export function _unregisterSessionForTest(channel: string, id: string): void {
|
|
191
|
+
unregisterSession(channel, id);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// ---------------------------------------------------------------------------
|
|
195
|
+
// The wake — push to a channel's MCP sessions
|
|
196
|
+
// ---------------------------------------------------------------------------
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Push an inbound message to every MCP session on `channel` as a
|
|
200
|
+
* `notifications/claude/agent` — the wake that pulls an idle session in to
|
|
201
|
+
* answer. The daemon calls this alongside the existing SSE `routeToChannel`, so
|
|
202
|
+
* both stdio bridges and HTTP MCP sessions receive the same inbound traffic.
|
|
203
|
+
*/
|
|
204
|
+
export function pushToChannel(
|
|
205
|
+
channel: string,
|
|
206
|
+
content: string,
|
|
207
|
+
meta: Record<string, string>,
|
|
208
|
+
): number {
|
|
209
|
+
const set = sessionsByChannel.get(channel);
|
|
210
|
+
if (!set) return 0;
|
|
211
|
+
let delivered = 0;
|
|
212
|
+
for (const session of set) {
|
|
213
|
+
// Only sessions with a LIVE GET push stream are deliverable. The SDK silently
|
|
214
|
+
// drops a notification to a streamless session (no throw, nothing buffered), so
|
|
215
|
+
// counting one here would falsely advance the channel's delivery mark. A
|
|
216
|
+
// streamless session is simply not woken by this live push (the deaf-on-restart
|
|
217
|
+
// backlog replay that used to recover it was retired with the interactive backend).
|
|
218
|
+
if (!sessionHasLivePushStream(session)) continue;
|
|
219
|
+
try {
|
|
220
|
+
void session.server.notification({
|
|
221
|
+
method: "notifications/claude/agent",
|
|
222
|
+
params: { content, meta: { source: "parachute-agent", ...meta } },
|
|
223
|
+
});
|
|
224
|
+
delivered++;
|
|
225
|
+
} catch {
|
|
226
|
+
// A dead session throws SYNCHRONOUSLY on a closed transport (SDK `send`), so
|
|
227
|
+
// this catch runs and the count stays honest; the transport's onclose handler
|
|
228
|
+
// removes the session from the set.
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
return delivered;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Push a permission verdict to a channel's MCP sessions (mirrors the SSE
|
|
236
|
+
* `permission_verdict` route → bridge's `notifications/claude/agent/permission`).
|
|
237
|
+
*/
|
|
238
|
+
export function pushPermissionVerdict(
|
|
239
|
+
channel: string,
|
|
240
|
+
verdict: { request_id: string; behavior: string },
|
|
241
|
+
): number {
|
|
242
|
+
const set = sessionsByChannel.get(channel);
|
|
243
|
+
if (!set) return 0;
|
|
244
|
+
let delivered = 0;
|
|
245
|
+
for (const session of set) {
|
|
246
|
+
// Same deliverability gate as pushToChannel: a streamless session can't
|
|
247
|
+
// receive the verdict (the SDK would drop it), so don't claim it did.
|
|
248
|
+
if (!sessionHasLivePushStream(session)) continue;
|
|
249
|
+
try {
|
|
250
|
+
void session.server.notification({
|
|
251
|
+
method: "notifications/claude/agent/permission",
|
|
252
|
+
params: { request_id: verdict.request_id, behavior: verdict.behavior },
|
|
253
|
+
});
|
|
254
|
+
delivered++;
|
|
255
|
+
} catch {}
|
|
256
|
+
}
|
|
257
|
+
return delivered;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// ---------------------------------------------------------------------------
|
|
261
|
+
// Tool surface — ported from bridge.ts, dispatched to the channel's transport
|
|
262
|
+
// ---------------------------------------------------------------------------
|
|
263
|
+
|
|
264
|
+
const INSTRUCTIONS = [
|
|
265
|
+
"You are connected to a live chat channel over HTTP MCP. A human is messaging you through it.",
|
|
266
|
+
"",
|
|
267
|
+
"CRITICAL — HOW THE HUMAN SEES YOU: they read ONLY what you send via the `reply` tool. Your normal assistant/transcript text is INVISIBLE to them. So for EVERY message that arrives on this channel you MUST call the `reply` tool to answer — even a one-word reply. Never answer only in your transcript: if you don't call `reply`, the human sees nothing at all.",
|
|
268
|
+
"",
|
|
269
|
+
'Inbound messages arrive as <channel source="parachute-agent" ...> with metadata attributes describing the sender. Treat each as a chat message from the human and respond by calling the reply tool — the daemon routes it back out the same channel. Pass back any addressing fields the inbound tag carried (e.g. chat_id) if present; omit them otherwise. Use reply_to (message_id) to thread a specific message; omit it for normal responses.',
|
|
270
|
+
"",
|
|
271
|
+
"If the tag has an image_path attribute, Read that file — it is an attachment the sender sent.",
|
|
272
|
+
"If the tag has attachment_file_id, call download_attachment with that file_id to fetch the file, then Read the returned path.",
|
|
273
|
+
"",
|
|
274
|
+
"Use react to add emoji reactions. Use edit_message for interim progress updates (edits do not push notifications — send a new reply when a long task completes so the user's device pings).",
|
|
275
|
+
"",
|
|
276
|
+
'reply accepts file paths (files: ["/abs/path.png"]) for attachments.',
|
|
277
|
+
].join("\n");
|
|
278
|
+
|
|
279
|
+
/** The tool list — identical schema to bridge.ts (transport-neutral: no chat_id required on reply). */
|
|
280
|
+
const TOOL_DEFS = [
|
|
281
|
+
{
|
|
282
|
+
name: "reply",
|
|
283
|
+
description:
|
|
284
|
+
"Send a message back through the channel to the sender. Supports text, file attachments, and quote-reply threading. The daemon routes it out whichever transport the channel uses.",
|
|
285
|
+
inputSchema: {
|
|
286
|
+
type: "object" as const,
|
|
287
|
+
properties: {
|
|
288
|
+
text: { type: "string", description: "Message text (optional if files provided)" },
|
|
289
|
+
reply_to: { type: "string", description: "Message ID to quote-reply (optional)" },
|
|
290
|
+
files: {
|
|
291
|
+
type: "array",
|
|
292
|
+
items: { type: "string" },
|
|
293
|
+
description: "Absolute file paths to attach (optional)",
|
|
294
|
+
},
|
|
295
|
+
chat_id: {
|
|
296
|
+
type: "string",
|
|
297
|
+
description:
|
|
298
|
+
"Addressing field some transports need (e.g. a Telegram chat ID). Include it ONLY if the inbound message tag carried one; omit it otherwise (e.g. for a web/UI channel).",
|
|
299
|
+
},
|
|
300
|
+
},
|
|
301
|
+
required: [] as string[],
|
|
302
|
+
},
|
|
303
|
+
},
|
|
304
|
+
{
|
|
305
|
+
name: "react",
|
|
306
|
+
description:
|
|
307
|
+
"Add an emoji reaction to a message, on transports that support reactions (e.g. Telegram's fixed emoji whitelist 👍 👎 ❤ 🔥 👀). Not all channels support this.",
|
|
308
|
+
inputSchema: {
|
|
309
|
+
type: "object" as const,
|
|
310
|
+
properties: {
|
|
311
|
+
message_id: { type: "string", description: "Message ID to react to" },
|
|
312
|
+
emoji: { type: "string", description: "Emoji reaction" },
|
|
313
|
+
chat_id: {
|
|
314
|
+
type: "string",
|
|
315
|
+
description: "Addressing field for transports that need it (e.g. Telegram chat ID); omit otherwise.",
|
|
316
|
+
},
|
|
317
|
+
},
|
|
318
|
+
required: ["message_id", "emoji"],
|
|
319
|
+
},
|
|
320
|
+
},
|
|
321
|
+
{
|
|
322
|
+
name: "edit_message",
|
|
323
|
+
description:
|
|
324
|
+
"Edit a message you previously sent. Useful for progress updates. On most transports edits don't push a notification.",
|
|
325
|
+
inputSchema: {
|
|
326
|
+
type: "object" as const,
|
|
327
|
+
properties: {
|
|
328
|
+
message_id: { type: "string", description: "Message ID to edit" },
|
|
329
|
+
text: { type: "string", description: "New text" },
|
|
330
|
+
chat_id: {
|
|
331
|
+
type: "string",
|
|
332
|
+
description: "Addressing field for transports that need it (e.g. Telegram chat ID); omit otherwise.",
|
|
333
|
+
},
|
|
334
|
+
},
|
|
335
|
+
required: ["message_id", "text"],
|
|
336
|
+
},
|
|
337
|
+
},
|
|
338
|
+
{
|
|
339
|
+
name: "download_attachment",
|
|
340
|
+
description: "Download a Telegram file attachment by file_id. Returns the local path.",
|
|
341
|
+
inputSchema: {
|
|
342
|
+
type: "object" as const,
|
|
343
|
+
properties: {
|
|
344
|
+
file_id: { type: "string", description: "Telegram file_id from the attachment_file_id attribute" },
|
|
345
|
+
},
|
|
346
|
+
required: ["file_id"],
|
|
347
|
+
},
|
|
348
|
+
},
|
|
349
|
+
];
|
|
350
|
+
|
|
351
|
+
// Tools that send/mutate on the channel require agent:write. download_attachment
|
|
352
|
+
// is deliberately NOT here: fetching an attachment that was sent *to* this session is
|
|
353
|
+
// read-access — an agent:read session can receive and read its own messages,
|
|
354
|
+
// attachments included. (The legacy stdio-bridge /api/download gates it as write; the
|
|
355
|
+
// MCP path is the principled read. If they ever need to match, relax the bridge, not this.)
|
|
356
|
+
const WRITE_TOOLS = new Set(["reply", "react", "edit_message"]);
|
|
357
|
+
|
|
358
|
+
// ---------------------------------------------------------------------------
|
|
359
|
+
// ATTACHED-BACKEND tool surface — the MCP pull/reply protocol (design
|
|
360
|
+
// 2026-06-18-channel-backend.md, phase 2). When the channel has a `backend:attached`
|
|
361
|
+
// agent registered, the session connects + PULLs the durable queue instead of being
|
|
362
|
+
// pushed to: `pending` / `next-message` / `reply` / `release`, dispatched to the
|
|
363
|
+
// {@link AttachedQueueRegistry}. The session "is" the agent by adopting the
|
|
364
|
+
// systemPrompt `next-message` returns (the def body) — reinforced by INSTRUCTIONS
|
|
365
|
+
// below, since MCP can't force a system prompt on the caller.
|
|
366
|
+
// ---------------------------------------------------------------------------
|
|
367
|
+
|
|
368
|
+
const CHANNEL_INSTRUCTIONS = [
|
|
369
|
+
"You are connected to a Parachute CHANNEL — a durable queue of messages a human sent to an agent you are handling. Nothing is pushed to you; you PULL.",
|
|
370
|
+
"",
|
|
371
|
+
"THE LOOP, every time you're ready to handle a message:",
|
|
372
|
+
" 1. `pending` — see how many messages are waiting (count + a preview of each).",
|
|
373
|
+
" 2. `next-message` — claim the oldest waiting message. It returns { id, text, inReplyTo, systemPrompt }.",
|
|
374
|
+
" 3. TREAT THE RETURNED `systemPrompt` AS YOUR INSTRUCTIONS FOR THIS REPLY — it is the agent's persona/role. Adopt it: answer as that agent would.",
|
|
375
|
+
" 4. Do the work in this session (your full tools, your env).",
|
|
376
|
+
" 5. `reply` { inReplyTo: <the claimed id>, text: <your answer> } — this writes the reply back to the human and marks the message handled.",
|
|
377
|
+
"",
|
|
378
|
+
"If you claim a message with `next-message` but can't handle it, call `release` { id } to return it to the queue for another session (or your next pass). Claimed messages auto-release after a while if you go away, so the queue never gets stranded.",
|
|
379
|
+
"",
|
|
380
|
+
"The human reads ONLY what you send via `reply`. Your transcript text is invisible to them — always finish a handled message with a `reply`.",
|
|
381
|
+
].join("\n");
|
|
382
|
+
|
|
383
|
+
/** The attached-backend pull/reply tool list (design 2026-06-18, phase 2). */
|
|
384
|
+
const CHANNEL_TOOL_DEFS = [
|
|
385
|
+
{
|
|
386
|
+
name: "pending",
|
|
387
|
+
description:
|
|
388
|
+
"How many inbound messages are waiting on this channel + a preview of each (id + a short text snippet). Use it to decide whether to pull. Read-only — claims nothing.",
|
|
389
|
+
inputSchema: { type: "object" as const, properties: {}, required: [] as string[] },
|
|
390
|
+
},
|
|
391
|
+
{
|
|
392
|
+
name: "next-message",
|
|
393
|
+
description:
|
|
394
|
+
"Claim the OLDEST waiting message and return it: { id, text, inReplyTo, systemPrompt }. Marks it in-flight so another connected session won't also handle it. Treat the returned systemPrompt as your instructions for the reply (it is the agent's persona). Returns nothing-to-do when the queue is empty. Pass the returned id back as `reply`'s inReplyTo.",
|
|
395
|
+
inputSchema: { type: "object" as const, properties: {}, required: [] as string[] },
|
|
396
|
+
},
|
|
397
|
+
{
|
|
398
|
+
name: "reply",
|
|
399
|
+
description:
|
|
400
|
+
"Send your answer back to the human AND mark the claimed message handled. inReplyTo is the id you got from next-message (threads the reply); text is your answer. Writes a durable outbound note that shows in the chat UI.",
|
|
401
|
+
inputSchema: {
|
|
402
|
+
type: "object" as const,
|
|
403
|
+
properties: {
|
|
404
|
+
inReplyTo: {
|
|
405
|
+
type: "string",
|
|
406
|
+
description: "The message id you claimed via next-message (threads the reply + marks it handled).",
|
|
407
|
+
},
|
|
408
|
+
text: { type: "string", description: "Your reply text." },
|
|
409
|
+
},
|
|
410
|
+
required: ["text"] as string[],
|
|
411
|
+
},
|
|
412
|
+
},
|
|
413
|
+
{
|
|
414
|
+
name: "release",
|
|
415
|
+
description:
|
|
416
|
+
"Un-claim a message you claimed but won't handle — returns it to the waiting queue (status pending) for another session or your next pass. id is the message id from next-message.",
|
|
417
|
+
inputSchema: {
|
|
418
|
+
type: "object" as const,
|
|
419
|
+
properties: {
|
|
420
|
+
id: { type: "string", description: "The message id to release back to pending." },
|
|
421
|
+
},
|
|
422
|
+
required: ["id"] as string[],
|
|
423
|
+
},
|
|
424
|
+
},
|
|
425
|
+
];
|
|
426
|
+
|
|
427
|
+
/** Attached-backend tools that mutate the queue/write outbound require agent:write
|
|
428
|
+
* (`pending` + `next-message`... next-message claims, so it mutates → write; pending
|
|
429
|
+
* is read-only). reply + release + next-message mutate; pending is read. */
|
|
430
|
+
const CHANNEL_WRITE_TOOLS = new Set(["next-message", "reply", "release"]);
|
|
431
|
+
|
|
432
|
+
/**
|
|
433
|
+
* Dispatch one ATTACHED-backend tool call to the {@link AttachedQueueRegistry},
|
|
434
|
+
* enforcing write scope on the mutating tools. Returns a tool-error result (not a
|
|
435
|
+
* throw) when no attached-backend agent is registered for `channel` — so a session that
|
|
436
|
+
* connected to a non-attached channel and called these tools gets a clean "not an attached
|
|
437
|
+
* agent" message rather than a crash. Pure over its inputs (the daemon's per-session
|
|
438
|
+
* handler + the unit tests both call it).
|
|
439
|
+
*/
|
|
440
|
+
export async function dispatchChannelTool(
|
|
441
|
+
channel: string,
|
|
442
|
+
attachedQueue: AttachedQueueRegistry,
|
|
443
|
+
scopes: string[],
|
|
444
|
+
name: string,
|
|
445
|
+
args: Record<string, unknown>,
|
|
446
|
+
): Promise<ToolResult> {
|
|
447
|
+
// Gate cleanly for a non-attached channel — these tools are meaningful only when an
|
|
448
|
+
// attached-backend agent is registered. (The daemon also only serves CHANNEL_TOOL_DEFS
|
|
449
|
+
// when the agent is registered, so a well-behaved client never reaches here for a
|
|
450
|
+
// non-attached channel — but a hand-crafted call should fail gracefully, not 500.)
|
|
451
|
+
if (!attachedQueue.hasChannel(channel)) {
|
|
452
|
+
return {
|
|
453
|
+
content: [{ type: "text", text: `channel "${channel}" has no attached-backend agent — the pull/reply tools are not available here` }],
|
|
454
|
+
isError: true,
|
|
455
|
+
};
|
|
456
|
+
}
|
|
457
|
+
if (CHANNEL_WRITE_TOOLS.has(name) && !grantsScope(scopes, SCOPE_WRITE)) {
|
|
458
|
+
return {
|
|
459
|
+
content: [
|
|
460
|
+
{
|
|
461
|
+
type: "text",
|
|
462
|
+
text: `tool "${name}" requires the ${SCOPE_WRITE} scope, which this connection's token lacks`,
|
|
463
|
+
},
|
|
464
|
+
],
|
|
465
|
+
isError: true,
|
|
466
|
+
};
|
|
467
|
+
}
|
|
468
|
+
try {
|
|
469
|
+
switch (name) {
|
|
470
|
+
case "pending": {
|
|
471
|
+
const view = await attachedQueue.pending(channel);
|
|
472
|
+
return { content: [{ type: "text", text: JSON.stringify(view) }] };
|
|
473
|
+
}
|
|
474
|
+
case "next-message": {
|
|
475
|
+
const claimed = await attachedQueue.claimNext(channel);
|
|
476
|
+
if (!claimed) {
|
|
477
|
+
return { content: [{ type: "text", text: JSON.stringify({ message: null, note: "no pending messages" }) }] };
|
|
478
|
+
}
|
|
479
|
+
return { content: [{ type: "text", text: JSON.stringify(claimed) }] };
|
|
480
|
+
}
|
|
481
|
+
case "reply": {
|
|
482
|
+
const text = typeof args.text === "string" ? args.text : "";
|
|
483
|
+
const inReplyTo = typeof args.inReplyTo === "string" ? args.inReplyTo : undefined;
|
|
484
|
+
const sent = await attachedQueue.reply(channel, { text, ...(inReplyTo ? { inReplyTo } : {}) });
|
|
485
|
+
const ids = sent.sent;
|
|
486
|
+
return {
|
|
487
|
+
content: [{ type: "text", text: `replied + marked handled (outbound id: ${ids.join(", ")})` }],
|
|
488
|
+
};
|
|
489
|
+
}
|
|
490
|
+
case "release": {
|
|
491
|
+
const id = typeof args.id === "string" ? args.id : "";
|
|
492
|
+
if (!id) {
|
|
493
|
+
return { content: [{ type: "text", text: "release requires an id" }], isError: true };
|
|
494
|
+
}
|
|
495
|
+
await attachedQueue.release(channel, id);
|
|
496
|
+
return { content: [{ type: "text", text: `released ${id} back to pending` }] };
|
|
497
|
+
}
|
|
498
|
+
default:
|
|
499
|
+
return { content: [{ type: "text", text: `unknown channel tool: ${name}` }], isError: true };
|
|
500
|
+
}
|
|
501
|
+
} catch (err) {
|
|
502
|
+
return {
|
|
503
|
+
content: [{ type: "text", text: `${name} failed: ${err instanceof Error ? err.message : String(err)}` }],
|
|
504
|
+
isError: true,
|
|
505
|
+
};
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
/** Fold a top-level chat_id into meta, mirroring the daemon's mergeMeta. */
|
|
510
|
+
function mergeMeta(args: Record<string, unknown>): Record<string, string> {
|
|
511
|
+
const meta: Record<string, string> = {};
|
|
512
|
+
if (typeof args.chat_id === "string") meta.chat_id = args.chat_id;
|
|
513
|
+
return meta;
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
/**
|
|
517
|
+
* Build the per-session MCP Server. Tool handlers dispatch to `transport` — the
|
|
518
|
+
* SAME transport methods the daemon's `/api/*` handlers call — and check write
|
|
519
|
+
* scope against `session.scopes` (mutated by the caller after construction so
|
|
520
|
+
* the closure reads the live value). `channel` is threaded so outbound args
|
|
521
|
+
* carry the right channel context.
|
|
522
|
+
*/
|
|
523
|
+
function buildServer(
|
|
524
|
+
channel: string,
|
|
525
|
+
transport: Transport,
|
|
526
|
+
session: McpSession,
|
|
527
|
+
attachedQueue?: AttachedQueueRegistry,
|
|
528
|
+
): Server {
|
|
529
|
+
// ── ATTACHED-BACKEND FORK (design 2026-06-18). When a `backend:attached` agent is
|
|
530
|
+
// registered for this channel, serve the PULL/REPLY surface (pending / next-message
|
|
531
|
+
// / reply / release) + its reinforcing INSTRUCTIONS, dispatched to the
|
|
532
|
+
// AttachedQueueRegistry. Otherwise serve the existing push surface (reply / react /
|
|
533
|
+
// edit / download), dispatched to the transport. Resolved at connect time — a
|
|
534
|
+
// channel doesn't switch backends under a live session.
|
|
535
|
+
const isAttachedBackend = !!attachedQueue?.hasChannel(channel);
|
|
536
|
+
const server = new Server(
|
|
537
|
+
// Per-channel name (`agent-<name>`) so it reads clearly in `/mcp` and lines
|
|
538
|
+
// up with the `--dangerously-load-development-channels=server:agent-<name>`
|
|
539
|
+
// flag + the `claude mcp add agent-<name>` name the setup UI/launcher use.
|
|
540
|
+
{ name: `agent-${channel}`, version: "0.1.0" },
|
|
541
|
+
{
|
|
542
|
+
capabilities: {
|
|
543
|
+
experimental: {
|
|
544
|
+
"claude/agent": {},
|
|
545
|
+
"claude/agent/permission": {},
|
|
546
|
+
},
|
|
547
|
+
tools: {},
|
|
548
|
+
},
|
|
549
|
+
instructions: isAttachedBackend ? CHANNEL_INSTRUCTIONS : INSTRUCTIONS,
|
|
550
|
+
},
|
|
551
|
+
);
|
|
552
|
+
|
|
553
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
554
|
+
tools: isAttachedBackend ? CHANNEL_TOOL_DEFS : TOOL_DEFS,
|
|
555
|
+
}));
|
|
556
|
+
|
|
557
|
+
// Dispatch reads `session.scopes` live (the daemon refreshes it per request),
|
|
558
|
+
// so a re-auth with a narrower token takes effect on the next tool call.
|
|
559
|
+
server.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
560
|
+
const args = (req.params.arguments ?? {}) as Record<string, unknown>;
|
|
561
|
+
const result =
|
|
562
|
+
isAttachedBackend && attachedQueue
|
|
563
|
+
? await dispatchChannelTool(channel, attachedQueue, session.scopes, req.params.name, args)
|
|
564
|
+
: await dispatchTool(channel, transport, session.scopes, req.params.name, args);
|
|
565
|
+
// Our ToolResult is the content/isError subset of the SDK's CallToolResult
|
|
566
|
+
// (which also has a task-variant we never emit); return it as that shape.
|
|
567
|
+
return result as { content: Array<{ type: "text"; text: string }>; isError?: boolean };
|
|
568
|
+
});
|
|
569
|
+
|
|
570
|
+
return server;
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
/** A tool-call result in the MCP content shape. */
|
|
574
|
+
export interface ToolResult {
|
|
575
|
+
content: Array<{ type: "text"; text: string }>;
|
|
576
|
+
isError?: boolean;
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
/**
|
|
580
|
+
* Dispatch one tool call to the channel's transport, enforcing write scope on
|
|
581
|
+
* the mutating tools (reply/react/edit) from the connection's `scopes`. Pure
|
|
582
|
+
* over its inputs — the daemon's per-session handler and the unit tests both
|
|
583
|
+
* call it, so tool behavior is asserted without standing up an MCP transport.
|
|
584
|
+
*/
|
|
585
|
+
export async function dispatchTool(
|
|
586
|
+
channel: string,
|
|
587
|
+
transport: Transport,
|
|
588
|
+
scopes: string[],
|
|
589
|
+
name: string,
|
|
590
|
+
args: Record<string, unknown>,
|
|
591
|
+
): Promise<ToolResult> {
|
|
592
|
+
// Read-only tokens connect + receive the wake, but cannot send. Enforce
|
|
593
|
+
// agent:write on the mutating tools using the connection's own scopes.
|
|
594
|
+
// Dual-accept (channel→agent rename): a pre-rename token carrying the legacy
|
|
595
|
+
// `channel:write` scope also authorizes — `grantsScope` matches agent:write OR
|
|
596
|
+
// its channel:write alias — so HTTP-MCP sends keep working until tokens re-mint.
|
|
597
|
+
if (WRITE_TOOLS.has(name) && !grantsScope(scopes, SCOPE_WRITE)) {
|
|
598
|
+
return {
|
|
599
|
+
content: [
|
|
600
|
+
{
|
|
601
|
+
type: "text",
|
|
602
|
+
text: `tool "${name}" requires the ${SCOPE_WRITE} scope, which this connection's token lacks`,
|
|
603
|
+
},
|
|
604
|
+
],
|
|
605
|
+
isError: true,
|
|
606
|
+
};
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
try {
|
|
610
|
+
switch (name) {
|
|
611
|
+
case "reply": {
|
|
612
|
+
const replyArgs: ReplyArgs = {
|
|
613
|
+
channel,
|
|
614
|
+
text: typeof args.text === "string" ? args.text : undefined,
|
|
615
|
+
files: Array.isArray(args.files) ? (args.files as string[]) : undefined,
|
|
616
|
+
reply_to: typeof args.reply_to === "string" ? args.reply_to : undefined,
|
|
617
|
+
meta: mergeMeta(args),
|
|
618
|
+
};
|
|
619
|
+
const result = await transport.reply(replyArgs);
|
|
620
|
+
const ids = result.sent;
|
|
621
|
+
const parts = ids.length === 1 ? `(id: ${ids[0]})` : `(ids: ${ids.join(", ")})`;
|
|
622
|
+
return {
|
|
623
|
+
content: [{ type: "text", text: `sent ${ids.length} part${ids.length === 1 ? "" : "s"} ${parts}` }],
|
|
624
|
+
};
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
case "react": {
|
|
628
|
+
if (!transport.react) return methodMissing(channel, transport, "react");
|
|
629
|
+
const reactArgs: ReactArgs = {
|
|
630
|
+
channel,
|
|
631
|
+
message_id: String(args.message_id ?? ""),
|
|
632
|
+
emoji: String(args.emoji ?? ""),
|
|
633
|
+
meta: mergeMeta(args),
|
|
634
|
+
};
|
|
635
|
+
await transport.react(reactArgs);
|
|
636
|
+
return { content: [{ type: "text", text: "reacted" }] };
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
case "edit_message": {
|
|
640
|
+
if (!transport.edit) return methodMissing(channel, transport, "edit");
|
|
641
|
+
const editArgs: EditArgs = {
|
|
642
|
+
channel,
|
|
643
|
+
message_id: String(args.message_id ?? ""),
|
|
644
|
+
text: String(args.text ?? ""),
|
|
645
|
+
meta: mergeMeta(args),
|
|
646
|
+
};
|
|
647
|
+
await transport.edit(editArgs);
|
|
648
|
+
return { content: [{ type: "text", text: "edited" }] };
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
case "download_attachment": {
|
|
652
|
+
if (!transport.download) return methodMissing(channel, transport, "download");
|
|
653
|
+
const dlArgs: DownloadArgs = { channel, file_id: String(args.file_id ?? "") };
|
|
654
|
+
const result = await transport.download(dlArgs);
|
|
655
|
+
return { content: [{ type: "text", text: result.path }] };
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
default:
|
|
659
|
+
return { content: [{ type: "text", text: `unknown tool: ${name}` }], isError: true };
|
|
660
|
+
}
|
|
661
|
+
} catch (err) {
|
|
662
|
+
return {
|
|
663
|
+
content: [{ type: "text", text: `${name} failed: ${err instanceof Error ? err.message : String(err)}` }],
|
|
664
|
+
isError: true,
|
|
665
|
+
};
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
/** Thin alias the reply-dispatch tests read against. */
|
|
670
|
+
export function callReplyTool(
|
|
671
|
+
channel: string,
|
|
672
|
+
transport: Transport,
|
|
673
|
+
scopes: string[],
|
|
674
|
+
args: Record<string, unknown>,
|
|
675
|
+
): Promise<ToolResult> {
|
|
676
|
+
return dispatchTool(channel, transport, scopes, "reply", args);
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
function methodMissing(channel: string, transport: Transport, method: string): ToolResult {
|
|
680
|
+
return {
|
|
681
|
+
content: [
|
|
682
|
+
{
|
|
683
|
+
type: "text",
|
|
684
|
+
text: `transport "${transport.kind}" for channel "${channel}" does not support ${method}`,
|
|
685
|
+
},
|
|
686
|
+
],
|
|
687
|
+
isError: true,
|
|
688
|
+
};
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
// ---------------------------------------------------------------------------
|
|
692
|
+
// HTTP entry — the daemon routes POST/GET/DELETE /mcp/<channel> here
|
|
693
|
+
// ---------------------------------------------------------------------------
|
|
694
|
+
|
|
695
|
+
/**
|
|
696
|
+
* Handle a stateful Streamable HTTP MCP request for `channel`, dispatching tool
|
|
697
|
+
* calls to `transport`. The daemon has ALREADY validated `agent:read` and
|
|
698
|
+
* passes the caller's scopes in (threaded onto the session for write-tool
|
|
699
|
+
* checks). Returns the transport's Response (JSON or an SSE stream).
|
|
700
|
+
*
|
|
701
|
+
* Session resolution mirrors the probe:
|
|
702
|
+
* - existing mcp-session-id → reuse its transport.
|
|
703
|
+
* - POST with no session id → a NEW session (initialize handshake).
|
|
704
|
+
* - anything else with no/unknown session id → 400 (no session to attach to).
|
|
705
|
+
*/
|
|
706
|
+
export async function handleMcp(
|
|
707
|
+
req: Request,
|
|
708
|
+
channel: string,
|
|
709
|
+
transport: Transport,
|
|
710
|
+
scopes: string[],
|
|
711
|
+
attachedQueue?: AttachedQueueRegistry,
|
|
712
|
+
): Promise<Response> {
|
|
713
|
+
const sid = req.headers.get("mcp-session-id");
|
|
714
|
+
|
|
715
|
+
if (sid && sessionsById.has(sid)) {
|
|
716
|
+
const existing = sessionsById.get(sid)!;
|
|
717
|
+
// Refresh the connection's scopes from the presented token each request, so
|
|
718
|
+
// a re-auth with a narrower/wider token takes effect (the daemon re-validates
|
|
719
|
+
// agent:read on every call before reaching here).
|
|
720
|
+
existing.scopes = scopes;
|
|
721
|
+
// A GET opens (or reopens) the standalone SSE push stream for the live wake
|
|
722
|
+
// (`pushToChannel`). The deaf-on-restart BACKLOG REPLAY that used to fire here
|
|
723
|
+
// was retired with the interactive backend (design
|
|
724
|
+
// 2026-06-19-retire-interactive-backend.md): the programmatic path runs
|
|
725
|
+
// synchronously and the channel path uses the durable note-claim queue, so
|
|
726
|
+
// there's no missed-while-idle backlog to replay onto a reconnecting session.
|
|
727
|
+
return existing.transport.handleRequest(req);
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
if (req.method === "DELETE") {
|
|
731
|
+
// No session to delete — treat as a no-op success.
|
|
732
|
+
return new Response(null, { status: 204 });
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
if (req.method !== "POST") {
|
|
736
|
+
return new Response(
|
|
737
|
+
JSON.stringify({ jsonrpc: "2.0", error: { code: -32000, message: "No valid session" }, id: null }),
|
|
738
|
+
{ status: 400, headers: { "content-type": "application/json" } },
|
|
739
|
+
);
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
// New session — build the transport + server, register on init.
|
|
743
|
+
const session: McpSession = {
|
|
744
|
+
// Placeholders replaced immediately below; the object identity is what the
|
|
745
|
+
// registry + tool closures capture.
|
|
746
|
+
server: undefined as unknown as Server,
|
|
747
|
+
transport: undefined as unknown as WebStandardStreamableHTTPServerTransport,
|
|
748
|
+
scopes,
|
|
749
|
+
};
|
|
750
|
+
|
|
751
|
+
const httpTransport = new WebStandardStreamableHTTPServerTransport({
|
|
752
|
+
sessionIdGenerator: () => crypto.randomUUID(),
|
|
753
|
+
enableJsonResponse: false,
|
|
754
|
+
onsessioninitialized: (id: string) => {
|
|
755
|
+
registerSession(channel, id, session);
|
|
756
|
+
},
|
|
757
|
+
});
|
|
758
|
+
|
|
759
|
+
const server = buildServer(channel, transport, session, attachedQueue);
|
|
760
|
+
session.server = server;
|
|
761
|
+
session.transport = httpTransport;
|
|
762
|
+
|
|
763
|
+
// Clean up on transport close (client disconnect / DELETE / stream end).
|
|
764
|
+
httpTransport.onclose = () => {
|
|
765
|
+
const id = httpTransport.sessionId;
|
|
766
|
+
if (id) unregisterSession(channel, id);
|
|
767
|
+
};
|
|
768
|
+
|
|
769
|
+
await server.connect(httpTransport);
|
|
770
|
+
return httpTransport.handleRequest(req);
|
|
771
|
+
}
|