feat(backend): platform server linking API for multi-platform CoPilot - #12615
Conversation
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
WalkthroughThis PR introduces platform bot account linking infrastructure with database models, API endpoints, and authentication to enable external platforms (Discord, Telegram, Slack, Teams, WhatsApp, GitHub, Linear) to establish verified connections with AutoGPT users and initiate CoPilot chat sessions through a token-based confirmation flow. Changes
Sequence DiagramssequenceDiagram
participant Bot as External Bot Platform
participant API as Platform Linking API
participant DB as Database
participant User as AutoGPT User
rect rgba(100, 200, 100, 0.5)
Note over Bot,User: Token-Based Linking Flow
Bot->>API: POST /tokens (platform, platform_server_id)
activate API
API->>API: Authenticate (X-Bot-API-Key)
API->>DB: Invalidate prior unused tokens
API->>DB: Create PlatformLinkToken (30min expiry)
API-->>Bot: LinkTokenResponse (token, link_url)
deactivate API
Bot->>User: Send link_url (embedded in user message)
activate User
User->>API: GET /tokens/{token}/info
API->>DB: Fetch PlatformLinkToken
API-->>User: LinkTokenInfoResponse (platform, link_type)
User->>API: POST /tokens/{token}/confirm (JWT auth)
activate API
API->>DB: Verify token unused & unexpired
API->>DB: Check no existing link for platform_server_id
API->>DB: Atomically update token.usedAt & create PlatformLink
API-->>User: ConfirmLinkResponse (success)
deactivate API
deactivate User
end
sequenceDiagram
participant Bot as External Bot Platform
participant API as Chat Proxy API
participant DB as Database
participant CoPilot as CoPilot Service
rect rgba(100, 150, 200, 0.5)
Note over Bot,CoPilot: Bot-Driven Chat Session & Streaming
Bot->>API: POST /chat/session (platform, platform_user_id, X-Bot-API-Key)
activate API
API->>API: Authenticate X-Bot-API-Key
API->>DB: find_user_link(platform, platform_user_id)
API->>CoPilot: create_chat_session(owner_user_id)
API-->>Bot: BotChatSessionResponse (session_id)
deactivate API
Bot->>API: POST /chat/stream (platform_user_id, session_id, message, X-Bot-API-Key)
activate API
API->>API: Authenticate & resolve owner_user_id
API->>DB: get_chat_session(session_id, owner_user_id)
API->>API: Create StreamingResponse (SSE)
API-->>Bot: Stream response header
loop SSE Stream Chunks
API->>CoPilot: subscribe_to_session()
CoPilot-->>API: Stream chunk
API-->>Bot: SSE event data
Note over API,Bot: Keepalive sent every 30s (no data)
end
CoPilot-->>API: StreamFinish or [DONE]
API->>API: Unsubscribe from stream
API-->>Bot: SSE [DONE]
deactivate API
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
Suggested labels
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
/review |
Multi-platform bot service that deploys CoPilot to Discord, Telegram, and Slack from a single codebase using Vercel's Chat SDK. ## What's included ### Core bot (src/bot.ts) - Chat SDK instance with dynamic adapter loading - onNewMention: resolves platform user → AutoGPT account - Unlinked users get a link prompt via the platform-linking API - Subscribed message handler with state management - MVP echo response (CoPilot API integration next) ### Platform API client (src/platform-api.ts) - Calls /api/platform-linking/resolve on every message - Creates link tokens for unlinked users - Checks link token status - Chat session creation and SSE streaming (prepared for CoPilot) ### Serverless routes (src/api/) - POST /api/webhooks/discord — Discord interactions endpoint - POST /api/webhooks/telegram — Telegram updates - POST /api/webhooks/slack — Slack events - GET /api/gateway/discord — Gateway cron for Discord messages ### Standalone mode (src/index.ts) - Long-running process for Docker/PM2 deployment - Auto-detects enabled adapters from env vars - Redis or in-memory state ## Stacked on - feat/platform-bot-linking (PR #12615)
There was a problem hiding this comment.
All 8 specialists have reported. Now I have everything I need. Let me compile the final verdict.
PR #12615 — feat: Platform bot linking API for multi-platform CoPilot
Author: Bentlybro | Requested by: ntindle | Files: routes.py (+381), routes_test.py (+25), rest_api.py (+6), migration.sql (+47), schema.prisma (+48), openapi.json (+212)
🎯 Verdict: REQUEST_CHANGES
What This PR Does
Adds a new backend subsystem for linking external chat platform identities (Discord, Telegram, Slack, etc.) to AutoGPT user accounts. This enables a multi-platform CoPilot bot to know which AutoGPT account a chat user belongs to. The flow: bot creates a one-time link token → sends user a URL → user clicks it, logs in, and confirms → the platform identity is now mapped to their account. The PR includes API routes, Prisma schema/migration, and OpenAPI spec updates. No frontend UI is included — that's planned as a follow-up.
Specialist Findings
🛡️ Security 🔴 — Three bot-facing endpoints (POST /tokens, GET /tokens/{token}/status, POST /resolve) ship with zero authentication. The code has a TODO acknowledging this (routes.py:103-108). Any internet caller can: (1) flood the token table via POST /tokens (no rate limiting, no cap per user), (2) enumerate linked users via POST /resolve by probing platform IDs — leaking internal user_id and platform_username, (3) poll any token's status and extract the linked user_id. Additionally, confirm_link_token (routes.py:210-263) performs 5 separate DB calls with no transaction — a race condition on double-click can trigger an unhandled unique constraint violation (500 error instead of clean 409).
- 🔴
routes.py:98-108— Unauthenticated bot endpoints: must add API key/service token auth before merge - 🔴
routes.py:210-263— Race condition in confirm flow: no transaction wrapping check-and-consume - 🟠
routes.py:181-195—/resolveleaksuser_idto unauthenticated callers - 🟠
routes.py:158-160—/tokens/{token}/statusleaksuser_idwithout auth - 🟠
routes.py:98-140— Token flooding: unlimited pending tokens per platform user, no rate limit
🏗️ Architecture api/features/<name>/ convention correctly. Router registration is consistent. However: (1) Enum duplication — PlatformType is defined in Prisma schema AND as a Python VALID_PLATFORMS set (routes.py:34-41); these will drift when someone adds a platform to Prisma without updating the Python set. Fix: import from prisma.enums. (2) No service layer — other features (otto, mcp) separate routes from data access; this puts Prisma calls directly in handlers. (3) No expired token cleanup mechanism despite having an expiresAt index. (4) Hardcoded link_url base URL (routes.py:139) breaks staging/self-hosted. (5) Redundant @@index([token]) when @unique already creates an index on PlatformLinkToken.token.
⚠️ routes.py:34-41/schema.prisma— Enum duplication will drift⚠️ routes.py:139— Hardcodedhttps://platform.agpt.cobase URL⚠️ schema.prisma:1349— Redundant index ontoken(unique already indexes)
⚡ Performance ✅ — All endpoints are O(1) indexed lookups, which is correct. The hot path (POST /resolve, called on every bot message) hits the composite unique index — efficient. Concerns: (1) confirm_link_token does 4 DB round trips without a transaction (also a correctness issue). (2) POST /tokens does a check-then-create (2 round trips) that could be a single upsert. (3) Redundant @@index([token]) doubles write cost on that column. (4) Token table grows unbounded — no TTL/cleanup.
⚠️ schema.prisma:1349— Remove redundant@@index([token])⚠️ No token cleanup job — table grows forever
🧪 Testing 🔴 — Only 25 lines of tests covering a single trivial helper (_validate_platform). Zero endpoint tests across all 6 API routes. The codebase has well-established patterns (TestClient + mock_jwt_user fixture + mocked Prisma) in sibling features (chat/routes_test.py, otto/routes_test.py, library/routes_test.py). Missing: happy-path linking flow, expired/used token handling, duplicate link prevention (409s), ownership checks on DELETE, auth enforcement on user-facing endpoints, race condition handling on concurrent confirm. Estimated coverage: ~5% of new code.
- 🔴
routes_test.py— Only tests_validate_platform, zero endpoint coverage - Missing: ~20+ test cases for the 6 endpoints following existing codebase patterns
📖 Quality LinkTokenStatusResponse.status is bare str — should be Literal["pending", "linked", "expired"] (routes.py:67). (2) delete_link returns dict instead of a response model (routes.py:310). (3) platform fields across all models are bare str — should use a shared Literal/enum type. (4) No max_length/min_length on platform_user_id (routes.py:54). (5) Logger uses f-strings instead of lazy %s formatting. (6) OpenAPI operation IDs are auto-generated and ugly (e.g., "getPlatform-linkingCheck if a link token has been consumed").
⚠️ routes.py:67—status: str→ useLiteral⚠️ routes.py:310—-> dict→ proper response model⚠️ routes.py:54— No input length validation onplatform_user_id
📦 Product link_url points to https://platform.agpt.co/link/{token} which returns 404 because no frontend /link/{token} page exists yet. Error messages are developer-speak, not user-friendly (e.g., exposing internal IDs in 409 responses). The channelId field is stored but never used — hints at an unimplemented callback mechanism. Token expiry of 30 minutes is reasonable but tight for mobile users. No notification after linking completes — the bot must poll.
⚠️ Frontend/link/{token}page missing — link is a dead end today⚠️ routes.py:120— Error messages expose internal platform user IDs⚠️ routes.py:63—channelIdstored but never used (dead field)
📬 Discussion ✅ — CI passing (25/26 checks, only cosmetic skips). No human reviews submitted yet (reviewDecision: REVIEW_REQUIRED). CodeRabbit skipped (detected draft). Author's PR description explicitly lists follow-up TODOs: API key auth, frontend page, bot service, token cleanup, rate limiting. No linked tracking issues.
🔎 QA ✅ — All new API endpoints are functional and accessible. Live testing confirmed:
POST /resolvereturns correct responses (200 for valid, 422 for invalid input, 400 for bad platform)POST /tokenscreates tokens with properlink_urlandexpires_atGET /tokens/{token}/statusreturnspending/404correctly- Auth-protected endpoints (
GET /links,POST /confirm,DELETE /links/{id}) properly return 401 when unauthenticated - Frontend is completely unaffected — all pages (landing, copilot, library, build, marketplace) load correctly
- No console errors related to this PR
Blockers (Must Fix)
-
routes.py:98-108— Bot-facing endpoints have NO authentication.POST /tokens,GET /tokens/{token}/status, andPOST /resolveare completely open. Anyone can create tokens, enumerate linked users, and extract internal user IDs. TheTODOcomment is not an acceptable substitute. Add API key auth before merge. (Flagged by: Security 🔴, Architecture, Product, Discussion) -
routes.py:210-263— Race condition inconfirm_link_token. Five separate DB calls with no transaction. Concurrent requests can create duplicate links; the unique constraint violation surfaces as an unhandled 500 error. Wrap in a Prisma transaction or use atomic conditional update. (Flagged by: Security 🔴, Performance, Architecture) -
routes_test.py— Virtually no test coverage. Only 3 tests for a trivial helper. Zero endpoint tests across 6 routes and 381 lines of new code. The codebase has clear testing patterns that were not followed. Add at minimum: happy-path linking flow, expired/used token handling, duplicate prevention, auth enforcement. (Flagged by: Testing 🔴)
Should Fix (Follow-up OK)
routes.py:34-41/schema.prisma— Enum duplication.VALID_PLATFORMSPython set will drift fromPlatformTypePrisma enum. Import fromprisma.enumsinstead. (Architecture, Quality)routes.py:139— Hardcoded base URL.https://platform.agpt.co/link/{token}should be configurable via env var. Breaks staging/dev/self-hosted. (Architecture, Product)schema.prisma:1349— Redundant@@index([token]). The@uniquealready creates an index. Remove to avoid doubled write cost. (Performance, Architecture)routes.py:67— Loose typing.status: strshould beLiteral["pending", "linked", "expired"]. Similarly,platformfields across models should use a shared type. (Quality)routes.py:310—delete_linkreturnsdict. Only endpoint without a proper response model. DefineDeleteLinkResponse. (Quality)- Token table cleanup —
PlatformLinkTokenrows accumulate forever. Add a periodic cleanup job. (Architecture, Performance) routes.py:54— No input length validation onplatform_user_id. A trivially long string would be persisted. AddField(max_length=255, min_length=1). (Quality)routes.py:98-140— Token flooding. No cap on pending tokens per(platform, platformUserId). Limit to 1 active token per identity. (Security, Performance)
Risk Assessment
Merge risk: HIGH | Rollback: EASY (new feature, no changes to existing tables/routes)
The core design is sound and the implementation is clean, but the three blockers — unauthenticated security-sensitive endpoints, a race condition, and near-zero test coverage — must be resolved before this merges. The feature is additive with easy rollback (drop the migration, remove the router), which reduces deployment risk.
REVIEW_COMPLETE
PR: #12615
Verdict: REQUEST_CHANGES
Blockers: 3
Adds the user-facing page that completes the platform bot linking flow. When an unlinked user messages the bot, they get a URL like: https://platform.agpt.co/link/{token} This page: 1. Validates the token (expired? already used?) 2. If user isn't logged in → redirects to login with ?next=/link/{token} 3. Shows a confirmation screen: 'Link your [platform] account to AutoGPT' 4. On click → calls POST /api/platform-linking/tokens/{token}/confirm 5. Shows success or error state ## Implementation - Lives in (no-navbar) route group (standalone page, no main nav) - Reuses AuthCard, Button, Text, Link components from existing auth pages - Same visual style as login/signup pages - Handles all edge cases: expired token, already linked, not authenticated ## Stacked on - feat/copilot-bot-service (PR #12618) - feat/platform-bot-linking (PR #12615)
Multi-platform bot service that deploys CoPilot to Discord, Telegram, and Slack from a single codebase using Vercel's Chat SDK. ## What's included ### Core bot (src/bot.ts) - Chat SDK instance with dynamic adapter loading - onNewMention: resolves platform user → AutoGPT account - Unlinked users get a link prompt via the platform-linking API - Subscribed message handler with state management - MVP echo response (CoPilot API integration next) ### Platform API client (src/platform-api.ts) - Calls /api/platform-linking/resolve on every message - Creates link tokens for unlinked users - Checks link token status - Chat session creation and SSE streaming (prepared for CoPilot) ### Serverless routes (src/api/) - POST /api/webhooks/discord — Discord interactions endpoint - POST /api/webhooks/telegram — Telegram updates - POST /api/webhooks/slack — Slack events - GET /api/gateway/discord — Gateway cron for Discord messages ### Standalone mode (src/index.ts) - Long-running process for Docker/PM2 deployment - Auto-detects enabled adapters from env vars - Redis or in-memory state ## Stacked on - feat/platform-bot-linking (PR #12615)
Adds the user-facing page that completes the platform bot linking flow. When an unlinked user messages the bot, they get a URL like: https://platform.agpt.co/link/{token} This page: 1. Validates the token (expired? already used?) 2. If user isn't logged in → redirects to login with ?next=/link/{token} 3. Shows a confirmation screen: 'Link your [platform] account to AutoGPT' 4. On click → calls POST /api/platform-linking/tokens/{token}/confirm 5. Shows success or error state ## Implementation - Lives in (no-navbar) route group (standalone page, no main nav) - Reuses AuthCard, Button, Text, Link components from existing auth pages - Same visual style as login/signup pages - Handles all edge cases: expired token, already linked, not authenticated ## Stacked on - feat/copilot-bot-service (PR #12618) - feat/platform-bot-linking (PR #12615)
|
/review |
There was a problem hiding this comment.
All 8 specialists have reported. Compiling the final verdict now.
PR #12615 — feat: Platform bot linking API for multi-platform CoPilot
Author: Bentlybro | Requested by: ntindle | Files: routes.py (+442), routes_test.py (+136), rest_api.py (+6), schema.prisma (+50), migration.sql (+44), openapi.json (+236)
🎯 Verdict: REQUEST_CHANGES
What This PR Does
Adds a backend API for linking external chat platform identities (Discord, Telegram, Slack, etc.) to AutoGPT user accounts. A bot creates a one-time link token, the user clicks it to log in and confirm, and the bot can then resolve the user's identity on subsequent messages. The Prisma schema, migration, and OpenAPI spec are all included. No frontend UI yet (backend-only first PR).
Specialist Findings
🛡️ Security
routes.py:74 — API key comparison uses Python != (not constant-time). Must use hmac.compare_digest() to prevent timing side-channel attacks on the bot API key.
routes.py:56-68 — verify_bot_api_key is dead code with a stub body (# Check header → return None). Never called by any endpoint, but if someone wires it up by mistake, it would pass all requests. Remove entirely.
routes.py:60-66, 71-73 — Default deployment (BOT_API_KEY="", ENV=development) has zero bot authentication. An attacker could create tokens, resolve identities, and poll status freely. Needs a loud startup warning or require explicit opt-in for dev bypass.
routes.py:275-306 — TOCTOU race between token consumption and link creation. Two concurrent requests with different tokens for the same platform identity could both pass the find_first check; the DB unique constraint catches it but produces an unhandled 500 instead of a clean 409. Wrap in try/except.
🏗️ Architecture ✅ — Clean module structure follows existing backend/api/features/ patterns. Route registration is standard. Dual auth (API key for bot, JWT for user) is well-separated.
routes.py:143-145 — Header extraction via Depends(lambda req: ...) is a hack that breaks FastAPI's dependency injection (see QA finding below). Should be a proper Header() dependency or typed function.
PlatformLinkToken rows accumulate forever. The @@index([expiresAt]) suggests cleanup was planned but never implemented.
Platform enum manually mirrors Prisma PlatformType — adding a platform to one without the other causes runtime failures with no compile-time safety.
⚡ Performance ✅ — Indexes are appropriate for the query patterns. Unique constraint on (platform, platformUserId) covers the hot-path /resolve query.
routes.py:234-255 — /resolve is called on every incoming bot message with no caching. A TTL cache (60-300s) would eliminate most DB hits since link status rarely changes.
PlatformLinkToken(platform, platformUserId) — both create_link_token and confirm_link_token query by this pair.
routes.py:341-363 — list_my_links returns all results unbounded. Add pagination as a guardrail.
routes.py:245, 160, 303 — Uses find_first where find_unique with the compound key would be more efficient and intentional.
🧪 Testing
create_link_token, confirm_link_token, get_link_token_status, resolve_platform_user, list_my_links, delete_link
TestClient — the broken Depends(lambda req: ...) pattern would have been caught immediately by any API-level test
delete_link
routes_test.py:48,83,100 — Class-level imports (from ... import X inside class body, accessed via self.X) are non-idiomatic; use module-level imports
📖 Quality
routes.py:56-70 — Dead verify_bot_api_key function alongside working _check_bot_api_key is confusing. Two auth functions with overlapping names but different approaches.
routes.py:143-145 — # noqa: ARG005 on lambda dependencies signals a code smell — the lambda hack requires lint suppression.
routes.py:49 — LINK_BASE_URL has no trailing-slash guard; f"{LINK_BASE_URL}/{token}" would produce double-slash if configured with trailing /.
"name": "req", "in": "query", "required": true parameters on bot endpoints — a client-facing bug from the lambda Depends pattern.
📦 Product ✅ — The linking flow follows industry-standard patterns (similar to Slack OAuth linking). Token expiry of 30 minutes is reasonable. One-to-many (user → platforms) and one-to-one (platform identity → user) enforcement is correct. Error messages are user-friendly.
channelId is stored in PlatformLinkToken but never used — no completion notification to the user on the platform side after linking. Acceptable as a follow-up.
/link/{token} — users clicking the link will 404. Must be documented as a required follow-up.
platformUsername has no update mechanism — goes stale if user changes their display name. Consider updating on each /resolve call.
📬 Discussion ✅ — PR is in draft state. One bot review (autogpt-pr-reviewer) requested changes with 3 blockers and 8 should-fix items. Bentlybro addressed most in follow-up commits (7bf10c6, 8a91bd8, 781224f). No human reviews yet. CI passing (26/27 checks, one pending/skipped). No merge conflicts.
verify_bot_api_key dead code.
🔎 QA ❌ — CRITICAL BUG: All 3 bot-facing endpoints are non-functional.
🔴 routes.py:143-145, 179-181, 213-215 — Depends(lambda req: req.headers.get("x-bot-api-key")) does not work. FastAPI interprets the untyped req parameter as a required query parameter, not the Request object. Result: 422 "Field required" without ?req=, and 500 'str' object has no attribute 'headers' with it. POST /tokens, GET /tokens/{token}/status, and POST /resolve are all broken.
✅ User-facing endpoints work correctly: POST /tokens/{token}/confirm (401 without JWT, 404 for invalid tokens), GET /links (returns list, 401 without auth), DELETE /links/{id} (404 for nonexistent, 401 without auth)
✅ Frontend unaffected — landing page, login, signup, copilot dashboard, build page all load normally
Blockers (Must Fix)
-
routes.py:143-145, 179-181, 213-215— All bot-facing endpoints are broken. TheDepends(lambda req: req.headers.get("x-bot-api-key"))pattern fails because FastAPI cannot inferreqis theRequest. Fix:from fastapi import Request async def get_bot_api_key(request: Request) -> str | None: return request.headers.get("x-bot-api-key")
Then use
x_bot_api_key: str | None = Depends(get_bot_api_key)on each endpoint. (Flagged by QA ❌, Architecture 🏗️, Quality 📖) -
routes.py:74— API key comparison must usehmac.compare_digest(request_api_key, BOT_API_KEY)instead of!=to prevent timing side-channel attacks. (Flagged by Security 🛡️) -
routes.py:56-68— Remove the deadverify_bot_api_keyfunction entirely. It has a broken stub body and is never called, but could be mistakenly wired up as an auth dependency that passes everything. (Flagged by Security 🛡️, Architecture 🏗️, Quality 📖, Product 📦) -
routes.py:275-306— Wrap thePlatformLink.prisma().create()in a try/except forUniqueConstraintViolationto handle the TOCTOU race gracefully (return 409 instead of 500). (Flagged by Security 🛡️, Architecture 🏗️, Performance ⚡)
Should Fix (Follow-up OK)
routes.py:234-255— Add a TTL cache onresolve_platform_user— this is called on every bot message and hits the DB each time.routes_test.py— Add integration tests for route handlers usingTestClient. The lambda Depends bug would have been caught by any API-level test. At minimum: happy path for create/confirm/resolve/list/delete.migration.sql— Add composite indexPlatformLinkToken(platform, platformUserId)for the queries increate_link_tokenandconfirm_link_token.routes.py:341-363— Add pagination tolist_my_links(take/skip or cursor-based).routes.py:60-66, 71-73— Add startup warning whenBOT_API_KEYis empty, or require explicitDISABLE_BOT_AUTH=truefor dev mode.- Add a periodic cleanup job for expired
PlatformLinkTokenrows. - Document the frontend
/link/{token}route as a required follow-up in the PR description. openapi.json— The spurious"req"query parameter on bot endpoints will confuse API clients and generated SDKs. Resolves automatically when blocker #1 is fixed.
Risk Assessment
Merge risk: HIGH — Core feature endpoints (bot-facing) are non-functional due to the FastAPI dependency injection bug. If merged as-is, no bot could use this API.
Rollback: EASY — New feature with no changes to existing code paths. Drop migration, remove router, done.
REVIEW_COMPLETE
PR: #12615
Verdict: REQUEST_CHANGES
Blockers: 4
Multi-platform bot service that deploys CoPilot to Discord, Telegram, and Slack from a single codebase using Vercel's Chat SDK. ## What's included ### Core bot (src/bot.ts) - Chat SDK instance with dynamic adapter loading - onNewMention: resolves platform user → AutoGPT account - Unlinked users get a link prompt via the platform-linking API - Subscribed message handler with state management - MVP echo response (CoPilot API integration next) ### Platform API client (src/platform-api.ts) - Calls /api/platform-linking/resolve on every message - Creates link tokens for unlinked users - Checks link token status - Chat session creation and SSE streaming (prepared for CoPilot) ### Serverless routes (src/api/) - POST /api/webhooks/discord — Discord interactions endpoint - POST /api/webhooks/telegram — Telegram updates - POST /api/webhooks/slack — Slack events - GET /api/gateway/discord — Gateway cron for Discord messages ### Standalone mode (src/index.ts) - Long-running process for Docker/PM2 deployment - Auto-detects enabled adapters from env vars - Redis or in-memory state ## Stacked on - feat/platform-bot-linking (PR #12615)
Adds the user-facing page that completes the platform bot linking flow. When an unlinked user messages the bot, they get a URL like: https://platform.agpt.co/link/{token} This page: 1. Validates the token (expired? already used?) 2. If user isn't logged in → redirects to login with ?next=/link/{token} 3. Shows a confirmation screen: 'Link your [platform] account to AutoGPT' 4. On click → calls POST /api/platform-linking/tokens/{token}/confirm 5. Shows success or error state ## Implementation - Lives in (no-navbar) route group (standalone page, no main nav) - Reuses AuthCard, Button, Text, Link components from existing auth pages - Same visual style as login/signup pages - Handles all edge cases: expired token, already linked, not authenticated ## Stacked on - feat/copilot-bot-service (PR #12618) - feat/platform-bot-linking (PR #12615)
Multi-platform bot service that deploys CoPilot to Discord, Telegram, and Slack from a single codebase using Vercel's Chat SDK. ## What's included ### Core bot (src/bot.ts) - Chat SDK instance with dynamic adapter loading - onNewMention: resolves platform user → AutoGPT account - Unlinked users get a link prompt via the platform-linking API - Subscribed message handler with state management - MVP echo response (CoPilot API integration next) ### Platform API client (src/platform-api.ts) - Calls /api/platform-linking/resolve on every message - Creates link tokens for unlinked users - Checks link token status - Chat session creation and SSE streaming (prepared for CoPilot) ### Serverless routes (src/api/) - POST /api/webhooks/discord — Discord interactions endpoint - POST /api/webhooks/telegram — Telegram updates - POST /api/webhooks/slack — Slack events - GET /api/gateway/discord — Gateway cron for Discord messages ### Standalone mode (src/index.ts) - Long-running process for Docker/PM2 deployment - Auto-detects enabled adapters from env vars - Redis or in-memory state ## Stacked on - feat/platform-bot-linking (PR #12615)
Adds the user-facing page that completes the platform bot linking flow. When an unlinked user messages the bot, they get a URL like: https://platform.agpt.co/link/{token} This page: 1. Validates the token (expired? already used?) 2. If user isn't logged in → redirects to login with ?next=/link/{token} 3. Shows a confirmation screen: 'Link your [platform] account to AutoGPT' 4. On click → calls POST /api/platform-linking/tokens/{token}/confirm 5. Shows success or error state ## Implementation - Lives in (no-navbar) route group (standalone page, no main nav) - Reuses AuthCard, Button, Text, Link components from existing auth pages - Same visual style as login/signup pages - Handles all edge cases: expired token, already linked, not authenticated ## Stacked on - feat/copilot-bot-service (PR #12618) - feat/platform-bot-linking (PR #12615)
Multi-platform bot service that deploys CoPilot to Discord, Telegram, and Slack from a single codebase using Vercel's Chat SDK. ## What's included ### Core bot (src/bot.ts) - Chat SDK instance with dynamic adapter loading - onNewMention: resolves platform user → AutoGPT account - Unlinked users get a link prompt via the platform-linking API - Subscribed message handler with state management - MVP echo response (CoPilot API integration next) ### Platform API client (src/platform-api.ts) - Calls /api/platform-linking/resolve on every message - Creates link tokens for unlinked users - Checks link token status - Chat session creation and SSE streaming (prepared for CoPilot) ### Serverless routes (src/api/) - POST /api/webhooks/discord — Discord interactions endpoint - POST /api/webhooks/telegram — Telegram updates - POST /api/webhooks/slack — Slack events - GET /api/gateway/discord — Gateway cron for Discord messages ### Standalone mode (src/index.ts) - Long-running process for Docker/PM2 deployment - Auto-detects enabled adapters from env vars - Redis or in-memory state ## Stacked on - feat/platform-bot-linking (PR #12615)
Adds the user-facing page that completes the platform bot linking flow. When an unlinked user messages the bot, they get a URL like: https://platform.agpt.co/link/{token} This page: 1. Validates the token (expired? already used?) 2. If user isn't logged in → redirects to login with ?next=/link/{token} 3. Shows a confirmation screen: 'Link your [platform] account to AutoGPT' 4. On click → calls POST /api/platform-linking/tokens/{token}/confirm 5. Shows success or error state ## Implementation - Lives in (no-navbar) route group (standalone page, no main nav) - Reuses AuthCard, Button, Text, Link components from existing auth pages - Same visual style as login/signup pages - Handles all edge cases: expired token, already linked, not authenticated ## Stacked on - feat/copilot-bot-service (PR #12618) - feat/platform-bot-linking (PR #12615)
|
/review |
|
This pull request has conflicts with the base branch, please resolve those so we can evaluate the pull request. |
test_create_token_for_unlinked_server was failing on CI with: RuntimeError: <asyncio.locks.Event object> is bound to a different event loop create_link_token uses `async with transaction()` which opens a real Prisma transaction — Prisma binds asyncio primitives (Event, Lock) to whichever event loop first touches the client. pytest-asyncio runs each test in its own loop, so by the second test the cached primitives belong to a dead loop and any await against them blows up. Swap in a fake asynccontextmanager that yields a MagicMock so the route runs without touching the real client.
Adds 23 endpoint-level tests to raise patch coverage on the platform_linking module: - create_user_link_token: success + 409 conflict - resolve_platform_user: linked + unlinked - get_link_token_status: 404, pending, expired-by-time, used-with-link, used-but-superseded - get_link_token_info: 404, used→404, expired→410, success - confirm_link_token: 404, wrong link type, already used, expired, already-linked-to-same-user, already-linked-to-other-user - confirm_user_link_token: 404, wrong link type, expired, already-linked-to-other-user All tests patch find_*, transaction, and PlatformLinkToken so no real DB is touched. Endpoints are invoked directly to exercise their full control flow without bringing up a TestClient.
There was a problem hiding this comment.
Actionable comments posted: 3
🧹 Nitpick comments (1)
autogpt_platform/backend/backend/api/features/platform_linking/routes.py (1)
355-363: CatchUniqueViolationErrordirectly instead of string-matching on error messages.The code at lines 355–363 and 438–446 relies on
"unique" in str(exc).lower(), which is fragile: unrelated errors containing the word "unique" (or localized/version-specific Prisma messages that change the substring) will be misclassified. Prisma exports a typedUniqueViolationErrorthat the rest of the codebase uses for this exact pattern.♻️ Proposed refactor
-from prisma.models import PlatformLink, PlatformLinkToken, PlatformUserLink +from prisma.errors import UniqueViolationError +from prisma.models import PlatformLink, PlatformLinkToken, PlatformUserLinkLines 355–363:
except HTTPException: raise - except Exception as exc: - if "unique" in str(exc).lower(): - raise HTTPException( - status_code=409, - detail="This server was just linked by another request.", - ) from exc - raise + except UniqueViolationError as exc: + raise HTTPException( + status_code=409, + detail="This server was just linked by another request.", + ) from excApply the same change to lines 438–446 for the DM-link handler.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@autogpt_platform/backend/backend/api/features/platform_linking/routes.py` around lines 355 - 363, Replace the fragile string-match catch with a direct exception handler for Prisma's UniqueViolationError: import UniqueViolationError from prisma.exceptions (or prisma.errors if that’s used elsewhere in the repo) and change the except block in the platform link handler (the block currently checking "unique" in str(exc).lower()) to "except UniqueViolationError as exc: raise HTTPException(status_code=409, detail='This server was just linked by another request.') from exc"; keep the existing "except HTTPException: raise" and the fallback "except Exception: raise" order. Make the identical change in the DM-link handler (the similar block at lines 438–446) so both handlers catch UniqueViolationError explicitly instead of string-matching.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In
`@autogpt_platform/backend/backend/api/features/platform_linking/routes_test.py`:
- Around line 10-34: Move the module-level helper _fake_transaction so all
imports appear first: cut the asynccontextmanager _fake_transaction definition
and paste it below the import block that includes check_bot_api_key and the
platform_linking models (BotChatRequest, ConfirmLinkResponse,
CreateLinkTokenRequest, CreateUserLinkTokenRequest, DeleteLinkResponse,
LinkTokenStatusResponse, LinkType, Platform, ResolveResponse,
ResolveServerRequest, ResolveUserRequest) to resolve ruff E402.
In `@autogpt_platform/frontend/src/app/api/openapi.json`:
- Around line 9582-9589: The BotChatRequest schema currently allows
platform_server_id to be an empty string; update the backend model that emits
the OpenAPI (the BotChatRequest model) so platform_server_id is either null or a
non-empty string (e.g., Optional[str] with a min_length/validator that rejects
""), matching the other server-id fields; after changing the BotChatRequest
field definition and/or adding a validator for platform_server_id, regenerate
the OpenAPI JSON so autogpt_platform/frontend/src/app/api/openapi.json reflects
the non-empty constraint.
- Around line 6175-6190: The OpenAPI response for the SSE CoPilot endpoint is
incorrectly documented as application/json; update the backend route metadata
that generates the spec for the operationId "postPlatform-linkingStream a
copilot response for a platform user (bot-facing)" (request schema
BotChatRequest) so the 200 response content type is "text/event-stream" (or
equivalent SSE media type) with an appropriate simple/schema (e.g., string or
empty), e.g., by changing the route/handler decorator or OpenAPI response
annotation to produce "text/event-stream" (or adding an ApiResponse/content
entry for "text/event-stream") rather than application/json; regenerate the
frontend openapi.json rather than hand-editing it.
---
Nitpick comments:
In `@autogpt_platform/backend/backend/api/features/platform_linking/routes.py`:
- Around line 355-363: Replace the fragile string-match catch with a direct
exception handler for Prisma's UniqueViolationError: import UniqueViolationError
from prisma.exceptions (or prisma.errors if that’s used elsewhere in the repo)
and change the except block in the platform link handler (the block currently
checking "unique" in str(exc).lower()) to "except UniqueViolationError as exc:
raise HTTPException(status_code=409, detail='This server was just linked by
another request.') from exc"; keep the existing "except HTTPException: raise"
and the fallback "except Exception: raise" order. Make the identical change in
the DM-link handler (the similar block at lines 438–446) so both handlers catch
UniqueViolationError explicitly instead of string-matching.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 495824d6-1c5d-4188-8e87-c94126abe189
📒 Files selected for processing (7)
autogpt_platform/backend/.env.defaultautogpt_platform/backend/backend/api/features/platform_linking/chat_proxy.pyautogpt_platform/backend/backend/api/features/platform_linking/routes.pyautogpt_platform/backend/backend/api/features/platform_linking/routes_test.pyautogpt_platform/backend/migrations/20260331120000_add_platform_bot_linking/migration.sqlautogpt_platform/backend/migrations/20260414160000_add_platform_user_links/migration.sqlautogpt_platform/frontend/src/app/api/openapi.json
✅ Files skipped from review due to trivial changes (1)
- autogpt_platform/backend/.env.default
Nick round 1 + Sentry + CodeRabbit findings:
- .gitignore: drop `.claude/` (we share skills) and `copilot-bot/dist/`
(Node.js artefact); only `.claude/settings.local.json` ignored.
- Auth consolidated onto FastAPI dependencies: every bot-facing route
uses `dependencies=[Security(get_bot_api_key)]` on the decorator.
get_bot_api_key validates inline. Dropped unused x_bot_api_key
parameters and inline check_bot_api_key() calls.
- `/tokens/{token}/info` now JWT-authed via Security(auth.requires_user).
- Config pulled into Settings:
Config.platform_link_base_url (was os.getenv in routes.py)
Secrets.platform_bot_api_key (was os.getenv in auth.py)
- SSE /chat/stream advertises text/event-stream in OpenAPI responses.
- BotChatRequest.platform_server_id uses Field(min_length=1) so the
no-empty-string constraint reaches the OpenAPI schema.
- bot_chat_stream calls mark_session_completed when subscribe returns
None so the session doesn't sit in 'running' until TTL expires.
- Dropped X-Accel-Buffering: no — we use GCE ingress, not nginx.
- _fake_transaction test helper moved below imports (ruff E402).
- test_resolve_rejects_wrong_api_key removed — redundant now that auth
runs at the FastAPI layer. Auth is tested in TestBotApiKeyAuth which
now mocks Settings directly.
Regenerated openapi.json.
…ice RPC Bot-facing platform_linking moves from REST with a shared X-Bot-API-Key header to a new PlatformLinkingManager(AppService) reachable over cluster-internal RPC. REST keeps only user-facing (JWT-authed) flows. - New backend/platform_linking/ package: models, links (CRUD helpers), chat (start_chat_turn orchestration), manager (AppService + Client). - Bot-facing REST endpoints, auth.py, chat_proxy.py, and PLATFORM_BOT_API_KEY are deleted. - Domain exceptions (LinkAlreadyExistsError / LinkTokenExpiredError / LinkFlowMismatchError) added to backend.util.exceptions; REST layer translates them to HTTP status codes. - start_chat_turn persists the user message via append_and_save_message before enqueueing, fixing a bug where the bot proxy kicked off turns with no user message recorded. - New Config.platform_linking_service_port=8009, client factory get_platform_linking_manager_client(), pyproject script platform-linking-manager. - Adversarial tests cover the confirm-token race (asyncio.gather with same-user and cross-user racers, no hijack), TokenPath regex guard on both GET and POST token paths, and DELETE link_id handling of weird inputs.
…OpenAPI - chat.start_chat_turn previously called stream_registry.create_session even when append_and_save_message flagged the message as a duplicate, leaving an orphan Redis stream with no producer. Matching REST chat behaviour, skip create_session + enqueue and raise DuplicateChatMessageError so the caller can drop cleanly. - Regenerate frontend/src/app/api/openapi.json to match the trimmed platform-linking REST surface.
…nager Per Nick's review and the existing pattern used by blocks / notifications / other AppServices: only DatabaseManager (owns pool) and AgentServer (main REST pod) hold their own Prisma connection. Every other service goes through DatabaseManagerAsyncClient so there's a single connection pool per cluster. - Move Prisma code from platform_linking/links.py to platform_linking/db.py (pure DB layer). find_*_link now return scalar user_id instead of Prisma model objects, which were rejected by AppService's result validator. - Add platform_linking_db() to backend/data/db_accessors.py following the existing auto-switch pattern (direct Prisma when db.is_connected(), DatabaseManagerAsyncClient otherwise). - Expose the 14 platform_linking DB operations on DatabaseManager + DatabaseManagerAsyncClient. - PlatformLinkingManager drops its lifespan Prisma connect; routes everything through platform_linking_db() which resolves to the RPC client in the manager pod. - REST routes + chat.py go through the same accessor — same code path on AgentServer (direct) and PlatformLinkingManager (RPC). - Tests move with the code: db_test.py replaces links_test.py; manager / chat / routes tests mock the accessor instead of Prisma.
…ocal-default link URL - Replace fragile `"unique" in str(exc).lower()` with typed UniqueViolationError catch in both confirm_server_link and confirm_user_link (matches the pattern used in backend/copilot/db.py, backend/data/credit.py, backend/util/workspace.py). - Point PLATFORM_LINK_BASE_URL in .env.default at http://localhost:3000/link so local and staging runs don't mint links pointing at production.









Why
AutoPilot (CoPilot) needs to reach users across chat platforms — Discord first, Telegram / Slack / Teams / WhatsApp next. To make usage and billing coherent, every conversation resolves to one AutoGPT account. There are two independent linking flows:
What
Backend for platform linking, split cleanly by trust boundary:
PlatformLinkingManager(AppService). No shared bearer token; trust is the cluster network itself.REST endpoints (JWT auth)
GET /api/platform-linking/tokens/{token}/info— non-sensitive display info for the link pagePOST /api/platform-linking/tokens/{token}/confirm— confirm a SERVER linkPOST /api/platform-linking/user-tokens/{token}/confirm— confirm a USER linkGET /api/platform-linking/links/DELETE /links/{id}— manage server linksGET /api/platform-linking/user-links/DELETE /user-links/{id}— manage DM linksPlatformLinkingManager@exposemethods (internal RPC)resolve_server_link(platform, platform_server_id) -> ResolveResponseresolve_user_link(platform, platform_user_id) -> ResolveResponsecreate_server_link_token(req) -> LinkTokenResponsecreate_user_link_token(req) -> LinkTokenResponseget_link_token_status(token) -> LinkTokenStatusResponsestart_chat_turn(req) -> ChatTurnHandle— resolves the owner, persists the user message, creates the stream-registry session, enqueues the turn; returns(session_id, turn_id, user_id, subscribe_from="0-0")so the caller subscribes directly to the per-turn Redis stream.New DB models
PlatformLink—(platform, platformServerId)→ owner's AutoGPTuserIdPlatformUserLink—(platform, platformUserId)→ AutoGPTuserId(for DMs)PlatformLinkToken— one-time token withlinkTypediscriminator (SERVER | USER) and 30-min TTLHow
backend/platform_linking/package:models.py(Pydantic types),links.py(link CRUD helpers — pure business logic),chat.py(start_chat_turnorchestration),manager.py(PlatformLinkingManager(AppService)+PlatformLinkingManagerClient). Pattern matchesbackend/notifications/+backend/data/db_manager.py.NotFoundError,LinkAlreadyExistsError,LinkTokenExpiredError,LinkFlowMismatchError,NotAuthorizedError— allValueErrorsubclasses inbackend.util.exceptionsso they auto-register with the RPC exception-mapping). REST routes translate to HTTP codes via a 7-line_translate()helper.find_server_link()andfind_user_link()each query their own table. A user who owns a linked server does not leak that identity into their DMs.update_manywithusedAt = None+expiresAt > nowin the WHERE clause;create_*_tokeninvalidates pending tokens before issuing a new one.start_chat_turnpersists the user message viaappend_and_save_messagebefore enqueueing the executor turn — mirrorsbackend/api/features/chat/routes.py. The previouschat_proxy.pyskipped this and ran the executor with no user message in history.subscribe_from="0-0", so late subscribers replay the full stream; no HTTP SSE proxy needed.session_id,turn_id,server_id, and AutoGPTuser_id(last 8 chars), but never raw platform user IDs.PlatformLinkingManagerruns as its ownAppProcesson port8009; client viaget_platform_linking_manager_client(). The infra chart lands in cloud-infrastructure#310.Tests
models_test.py) — Platform / LinkType enums, request validation (CreateLinkToken / ResolveServer / BotChat), response schemas.links_test.py) — resolve, token create (both flows, 409 on already-linked), token status (pending / linked / expired / superseded-with-no-link), token info (404 / 410), confirm (404 / wrong flow / already used / expired / same-user / other-user), delete authz.manager_test.py) —@exposemethods delegate to helpers; client surface covers bot-facing ops and excludes user-facing ones.manager_test.py,routes_test.py):asyncio.gatherdouble-confirm with same user and with two different users — exactly one winner, other gets cleanLinkTokenExpiredError, no doublePlatformLink.create.TokenPathregex guard: rejects%24, URL-encoded path traversal, >64 chars; acceptssecrets.token_urlsafeshape.link_idwith SQL-injection-style and path-traversal inputs returns 404 viaNotFoundError.Stack
PlatformLinkingManagerClient)/link/{token}frontend pagecopilot-bot+ newplatform-linking-managerMerge order: this → #12618 → #12624, infra whenever.