Skip to content
Merged
Show file tree
Hide file tree
Changes from 40 commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
ae6ee04
feat: Platform bot linking API for multi-platform CoPilot
Bentlybro Mar 31, 2026
cfdef71
style: lint + format platform linking routes
Bentlybro Mar 31, 2026
88cfe03
fix: use Model.prisma() pattern for pyright compatibility
Bentlybro Mar 31, 2026
b228165
chore: update openapi.json with platform-linking endpoints
Bentlybro Mar 31, 2026
7993ae7
fix: import ordering (isort)
Bentlybro Mar 31, 2026
5b0bc35
fix: Address review feedback on platform linking PR
Bentlybro Mar 31, 2026
f46355e
fix: suppress pyright error on intentionally invalid test input
Bentlybro Mar 31, 2026
b636ab6
fix: use model_validate for invalid platform test (no type ignore)
Bentlybro Mar 31, 2026
3952519
feat: Bot chat proxy — CoPilot streaming via bot API key
Bentlybro Mar 31, 2026
af14907
fix: correct unsubscribe_from_session parameter (subscriber_queue not…
Bentlybro Mar 31, 2026
95f436d
fix: Address review round 2 — all 4 blockers
Bentlybro Mar 31, 2026
9685838
refactor(backend): split platform-linking routes into models, auth, r…
Bentlybro Apr 1, 2026
be82e38
ci: regenerate openapi.json after platform-linking schema changes
Bentlybro Apr 2, 2026
9c7dcbc
feat(backend): server-level platform linking with impersonation fix
Bentlybro Apr 8, 2026
017134b
ci: regenerate openapi.json after server-level platform linking changes
Bentlybro Apr 8, 2026
c42398f
fix(backend): warn loudly when bot API key bypass is active + documen…
Bentlybro Apr 9, 2026
ce8af2a
chore: ignore copilot-bot/node_modules and .claude/ in root gitignore
Bentlybro Apr 9, 2026
2431318
fix(backend): DM fallback in resolve — users already linked via a ser…
Bentlybro Apr 9, 2026
c743882
fix(backend): append ?platform= to link URL so frontend knows the sou…
Bentlybro Apr 9, 2026
51c6401
feat(backend): add GET /tokens/{token}/info endpoint — returns platfo…
Bentlybro Apr 9, 2026
7f10dfa
ci: regenerate openapi.json — add LinkTokenInfoResponse and /tokens/{…
Bentlybro Apr 9, 2026
aadb7cc
Update openapi.json
Bentlybro Apr 14, 2026
8a1a87b
Minor formatting cleanup in platform_linking
Bentlybro Apr 14, 2026
ff0c576
Update platform_cost_test.py
Bentlybro Apr 14, 2026
3fadfc8
Ensure create_chat_session persists sessions
Bentlybro Apr 14, 2026
be8cc33
fix(backend): fix test assertion for link_url with platform query param
Bentlybro Apr 14, 2026
4a2de44
refactor(backend): extract find_server_link helper, cache auth settings
Bentlybro Apr 14, 2026
9bd8806
test(backend): update mocks for find_server_link helper + clear auth …
Bentlybro Apr 14, 2026
b7080d2
feat(backend): separate server vs user (DM) platform linking
Bentlybro Apr 14, 2026
84953f7
fix(backend): address PR review findings on platform linking
Bentlybro Apr 14, 2026
6cec213
fix(backend): stop logging platform user IDs, add APIKeyHeader scheme
Bentlybro Apr 14, 2026
d60fc4e
fix: apply CodeRabbit auto-fixes
coderabbitai[bot] Apr 14, 2026
ac38853
fix(backend): split USER-link schema migration + lint trailing newlines
Bentlybro Apr 14, 2026
f72095c
fix(backend): update create_link_token tests for find_server_link ref…
Bentlybro Apr 14, 2026
0407f85
Merge remote-tracking branch 'origin/dev' into feat/platform-bot-linking
Bentlybro Apr 16, 2026
b34abb2
test(backend): mock transaction() in platform-linking route test
Bentlybro Apr 16, 2026
f754b63
style(backend): black format routes_test.py
Bentlybro Apr 16, 2026
724c90c
test(backend): expand platform_linking route coverage
Bentlybro Apr 16, 2026
d048eb3
fix(backend/platform-linking): address PR #12615 review
Bentlybro Apr 16, 2026
77c7b05
refactor(backend/platform-linking): move bot-facing routes to AppServ…
Bentlybro Apr 16, 2026
18fd4da
fix(backend/platform-linking): raise on duplicate chat message; sync …
Bentlybro Apr 16, 2026
228ab06
refactor(backend/platform-linking): route DB calls through DatabaseMa…
Bentlybro Apr 17, 2026
cf02901
style(backend/platform-linking): isort db_manager.py import
Bentlybro Apr 17, 2026
a03bd53
fix(backend/platform-linking): catch UniqueViolationError directly; l…
Bentlybro Apr 17, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions autogpt_platform/.gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
*.ignore.*
*.ign.*
.application.logs

# Claude Code local settings only — the rest of .claude/ is shared (skills etc.)
.claude/settings.local.json
3 changes: 3 additions & 0 deletions autogpt_platform/backend/.env.default
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,9 @@ MEM0_API_KEY=
OPENWEATHERMAP_API_KEY=
GOOGLE_MAPS_API_KEY=

# Platform Bot Linking
PLATFORM_LINK_BASE_URL=https://platform.agpt.co/link

# Communication Services
DISCORD_BOT_TOKEN=
MEDIUM_API_KEY=
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""Platform bot linking — user-facing REST routes."""
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
"""User-facing platform_linking REST routes (JWT auth)."""

import logging
from typing import Annotated

from autogpt_libs import auth
from fastapi import APIRouter, HTTPException, Path, Security

from backend.platform_linking.links import (
confirm_server_link,
confirm_user_link,
delete_server_link,
delete_user_link,
get_link_token_info,
list_server_links,
list_user_links,
)
from backend.platform_linking.models import (
ConfirmLinkResponse,
ConfirmUserLinkResponse,
DeleteLinkResponse,
LinkTokenInfoResponse,
PlatformLinkInfo,
PlatformUserLinkInfo,
)
from backend.util.exceptions import (
LinkAlreadyExistsError,
LinkFlowMismatchError,
LinkTokenExpiredError,
NotAuthorizedError,
NotFoundError,
)

logger = logging.getLogger(__name__)

router = APIRouter()

TokenPath = Annotated[
str,
Path(max_length=64, pattern=r"^[A-Za-z0-9_-]+$"),
]


def _translate(exc: Exception) -> HTTPException:
if isinstance(exc, NotFoundError):
return HTTPException(status_code=404, detail=str(exc))
if isinstance(exc, NotAuthorizedError):
return HTTPException(status_code=403, detail=str(exc))
if isinstance(exc, LinkAlreadyExistsError):
return HTTPException(status_code=409, detail=str(exc))
if isinstance(exc, LinkTokenExpiredError):
return HTTPException(status_code=410, detail=str(exc))
if isinstance(exc, LinkFlowMismatchError):
return HTTPException(status_code=400, detail=str(exc))
return HTTPException(status_code=500, detail="Internal error.")


@router.get(
Comment thread
ntindle marked this conversation as resolved.
"/tokens/{token}/info",
response_model=LinkTokenInfoResponse,
dependencies=[Security(auth.requires_user)],
Comment thread
ntindle marked this conversation as resolved.
summary="Get display info for a link token",
)
async def get_link_token_info_route(token: TokenPath) -> LinkTokenInfoResponse:
try:
return await get_link_token_info(token)
except (NotFoundError, LinkTokenExpiredError) as exc:
raise _translate(exc) from exc


@router.post(
"/tokens/{token}/confirm",
response_model=ConfirmLinkResponse,
dependencies=[Security(auth.requires_user)],
summary="Confirm a SERVER link token (user must be authenticated)",
)
async def confirm_link_token(
token: TokenPath,
user_id: Annotated[str, Security(auth.get_user_id)],
) -> ConfirmLinkResponse:
try:
return await confirm_server_link(token, user_id)
Comment thread
ntindle marked this conversation as resolved.
Outdated
except (
NotFoundError,
LinkFlowMismatchError,
LinkTokenExpiredError,
LinkAlreadyExistsError,
) as exc:
raise _translate(exc) from exc


@router.post(
"/user-tokens/{token}/confirm",
response_model=ConfirmUserLinkResponse,
dependencies=[Security(auth.requires_user)],
summary="Confirm a USER link token (user must be authenticated)",
)
async def confirm_user_link_token(
token: TokenPath,
user_id: Annotated[str, Security(auth.get_user_id)],
) -> ConfirmUserLinkResponse:
try:
return await confirm_user_link(token, user_id)
except (
NotFoundError,
LinkFlowMismatchError,
LinkTokenExpiredError,
LinkAlreadyExistsError,
) as exc:
raise _translate(exc) from exc


@router.get(
"/links",
response_model=list[PlatformLinkInfo],
dependencies=[Security(auth.requires_user)],
summary="List all platform servers linked to the authenticated user",
)
async def list_my_links(
user_id: Annotated[str, Security(auth.get_user_id)],
) -> list[PlatformLinkInfo]:
return await list_server_links(user_id)


@router.get(
"/user-links",
response_model=list[PlatformUserLinkInfo],
dependencies=[Security(auth.requires_user)],
summary="List all DM links for the authenticated user",
)
async def list_my_user_links(
user_id: Annotated[str, Security(auth.get_user_id)],
) -> list[PlatformUserLinkInfo]:
return await list_user_links(user_id)


@router.delete(
"/links/{link_id}",
response_model=DeleteLinkResponse,
dependencies=[Security(auth.requires_user)],
summary="Unlink a platform server",
)
async def delete_link(
link_id: str,
user_id: Annotated[str, Security(auth.get_user_id)],
) -> DeleteLinkResponse:
try:
return await delete_server_link(link_id, user_id)
except (NotFoundError, NotAuthorizedError) as exc:
raise _translate(exc) from exc


@router.delete(
"/user-links/{link_id}",
response_model=DeleteLinkResponse,
dependencies=[Security(auth.requires_user)],
summary="Unlink a DM / user link",
)
async def delete_user_link_route(
link_id: str,
user_id: Annotated[str, Security(auth.get_user_id)],
) -> DeleteLinkResponse:
try:
return await delete_user_link(link_id, user_id)
except (NotFoundError, NotAuthorizedError) as exc:
raise _translate(exc) from exc
Loading
Loading