mirror of
https://github.com/Significant-Gravitas/AutoGPT.git
synced 2026-04-08 03:00:28 -04:00
fix: Address review feedback on platform linking PR
Fixes all blockers and should-fix items from the automated review:
## Blockers fixed
1. **Bot-facing endpoint auth** — Added X-Bot-API-Key header auth for
all bot-facing endpoints (POST /tokens, GET /tokens/{token}/status,
POST /resolve). Configurable via PLATFORM_BOT_API_KEY env var.
Dev mode allows keyless access.
2. **Race condition in confirm_link_token** — Replaced 5 separate DB
calls with atomic update_many (WHERE token=X AND usedAt=NULL).
If another request consumed the token first, returns 410 cleanly
instead of a 500 unique constraint violation.
3. **Test coverage** — Added tests for: Platform enum, bot API key
auth (valid/invalid/missing), request validation (empty IDs,
too-long IDs, invalid platforms), response model typing.
## Should-fix items addressed
- **Enum duplication** — Created Python Platform(str, Enum) that
mirrors the Prisma PlatformType. Used throughout request models
for validation instead of bare strings.
- **Hardcoded base URL** — Now configurable via PLATFORM_LINK_BASE_URL
env var. Defaults to https://platform.agpt.co/link.
- **Redundant @@index([token])** — Removed from schema and migration.
The @unique already creates an index.
- **Literal status type** — LinkTokenStatusResponse.status is now
Literal['pending', 'linked', 'expired'] not bare str.
- **delete_link returns dict** — Now returns DeleteLinkResponse model.
- **Input length validation** — Added min_length=1, max_length=255
to platform_user_id and platform_username fields.
- **Token flooding** — Existing pending tokens are invalidated before
creating a new one (only 1 active token per platform identity).
- **Resolve leaks user info** — Removed platform_username from
resolve response (only returns user_id + linked boolean).
- **Error messages** — Sanitized to not expose internal IDs.
- **Logger** — Switched from f-strings to lazy %s formatting.
This commit is contained in:
@@ -15,12 +15,14 @@ Flow:
|
||||
"""
|
||||
|
||||
import logging
|
||||
import os
|
||||
import secrets
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from typing import Annotated
|
||||
from enum import Enum
|
||||
from typing import Annotated, Literal
|
||||
|
||||
from autogpt_libs import auth
|
||||
from fastapi import APIRouter, HTTPException, Security
|
||||
from fastapi import APIRouter, Depends, HTTPException, Security
|
||||
from prisma.models import PlatformLink, PlatformLinkToken
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
@@ -29,16 +31,53 @@ logger = logging.getLogger(__name__)
|
||||
router = APIRouter()
|
||||
|
||||
LINK_TOKEN_EXPIRY_MINUTES = 30
|
||||
LINK_BASE_URL = os.getenv("PLATFORM_LINK_BASE_URL", "https://platform.agpt.co/link")
|
||||
|
||||
VALID_PLATFORMS = {
|
||||
"DISCORD",
|
||||
"TELEGRAM",
|
||||
"SLACK",
|
||||
"TEAMS",
|
||||
"WHATSAPP",
|
||||
"GITHUB",
|
||||
"LINEAR",
|
||||
}
|
||||
|
||||
# ── Platform enum (mirrors Prisma PlatformType) ───────────────────────
|
||||
|
||||
|
||||
class Platform(str, Enum):
|
||||
DISCORD = "DISCORD"
|
||||
TELEGRAM = "TELEGRAM"
|
||||
SLACK = "SLACK"
|
||||
TEAMS = "TEAMS"
|
||||
WHATSAPP = "WHATSAPP"
|
||||
GITHUB = "GITHUB"
|
||||
LINEAR = "LINEAR"
|
||||
|
||||
|
||||
# ── API Key auth for bot-facing endpoints ─────────────────────────────
|
||||
|
||||
BOT_API_KEY = os.getenv("PLATFORM_BOT_API_KEY", "")
|
||||
|
||||
|
||||
async def verify_bot_api_key(
|
||||
authorization: str = Depends(lambda: ""),
|
||||
) -> None:
|
||||
"""Dependency that verifies the bot service API key."""
|
||||
# If no key is configured, reject all requests in production
|
||||
if not BOT_API_KEY:
|
||||
if os.getenv("ENV", "development") != "development":
|
||||
raise HTTPException(
|
||||
status_code=503,
|
||||
detail="Bot API key not configured.",
|
||||
)
|
||||
# Allow in development without key
|
||||
return
|
||||
|
||||
# Check header
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def _check_bot_api_key(request_api_key: str | None) -> None:
|
||||
"""Validate the bot API key from the X-Bot-API-Key header."""
|
||||
if not BOT_API_KEY:
|
||||
# Development mode: allow without key
|
||||
return
|
||||
if not request_api_key or request_api_key != BOT_API_KEY:
|
||||
raise HTTPException(status_code=401, detail="Invalid bot API key.")
|
||||
|
||||
|
||||
# ── Request / Response Models ──────────────────────────────────────────
|
||||
@@ -47,17 +86,21 @@ VALID_PLATFORMS = {
|
||||
class CreateLinkTokenRequest(BaseModel):
|
||||
"""Request from the bot service to create a linking token."""
|
||||
|
||||
platform: str = Field(
|
||||
description=(
|
||||
"Platform name: DISCORD, TELEGRAM, SLACK, TEAMS, WHATSAPP, GITHUB, LINEAR"
|
||||
)
|
||||
platform: Platform = Field(description="Platform name")
|
||||
platform_user_id: str = Field(
|
||||
description="The user's ID on the platform",
|
||||
min_length=1,
|
||||
max_length=255,
|
||||
)
|
||||
platform_user_id: str = Field(description="The user's ID on the platform")
|
||||
platform_username: str | None = Field(
|
||||
default=None, description="Display name (best effort)"
|
||||
default=None,
|
||||
description="Display name (best effort)",
|
||||
max_length=255,
|
||||
)
|
||||
channel_id: str | None = Field(
|
||||
default=None, description="Channel ID for sending confirmation back"
|
||||
default=None,
|
||||
description="Channel ID for sending confirmation back",
|
||||
max_length=255,
|
||||
)
|
||||
|
||||
|
||||
@@ -68,21 +111,20 @@ class LinkTokenResponse(BaseModel):
|
||||
|
||||
|
||||
class LinkTokenStatusResponse(BaseModel):
|
||||
status: str # "pending", "linked", "expired"
|
||||
status: Literal["pending", "linked", "expired"]
|
||||
user_id: str | None = None
|
||||
|
||||
|
||||
class ResolveRequest(BaseModel):
|
||||
"""Resolve a platform identity to an AutoGPT user."""
|
||||
|
||||
platform: str
|
||||
platform_user_id: str
|
||||
platform: Platform
|
||||
platform_user_id: str = Field(min_length=1, max_length=255)
|
||||
|
||||
|
||||
class ResolveResponse(BaseModel):
|
||||
linked: bool
|
||||
user_id: str | None = None
|
||||
platform_username: str | None = None
|
||||
|
||||
|
||||
class PlatformLinkInfo(BaseModel):
|
||||
@@ -100,6 +142,10 @@ class ConfirmLinkResponse(BaseModel):
|
||||
platform_username: str | None
|
||||
|
||||
|
||||
class DeleteLinkResponse(BaseModel):
|
||||
success: bool
|
||||
|
||||
|
||||
# ── Bot-facing endpoints (API key auth) ───────────────────────────────
|
||||
|
||||
|
||||
@@ -110,15 +156,17 @@ class ConfirmLinkResponse(BaseModel):
|
||||
)
|
||||
async def create_link_token(
|
||||
request: CreateLinkTokenRequest,
|
||||
x_bot_api_key: str | None = Depends(
|
||||
lambda req: req.headers.get("x-bot-api-key") # noqa: ARG005
|
||||
),
|
||||
) -> LinkTokenResponse:
|
||||
"""
|
||||
Called by the bot service when it encounters an unlinked user.
|
||||
Generates a one-time token the user can use to link their account.
|
||||
|
||||
TODO: Add API key auth for bot service (for now, open for development).
|
||||
"""
|
||||
platform = request.platform.upper()
|
||||
_validate_platform(platform)
|
||||
_check_bot_api_key(x_bot_api_key)
|
||||
|
||||
platform = request.platform.value
|
||||
|
||||
# Check if already linked
|
||||
existing = await PlatformLink.prisma().find_first(
|
||||
@@ -130,12 +178,19 @@ async def create_link_token(
|
||||
if existing:
|
||||
raise HTTPException(
|
||||
status_code=409,
|
||||
detail=(
|
||||
f"Platform user {request.platform_user_id} on {platform} "
|
||||
f"is already linked to an account."
|
||||
),
|
||||
detail="This platform account is already linked.",
|
||||
)
|
||||
|
||||
# Invalidate any existing pending tokens for this user
|
||||
await PlatformLinkToken.prisma().update_many(
|
||||
where={
|
||||
"platform": platform,
|
||||
"platformUserId": request.platform_user_id,
|
||||
"usedAt": None,
|
||||
},
|
||||
data={"usedAt": datetime.now(timezone.utc)},
|
||||
)
|
||||
|
||||
# Generate token
|
||||
token = secrets.token_urlsafe(32)
|
||||
expires_at = datetime.now(timezone.utc) + timedelta(
|
||||
@@ -154,12 +209,12 @@ async def create_link_token(
|
||||
)
|
||||
|
||||
logger.info(
|
||||
f"Created link token for {platform}:{request.platform_user_id} "
|
||||
f"(expires {expires_at.isoformat()})"
|
||||
"Created link token for %s (expires %s)",
|
||||
platform,
|
||||
expires_at.isoformat(),
|
||||
)
|
||||
|
||||
# TODO: Make base URL configurable
|
||||
link_url = f"https://platform.agpt.co/link/{token}"
|
||||
link_url = f"{LINK_BASE_URL}/{token}"
|
||||
|
||||
return LinkTokenResponse(
|
||||
token=token,
|
||||
@@ -173,10 +228,17 @@ async def create_link_token(
|
||||
response_model=LinkTokenStatusResponse,
|
||||
summary="Check if a link token has been consumed",
|
||||
)
|
||||
async def get_link_token_status(token: str) -> LinkTokenStatusResponse:
|
||||
async def get_link_token_status(
|
||||
token: str,
|
||||
x_bot_api_key: str | None = Depends(
|
||||
lambda req: req.headers.get("x-bot-api-key") # noqa: ARG005
|
||||
),
|
||||
) -> LinkTokenStatusResponse:
|
||||
"""
|
||||
Called by the bot service to check if a user has completed linking.
|
||||
"""
|
||||
_check_bot_api_key(x_bot_api_key)
|
||||
|
||||
link_token = await PlatformLinkToken.prisma().find_unique(where={"token": token})
|
||||
|
||||
if not link_token:
|
||||
@@ -208,17 +270,19 @@ async def get_link_token_status(token: str) -> LinkTokenStatusResponse:
|
||||
)
|
||||
async def resolve_platform_user(
|
||||
request: ResolveRequest,
|
||||
x_bot_api_key: str | None = Depends(
|
||||
lambda req: req.headers.get("x-bot-api-key") # noqa: ARG005
|
||||
),
|
||||
) -> ResolveResponse:
|
||||
"""
|
||||
Called by the bot service on every incoming message to check if
|
||||
the platform user has a linked AutoGPT account.
|
||||
"""
|
||||
platform = request.platform.upper()
|
||||
_validate_platform(platform)
|
||||
_check_bot_api_key(x_bot_api_key)
|
||||
|
||||
link = await PlatformLink.prisma().find_first(
|
||||
where={
|
||||
"platform": platform,
|
||||
"platform": request.platform.value,
|
||||
"platformUserId": request.platform_user_id,
|
||||
}
|
||||
)
|
||||
@@ -229,7 +293,6 @@ async def resolve_platform_user(
|
||||
return ResolveResponse(
|
||||
linked=True,
|
||||
user_id=link.userId,
|
||||
platform_username=link.platformUsername,
|
||||
)
|
||||
|
||||
|
||||
@@ -249,19 +312,34 @@ async def confirm_link_token(
|
||||
"""
|
||||
Called by the frontend when the user clicks the link and is logged in.
|
||||
Consumes the token and creates the platform link.
|
||||
Uses atomic update_many to prevent race conditions on double-click.
|
||||
"""
|
||||
# Fetch and validate token
|
||||
link_token = await PlatformLinkToken.prisma().find_unique(where={"token": token})
|
||||
|
||||
if not link_token:
|
||||
raise HTTPException(status_code=404, detail="Token not found")
|
||||
raise HTTPException(status_code=404, detail="Token not found.")
|
||||
|
||||
if link_token.usedAt is not None:
|
||||
raise HTTPException(status_code=410, detail="Token already used")
|
||||
raise HTTPException(status_code=410, detail="This link has already been used.")
|
||||
|
||||
if link_token.expiresAt.replace(tzinfo=timezone.utc) < datetime.now(timezone.utc):
|
||||
raise HTTPException(status_code=410, detail="Token expired")
|
||||
raise HTTPException(status_code=410, detail="This link has expired.")
|
||||
|
||||
# Check if this platform identity is already linked to someone else
|
||||
# Atomically mark token as used (only if still unused)
|
||||
updated = await PlatformLinkToken.prisma().update_many(
|
||||
where={
|
||||
"token": token,
|
||||
"usedAt": None,
|
||||
},
|
||||
data={"usedAt": datetime.now(timezone.utc)},
|
||||
)
|
||||
|
||||
if updated == 0:
|
||||
# Another request already consumed it
|
||||
raise HTTPException(status_code=410, detail="This link has already been used.")
|
||||
|
||||
# Check if this platform identity is already linked
|
||||
existing = await PlatformLink.prisma().find_first(
|
||||
where={
|
||||
"platform": link_token.platform,
|
||||
@@ -269,15 +347,12 @@ async def confirm_link_token(
|
||||
}
|
||||
)
|
||||
if existing:
|
||||
if existing.userId == user_id:
|
||||
raise HTTPException(
|
||||
status_code=409,
|
||||
detail="This platform account is already linked to your account.",
|
||||
)
|
||||
raise HTTPException(
|
||||
status_code=409,
|
||||
detail="This platform account is already linked to another user.",
|
||||
detail = (
|
||||
"This platform account is already linked to your account."
|
||||
if existing.userId == user_id
|
||||
else "This platform account is already linked to another user."
|
||||
)
|
||||
raise HTTPException(status_code=409, detail=detail)
|
||||
|
||||
# Create the link
|
||||
await PlatformLink.prisma().create(
|
||||
@@ -289,15 +364,11 @@ async def confirm_link_token(
|
||||
}
|
||||
)
|
||||
|
||||
# Mark token as used
|
||||
await PlatformLinkToken.prisma().update(
|
||||
where={"token": token},
|
||||
data={"usedAt": datetime.now(timezone.utc)},
|
||||
)
|
||||
|
||||
logger.info(
|
||||
f"Linked {link_token.platform}:{link_token.platformUserId} "
|
||||
f"to user {user_id[-8:]}"
|
||||
"Linked %s:%s to user ...%s",
|
||||
link_token.platform,
|
||||
link_token.platformUserId,
|
||||
user_id[-8:],
|
||||
)
|
||||
|
||||
return ConfirmLinkResponse(
|
||||
@@ -339,13 +410,14 @@ async def list_my_links(
|
||||
|
||||
@router.delete(
|
||||
"/links/{link_id}",
|
||||
response_model=DeleteLinkResponse,
|
||||
dependencies=[Security(auth.requires_user)],
|
||||
summary="Unlink a platform identity",
|
||||
)
|
||||
async def delete_link(
|
||||
link_id: str,
|
||||
user_id: Annotated[str, Security(auth.get_user_id)],
|
||||
) -> dict:
|
||||
) -> DeleteLinkResponse:
|
||||
"""
|
||||
Removes a platform link. The user will need to re-link if they
|
||||
want to use the bot on that platform again.
|
||||
@@ -353,29 +425,18 @@ async def delete_link(
|
||||
link = await PlatformLink.prisma().find_unique(where={"id": link_id})
|
||||
|
||||
if not link:
|
||||
raise HTTPException(status_code=404, detail="Link not found")
|
||||
raise HTTPException(status_code=404, detail="Link not found.")
|
||||
|
||||
if link.userId != user_id:
|
||||
raise HTTPException(status_code=403, detail="Not your link")
|
||||
raise HTTPException(status_code=403, detail="Not your link.")
|
||||
|
||||
await PlatformLink.prisma().delete(where={"id": link_id})
|
||||
|
||||
logger.info(
|
||||
f"Unlinked {link.platform}:{link.platformUserId} from user {user_id[-8:]}"
|
||||
"Unlinked %s:%s from user ...%s",
|
||||
link.platform,
|
||||
link.platformUserId,
|
||||
user_id[-8:],
|
||||
)
|
||||
|
||||
return {"success": True}
|
||||
|
||||
|
||||
# ── Helpers ────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
def _validate_platform(platform: str) -> None:
|
||||
if platform not in VALID_PLATFORMS:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail=(
|
||||
f"Invalid platform '{platform}'. "
|
||||
f"Must be one of: {', '.join(sorted(VALID_PLATFORMS))}"
|
||||
),
|
||||
)
|
||||
return DeleteLinkResponse(success=True)
|
||||
|
||||
@@ -1,25 +1,136 @@
|
||||
"""Tests for platform bot linking API routes."""
|
||||
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
from fastapi import HTTPException
|
||||
|
||||
from backend.api.features.platform_linking.routes import (
|
||||
VALID_PLATFORMS,
|
||||
_validate_platform,
|
||||
)
|
||||
from backend.api.features.platform_linking.routes import Platform, _check_bot_api_key
|
||||
|
||||
|
||||
class TestValidatePlatform:
|
||||
def test_valid_platforms(self):
|
||||
for platform in VALID_PLATFORMS:
|
||||
# Should not raise
|
||||
_validate_platform(platform)
|
||||
class TestPlatformEnum:
|
||||
def test_all_platforms_exist(self):
|
||||
assert Platform.DISCORD.value == "DISCORD"
|
||||
assert Platform.TELEGRAM.value == "TELEGRAM"
|
||||
assert Platform.SLACK.value == "SLACK"
|
||||
assert Platform.TEAMS.value == "TEAMS"
|
||||
assert Platform.WHATSAPP.value == "WHATSAPP"
|
||||
assert Platform.GITHUB.value == "GITHUB"
|
||||
assert Platform.LINEAR.value == "LINEAR"
|
||||
|
||||
def test_invalid_platform(self):
|
||||
|
||||
class TestBotApiKeyAuth:
|
||||
@patch("backend.api.features.platform_linking.routes.BOT_API_KEY", "")
|
||||
def test_no_key_configured_allows_in_dev(self):
|
||||
# No key configured = dev mode, should not raise
|
||||
_check_bot_api_key(None)
|
||||
|
||||
@patch("backend.api.features.platform_linking.routes.BOT_API_KEY", "secret123")
|
||||
def test_valid_key(self):
|
||||
_check_bot_api_key("secret123")
|
||||
|
||||
@patch("backend.api.features.platform_linking.routes.BOT_API_KEY", "secret123")
|
||||
def test_invalid_key_rejected(self):
|
||||
with pytest.raises(HTTPException) as exc_info:
|
||||
_validate_platform("INVALID")
|
||||
assert exc_info.value.status_code == 400
|
||||
_check_bot_api_key("wrong")
|
||||
assert exc_info.value.status_code == 401
|
||||
|
||||
def test_lowercase_rejected(self):
|
||||
with pytest.raises(HTTPException):
|
||||
_validate_platform("discord")
|
||||
@patch("backend.api.features.platform_linking.routes.BOT_API_KEY", "secret123")
|
||||
def test_missing_key_rejected(self):
|
||||
with pytest.raises(HTTPException) as exc_info:
|
||||
_check_bot_api_key(None)
|
||||
assert exc_info.value.status_code == 401
|
||||
|
||||
|
||||
class TestCreateLinkTokenRequest:
|
||||
"""Test request validation."""
|
||||
|
||||
from backend.api.features.platform_linking.routes import CreateLinkTokenRequest
|
||||
|
||||
def test_valid_request(self):
|
||||
req = self.CreateLinkTokenRequest(
|
||||
platform=Platform.DISCORD,
|
||||
platform_user_id="353922987235213313",
|
||||
)
|
||||
assert req.platform == Platform.DISCORD
|
||||
assert req.platform_user_id == "353922987235213313"
|
||||
|
||||
def test_empty_platform_user_id_rejected(self):
|
||||
from pydantic import ValidationError
|
||||
|
||||
with pytest.raises(ValidationError):
|
||||
self.CreateLinkTokenRequest(
|
||||
platform=Platform.DISCORD,
|
||||
platform_user_id="",
|
||||
)
|
||||
|
||||
def test_too_long_platform_user_id_rejected(self):
|
||||
from pydantic import ValidationError
|
||||
|
||||
with pytest.raises(ValidationError):
|
||||
self.CreateLinkTokenRequest(
|
||||
platform=Platform.DISCORD,
|
||||
platform_user_id="x" * 256,
|
||||
)
|
||||
|
||||
def test_invalid_platform_rejected(self):
|
||||
from pydantic import ValidationError
|
||||
|
||||
with pytest.raises(ValidationError):
|
||||
self.CreateLinkTokenRequest(
|
||||
platform="INVALID",
|
||||
platform_user_id="123",
|
||||
)
|
||||
|
||||
|
||||
class TestResolveRequest:
|
||||
from backend.api.features.platform_linking.routes import ResolveRequest
|
||||
|
||||
def test_valid_request(self):
|
||||
req = self.ResolveRequest(
|
||||
platform=Platform.TELEGRAM,
|
||||
platform_user_id="123456789",
|
||||
)
|
||||
assert req.platform == Platform.TELEGRAM
|
||||
|
||||
def test_empty_id_rejected(self):
|
||||
from pydantic import ValidationError
|
||||
|
||||
with pytest.raises(ValidationError):
|
||||
self.ResolveRequest(
|
||||
platform=Platform.SLACK,
|
||||
platform_user_id="",
|
||||
)
|
||||
|
||||
|
||||
class TestResponseModels:
|
||||
"""Test that response models are properly typed."""
|
||||
|
||||
from backend.api.features.platform_linking.routes import (
|
||||
ConfirmLinkResponse,
|
||||
DeleteLinkResponse,
|
||||
LinkTokenStatusResponse,
|
||||
)
|
||||
|
||||
def test_link_token_status_literal(self):
|
||||
resp = self.LinkTokenStatusResponse(status="pending")
|
||||
assert resp.status == "pending"
|
||||
|
||||
resp = self.LinkTokenStatusResponse(status="linked", user_id="abc")
|
||||
assert resp.status == "linked"
|
||||
|
||||
resp = self.LinkTokenStatusResponse(status="expired")
|
||||
assert resp.status == "expired"
|
||||
|
||||
def test_confirm_link_response(self):
|
||||
resp = self.ConfirmLinkResponse(
|
||||
success=True,
|
||||
platform="DISCORD",
|
||||
platform_user_id="123",
|
||||
platform_username="testuser",
|
||||
)
|
||||
assert resp.success is True
|
||||
|
||||
def test_delete_link_response(self):
|
||||
resp = self.DeleteLinkResponse(success=True)
|
||||
assert resp.success is True
|
||||
|
||||
@@ -37,9 +37,6 @@ CREATE INDEX "PlatformLink_userId_idx" ON "PlatformLink"("userId");
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "PlatformLinkToken_token_key" ON "PlatformLinkToken"("token");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "PlatformLinkToken_token_idx" ON "PlatformLinkToken"("token");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "PlatformLinkToken_expiresAt_idx" ON "PlatformLinkToken"("expiresAt");
|
||||
|
||||
|
||||
@@ -1350,6 +1350,5 @@ model PlatformLinkToken {
|
||||
usedAt DateTime?
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
@@index([token])
|
||||
@@index([expiresAt])
|
||||
}
|
||||
|
||||
@@ -5503,11 +5503,7 @@
|
||||
"description": "Successful Response",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": true,
|
||||
"title": "Response Deleteplatform-Linkingunlink A Platform Identity"
|
||||
}
|
||||
"schema": { "$ref": "#/components/schemas/DeleteLinkResponse" }
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -5531,13 +5527,21 @@
|
||||
"summary": "Resolve a platform identity to an AutoGPT user",
|
||||
"description": "Called by the bot service on every incoming message to check if\nthe platform user has a linked AutoGPT account.",
|
||||
"operationId": "postPlatform-linkingResolve a platform identity to an autogpt user",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "req",
|
||||
"in": "query",
|
||||
"required": true,
|
||||
"schema": { "title": "Req" }
|
||||
}
|
||||
],
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": { "$ref": "#/components/schemas/ResolveRequest" }
|
||||
}
|
||||
},
|
||||
"required": true
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
@@ -5563,17 +5567,25 @@
|
||||
"post": {
|
||||
"tags": ["platform-linking"],
|
||||
"summary": "Create a link token for an unlinked platform user",
|
||||
"description": "Called by the bot service when it encounters an unlinked user.\nGenerates a one-time token the user can use to link their account.\n\nTODO: Add API key auth for bot service (for now, open for development).",
|
||||
"description": "Called by the bot service when it encounters an unlinked user.\nGenerates a one-time token the user can use to link their account.",
|
||||
"operationId": "postPlatform-linkingCreate a link token for an unlinked platform user",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "req",
|
||||
"in": "query",
|
||||
"required": true,
|
||||
"schema": { "title": "Req" }
|
||||
}
|
||||
],
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/CreateLinkTokenRequest"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": true
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
@@ -5599,7 +5611,7 @@
|
||||
"post": {
|
||||
"tags": ["platform-linking"],
|
||||
"summary": "Confirm a link token (user must be authenticated)",
|
||||
"description": "Called by the frontend when the user clicks the link and is logged in.\nConsumes the token and creates the platform link.",
|
||||
"description": "Called by the frontend when the user clicks the link and is logged in.\nConsumes the token and creates the platform link.\nUses atomic update_many to prevent race conditions on double-click.",
|
||||
"operationId": "postPlatform-linkingConfirm a link token (user must be authenticated)",
|
||||
"security": [{ "HTTPBearerJWT": [] }],
|
||||
"parameters": [
|
||||
@@ -5645,6 +5657,12 @@
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"schema": { "type": "string", "title": "Token" }
|
||||
},
|
||||
{
|
||||
"name": "req",
|
||||
"in": "query",
|
||||
"required": true,
|
||||
"schema": { "title": "Req" }
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
@@ -8738,22 +8756,29 @@
|
||||
"CreateLinkTokenRequest": {
|
||||
"properties": {
|
||||
"platform": {
|
||||
"type": "string",
|
||||
"title": "Platform",
|
||||
"description": "Platform name: DISCORD, TELEGRAM, SLACK, TEAMS, WHATSAPP, GITHUB, LINEAR"
|
||||
"$ref": "#/components/schemas/Platform",
|
||||
"description": "Platform name"
|
||||
},
|
||||
"platform_user_id": {
|
||||
"type": "string",
|
||||
"maxLength": 255,
|
||||
"minLength": 1,
|
||||
"title": "Platform User Id",
|
||||
"description": "The user's ID on the platform"
|
||||
},
|
||||
"platform_username": {
|
||||
"anyOf": [{ "type": "string" }, { "type": "null" }],
|
||||
"anyOf": [
|
||||
{ "type": "string", "maxLength": 255 },
|
||||
{ "type": "null" }
|
||||
],
|
||||
"title": "Platform Username",
|
||||
"description": "Display name (best effort)"
|
||||
},
|
||||
"channel_id": {
|
||||
"anyOf": [{ "type": "string" }, { "type": "null" }],
|
||||
"anyOf": [
|
||||
{ "type": "string", "maxLength": 255 },
|
||||
{ "type": "null" }
|
||||
],
|
||||
"title": "Channel Id",
|
||||
"description": "Channel ID for sending confirmation back"
|
||||
}
|
||||
@@ -8945,6 +8970,12 @@
|
||||
"required": ["version_counts"],
|
||||
"title": "DeleteGraphResponse"
|
||||
},
|
||||
"DeleteLinkResponse": {
|
||||
"properties": { "success": { "type": "boolean", "title": "Success" } },
|
||||
"type": "object",
|
||||
"required": ["success"],
|
||||
"title": "DeleteLinkResponse"
|
||||
},
|
||||
"DiscoverToolsRequest": {
|
||||
"properties": {
|
||||
"server_url": {
|
||||
@@ -10732,7 +10763,11 @@
|
||||
},
|
||||
"LinkTokenStatusResponse": {
|
||||
"properties": {
|
||||
"status": { "type": "string", "title": "Status" },
|
||||
"status": {
|
||||
"type": "string",
|
||||
"enum": ["pending", "linked", "expired"],
|
||||
"title": "Status"
|
||||
},
|
||||
"user_id": {
|
||||
"anyOf": [{ "type": "string" }, { "type": "null" }],
|
||||
"title": "User Id"
|
||||
@@ -11581,6 +11616,19 @@
|
||||
"title": "PendingHumanReviewModel",
|
||||
"description": "Response model for pending human review data.\n\nRepresents a human review request that is awaiting user action.\nContains all necessary information for a user to review and approve\nor reject data from a Human-in-the-Loop block execution.\n\nAttributes:\n id: Unique identifier for the review record\n user_id: ID of the user who must perform the review\n node_exec_id: ID of the node execution that created this review\n node_id: ID of the node definition (for grouping reviews from same node)\n graph_exec_id: ID of the graph execution containing the node\n graph_id: ID of the graph template being executed\n graph_version: Version number of the graph template\n payload: The actual data payload awaiting review\n instructions: Instructions or message for the reviewer\n editable: Whether the reviewer can edit the data\n status: Current review status (WAITING, APPROVED, or REJECTED)\n review_message: Optional message from the reviewer\n created_at: Timestamp when review was created\n updated_at: Timestamp when review was last modified\n reviewed_at: Timestamp when review was completed (if applicable)"
|
||||
},
|
||||
"Platform": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"DISCORD",
|
||||
"TELEGRAM",
|
||||
"SLACK",
|
||||
"TEAMS",
|
||||
"WHATSAPP",
|
||||
"GITHUB",
|
||||
"LINEAR"
|
||||
],
|
||||
"title": "Platform"
|
||||
},
|
||||
"PlatformLinkInfo": {
|
||||
"properties": {
|
||||
"id": { "type": "string", "title": "Id" },
|
||||
@@ -12130,8 +12178,13 @@
|
||||
},
|
||||
"ResolveRequest": {
|
||||
"properties": {
|
||||
"platform": { "type": "string", "title": "Platform" },
|
||||
"platform_user_id": { "type": "string", "title": "Platform User Id" }
|
||||
"platform": { "$ref": "#/components/schemas/Platform" },
|
||||
"platform_user_id": {
|
||||
"type": "string",
|
||||
"maxLength": 255,
|
||||
"minLength": 1,
|
||||
"title": "Platform User Id"
|
||||
}
|
||||
},
|
||||
"type": "object",
|
||||
"required": ["platform", "platform_user_id"],
|
||||
@@ -12144,10 +12197,6 @@
|
||||
"user_id": {
|
||||
"anyOf": [{ "type": "string" }, { "type": "null" }],
|
||||
"title": "User Id"
|
||||
},
|
||||
"platform_username": {
|
||||
"anyOf": [{ "type": "string" }, { "type": "null" }],
|
||||
"title": "Platform Username"
|
||||
}
|
||||
},
|
||||
"type": "object",
|
||||
|
||||
Reference in New Issue
Block a user