diff --git a/autogpt_platform/backend/backend/data/model.py b/autogpt_platform/backend/backend/data/model.py index 1c32a9f444..ced7ff9436 100644 --- a/autogpt_platform/backend/backend/data/model.py +++ b/autogpt_platform/backend/backend/data/model.py @@ -433,6 +433,15 @@ class OAuthState(BaseModel): code_verifier: Optional[str] = None """Unix timestamp (seconds) indicating when this OAuth state expires""" scopes: list[str] + # Fields for external API OAuth flows + callback_url: Optional[str] = None + """External app's callback URL for OAuth redirect""" + state_metadata: dict[str, Any] = Field(default_factory=dict) + """Metadata to echo back to external app on completion""" + api_key_id: Optional[str] = None + """ID of the API key that initiated this OAuth flow""" + is_external: bool = False + """Whether this OAuth flow was initiated via external API""" class UserMetadata(BaseModel): diff --git a/autogpt_platform/backend/backend/integrations/credentials_store.py b/autogpt_platform/backend/backend/integrations/credentials_store.py index 75ae346d5d..405c57cb05 100644 --- a/autogpt_platform/backend/backend/integrations/credentials_store.py +++ b/autogpt_platform/backend/backend/integrations/credentials_store.py @@ -399,7 +399,15 @@ class IntegrationCredentialsStore: # ===================== OAUTH STATES ===================== # async def store_state_token( - self, user_id: str, provider: str, scopes: list[str], use_pkce: bool = False + self, + user_id: str, + provider: str, + scopes: list[str], + use_pkce: bool = False, + # New parameters for external API OAuth flows + callback_url: Optional[str] = None, + state_metadata: Optional[dict] = None, + api_key_id: Optional[str] = None, ) -> tuple[str, str]: token = secrets.token_urlsafe(32) expires_at = datetime.now(timezone.utc) + timedelta(minutes=10) @@ -412,6 +420,11 @@ class IntegrationCredentialsStore: code_verifier=code_verifier, expires_at=int(expires_at.timestamp()), scopes=scopes, + # External API OAuth flow fields + callback_url=callback_url, + state_metadata=state_metadata or {}, + api_key_id=api_key_id, + is_external=callback_url is not None, ) async with self.edit_user_integrations(user_id) as user_integrations: diff --git a/autogpt_platform/backend/backend/integrations/oauth/github.py b/autogpt_platform/backend/backend/integrations/oauth/github.py index ebec116660..606212abf1 100644 --- a/autogpt_platform/backend/backend/integrations/oauth/github.py +++ b/autogpt_platform/backend/backend/integrations/oauth/github.py @@ -25,6 +25,7 @@ class GitHubOAuthHandler(BaseOAuthHandler): """ # noqa PROVIDER_NAME = ProviderName.GITHUB + DEFAULT_SCOPES = ["repo"] def __init__(self, client_id: str, client_secret: str, redirect_uri: str): self.client_id = client_id diff --git a/autogpt_platform/backend/backend/server/external/api.py b/autogpt_platform/backend/backend/server/external/api.py index f865d69aef..3bafa63108 100644 --- a/autogpt_platform/backend/backend/server/external/api.py +++ b/autogpt_platform/backend/backend/server/external/api.py @@ -3,6 +3,7 @@ from fastapi import FastAPI from backend.monitoring.instrumentation import instrument_fastapi from backend.server.middleware.security import SecurityHeadersMiddleware +from .routes.integrations import integrations_router from .routes.tools import tools_router from .routes.v1 import v1_router @@ -16,6 +17,7 @@ external_app = FastAPI( external_app.add_middleware(SecurityHeadersMiddleware) external_app.include_router(v1_router, prefix="/v1") external_app.include_router(tools_router, prefix="/v1") +external_app.include_router(integrations_router, prefix="/v1") # Add Prometheus instrumentation instrument_fastapi( diff --git a/autogpt_platform/backend/backend/server/external/routes/integrations.py b/autogpt_platform/backend/backend/server/external/routes/integrations.py new file mode 100644 index 0000000000..ff9a260f0e --- /dev/null +++ b/autogpt_platform/backend/backend/server/external/routes/integrations.py @@ -0,0 +1,616 @@ +""" +External API endpoints for integrations and credentials. + +This module provides endpoints for external applications (like Autopilot) to: +- Initiate OAuth flows with custom callback URLs +- Complete OAuth flows by exchanging authorization codes +- Create API key, user/password, and host-scoped credentials +- List and manage user credentials +""" + +import logging +from typing import TYPE_CHECKING, Annotated, Any, Literal, Optional, Union +from urllib.parse import urlparse + +from fastapi import APIRouter, Body, HTTPException, Path, Query, Security, status +from prisma.enums import APIKeyPermission +from pydantic import BaseModel, Field, SecretStr + +from backend.data.api_key import APIKeyInfo +from backend.data.model import ( + APIKeyCredentials, + Credentials, + CredentialsType, + HostScopedCredentials, + OAuth2Credentials, + UserPasswordCredentials, +) +from backend.integrations.creds_manager import IntegrationCredentialsManager +from backend.integrations.oauth import CREDENTIALS_BY_PROVIDER, HANDLERS_BY_NAME +from backend.integrations.providers import ProviderName +from backend.server.external.middleware import require_permission +from backend.server.integrations.models import get_all_provider_names +from backend.util.settings import Settings + +if TYPE_CHECKING: + from backend.integrations.oauth import BaseOAuthHandler + +logger = logging.getLogger(__name__) +settings = Settings() +creds_manager = IntegrationCredentialsManager() + +integrations_router = APIRouter(prefix="/integrations", tags=["integrations"]) + + +# ==================== Request/Response Models ==================== # + + +class OAuthInitiateRequest(BaseModel): + """Request model for initiating an OAuth flow.""" + + callback_url: str = Field( + ..., description="The external app's callback URL for OAuth redirect" + ) + scopes: list[str] = Field( + default_factory=list, description="OAuth scopes to request" + ) + state_metadata: dict[str, Any] = Field( + default_factory=dict, + description="Arbitrary metadata to echo back on completion", + ) + + +class OAuthInitiateResponse(BaseModel): + """Response model for OAuth initiation.""" + + login_url: str = Field(..., description="URL to redirect user for OAuth consent") + state_token: str = Field(..., description="State token for CSRF protection") + expires_at: int = Field( + ..., description="Unix timestamp when the state token expires" + ) + + +class OAuthCompleteRequest(BaseModel): + """Request model for completing an OAuth flow.""" + + code: str = Field(..., description="Authorization code from OAuth provider") + state_token: str = Field(..., description="State token from initiate request") + + +class OAuthCompleteResponse(BaseModel): + """Response model for OAuth completion.""" + + credentials_id: str = Field(..., description="ID of the stored credentials") + provider: str = Field(..., description="Provider name") + type: str = Field(..., description="Credential type (oauth2)") + title: Optional[str] = Field(None, description="Credential title") + scopes: list[str] = Field(default_factory=list, description="Granted scopes") + username: Optional[str] = Field(None, description="Username from provider") + state_metadata: dict[str, Any] = Field( + default_factory=dict, description="Echoed metadata from initiate request" + ) + + +class CredentialSummary(BaseModel): + """Summary of a credential without sensitive data.""" + + id: str + provider: str + type: CredentialsType + title: Optional[str] = None + scopes: Optional[list[str]] = None + username: Optional[str] = None + host: Optional[str] = None + + +class ProviderInfo(BaseModel): + """Information about an integration provider.""" + + name: str + supports_oauth: bool = False + supports_api_key: bool = False + supports_user_password: bool = False + supports_host_scoped: bool = False + default_scopes: list[str] = Field(default_factory=list) + + +# ==================== Credential Creation Models ==================== # + + +class CreateAPIKeyCredentialRequest(BaseModel): + """Request model for creating API key credentials.""" + + type: Literal["api_key"] = "api_key" + api_key: str = Field(..., description="The API key") + title: str = Field(..., description="A name for this credential") + expires_at: Optional[int] = Field( + None, description="Unix timestamp when the API key expires" + ) + + +class CreateUserPasswordCredentialRequest(BaseModel): + """Request model for creating username/password credentials.""" + + type: Literal["user_password"] = "user_password" + username: str = Field(..., description="Username") + password: str = Field(..., description="Password") + title: str = Field(..., description="A name for this credential") + + +class CreateHostScopedCredentialRequest(BaseModel): + """Request model for creating host-scoped credentials.""" + + type: Literal["host_scoped"] = "host_scoped" + host: str = Field(..., description="Host/domain pattern to match") + headers: dict[str, str] = Field(..., description="Headers to include in requests") + title: str = Field(..., description="A name for this credential") + + +# Union type for credential creation +CreateCredentialRequest = Annotated[ + CreateAPIKeyCredentialRequest + | CreateUserPasswordCredentialRequest + | CreateHostScopedCredentialRequest, + Field(discriminator="type"), +] + + +class CreateCredentialResponse(BaseModel): + """Response model for credential creation.""" + + id: str + provider: str + type: CredentialsType + title: Optional[str] = None + + +# ==================== Helper Functions ==================== # + + +def validate_callback_url(callback_url: str) -> bool: + """Validate that the callback URL is from an allowed origin.""" + allowed_origins = settings.config.external_oauth_callback_origins + + try: + parsed = urlparse(callback_url) + callback_origin = f"{parsed.scheme}://{parsed.netloc}" + + for allowed in allowed_origins: + # Simple origin matching + if callback_origin == allowed: + return True + # Allow localhost with any port in development + if "localhost" in allowed and "localhost" in callback_origin: + return True + + return False + except Exception: + return False + + +def _get_oauth_handler_for_external( + provider_name: str, redirect_uri: str +) -> "BaseOAuthHandler": + """Get an OAuth handler configured with an external redirect URI.""" + # Ensure blocks are loaded so SDK providers are available + try: + from backend.blocks import load_all_blocks + + load_all_blocks() + except Exception as e: + logger.warning(f"Failed to load blocks: {e}") + + if provider_name not in HANDLERS_BY_NAME: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=f"Provider '{provider_name}' does not support OAuth", + ) + + # Check if this provider has custom OAuth credentials + oauth_credentials = CREDENTIALS_BY_PROVIDER.get(provider_name) + + if oauth_credentials and not oauth_credentials.use_secrets: + import os + + client_id = ( + os.getenv(oauth_credentials.client_id_env_var) + if oauth_credentials.client_id_env_var + else None + ) + client_secret = ( + os.getenv(oauth_credentials.client_secret_env_var) + if oauth_credentials.client_secret_env_var + else None + ) + else: + client_id = getattr(settings.secrets, f"{provider_name}_client_id", None) + client_secret = getattr(settings.secrets, f"{provider_name}_client_secret", None) + + if not (client_id and client_secret): + logger.error(f"Attempt to use unconfigured {provider_name} OAuth integration") + raise HTTPException( + status_code=status.HTTP_501_NOT_IMPLEMENTED, + detail={ + "message": f"Integration with provider '{provider_name}' is not configured.", + "hint": "Set client ID and secret in the application's deployment environment", + }, + ) + + handler_class = HANDLERS_BY_NAME[provider_name] + return handler_class( + client_id=client_id, + client_secret=client_secret, + redirect_uri=redirect_uri, + ) + + +# ==================== Endpoints ==================== # + + +@integrations_router.get("/providers", response_model=list[ProviderInfo]) +async def list_providers( + api_key: APIKeyInfo = Security(require_permission(APIKeyPermission.READ_INTEGRATIONS)), +) -> list[ProviderInfo]: + """ + List all available integration providers. + + Returns a list of all providers with their supported credential types. + Most providers support API key credentials, and some also support OAuth. + """ + # Ensure blocks are loaded + try: + from backend.blocks import load_all_blocks + + load_all_blocks() + except Exception as e: + logger.warning(f"Failed to load blocks: {e}") + + providers = [] + for name in get_all_provider_names(): + supports_oauth = name in HANDLERS_BY_NAME + handler_class = HANDLERS_BY_NAME.get(name) + default_scopes = getattr(handler_class, "DEFAULT_SCOPES", []) if handler_class else [] + + providers.append( + ProviderInfo( + name=name, + supports_oauth=supports_oauth, + supports_api_key=True, # All providers can accept API keys + supports_user_password=name in ("smtp",), # SMTP uses user/password + supports_host_scoped=name == "http", # HTTP block uses host-scoped + default_scopes=default_scopes, + ) + ) + + return providers + + +@integrations_router.post( + "/{provider}/oauth/initiate", + response_model=OAuthInitiateResponse, + summary="Initiate OAuth flow", +) +async def initiate_oauth( + provider: Annotated[str, Path(title="The OAuth provider")], + request: OAuthInitiateRequest, + api_key: APIKeyInfo = Security( + require_permission(APIKeyPermission.MANAGE_INTEGRATIONS) + ), +) -> OAuthInitiateResponse: + """ + Initiate an OAuth flow for an external application. + + This endpoint allows external apps to start an OAuth flow with a custom + callback URL. The callback URL must be from an allowed origin configured + in the platform settings. + + Returns a login URL to redirect the user to, along with a state token + for CSRF protection. + """ + # Validate callback URL + if not validate_callback_url(request.callback_url): + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=f"Callback URL origin is not allowed. Allowed origins: {settings.config.external_oauth_callback_origins}", + ) + + # Validate provider + try: + provider_name = ProviderName(provider) + except ValueError: + # Check if it's a dynamically registered provider + if provider not in HANDLERS_BY_NAME: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=f"Provider '{provider}' not found", + ) + provider_name = provider + + # Get OAuth handler with external callback URL + handler = _get_oauth_handler_for_external( + provider if isinstance(provider_name, str) else provider_name.value, + request.callback_url, + ) + + # Store state token with external flow metadata + state_token, code_challenge = await creds_manager.store.store_state_token( + user_id=api_key.user_id, + provider=provider if isinstance(provider_name, str) else provider_name.value, + scopes=request.scopes, + callback_url=request.callback_url, + state_metadata=request.state_metadata, + api_key_id=api_key.id, + ) + + # Build login URL + login_url = handler.get_login_url( + request.scopes, state_token, code_challenge=code_challenge + ) + + # Calculate expiration (10 minutes from now) + from datetime import datetime, timedelta, timezone + + expires_at = int((datetime.now(timezone.utc) + timedelta(minutes=10)).timestamp()) + + return OAuthInitiateResponse( + login_url=login_url, + state_token=state_token, + expires_at=expires_at, + ) + + +@integrations_router.post( + "/{provider}/oauth/complete", + response_model=OAuthCompleteResponse, + summary="Complete OAuth flow", +) +async def complete_oauth( + provider: Annotated[str, Path(title="The OAuth provider")], + request: OAuthCompleteRequest, + api_key: APIKeyInfo = Security( + require_permission(APIKeyPermission.MANAGE_INTEGRATIONS) + ), +) -> OAuthCompleteResponse: + """ + Complete an OAuth flow by exchanging the authorization code for tokens. + + This endpoint should be called after the user has authorized the application + and been redirected back to the external app's callback URL with an + authorization code. + """ + # Verify state token + valid_state = await creds_manager.store.verify_state_token( + api_key.user_id, request.state_token, provider + ) + + if not valid_state: + logger.warning(f"Invalid or expired state token for user {api_key.user_id}") + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="Invalid or expired state token", + ) + + # Verify this is an external flow + if not valid_state.is_external: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="State token was not created for external OAuth flow", + ) + + # Get OAuth handler with the original callback URL + handler = _get_oauth_handler_for_external(provider, valid_state.callback_url) + + try: + scopes = valid_state.scopes + scopes = handler.handle_default_scopes(scopes) + + credentials = await handler.exchange_code_for_tokens( + request.code, scopes, valid_state.code_verifier + ) + + # Handle Linear's space-separated scopes + if len(credentials.scopes) == 1 and " " in credentials.scopes[0]: + credentials.scopes = credentials.scopes[0].split(" ") + + # Check scope mismatch + if not set(scopes).issubset(set(credentials.scopes)): + logger.warning( + f"Granted scopes {credentials.scopes} for provider {provider} " + f"do not include all requested scopes {scopes}" + ) + + except Exception as e: + logger.error(f"OAuth2 Code->Token exchange failed for provider {provider}: {e}") + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=f"OAuth2 callback failed to exchange code for tokens: {str(e)}", + ) + + # Store credentials + await creds_manager.create(api_key.user_id, credentials) + + logger.info( + f"Successfully completed external OAuth for user {api_key.user_id} " + f"and provider {provider}" + ) + + return OAuthCompleteResponse( + credentials_id=credentials.id, + provider=credentials.provider, + type=credentials.type, + title=credentials.title, + scopes=credentials.scopes, + username=credentials.username, + state_metadata=valid_state.state_metadata, + ) + + +@integrations_router.get("/credentials", response_model=list[CredentialSummary]) +async def list_credentials( + api_key: APIKeyInfo = Security(require_permission(APIKeyPermission.READ_INTEGRATIONS)), +) -> list[CredentialSummary]: + """ + List all credentials for the authenticated user. + + Returns metadata about each credential without exposing sensitive tokens. + """ + credentials = await creds_manager.store.get_all_creds(api_key.user_id) + return [ + CredentialSummary( + id=cred.id, + provider=cred.provider, + type=cred.type, + title=cred.title, + scopes=cred.scopes if isinstance(cred, OAuth2Credentials) else None, + username=cred.username if isinstance(cred, OAuth2Credentials) else None, + host=cred.host if isinstance(cred, HostScopedCredentials) else None, + ) + for cred in credentials + ] + + +@integrations_router.get( + "/{provider}/credentials", response_model=list[CredentialSummary] +) +async def list_credentials_by_provider( + provider: Annotated[str, Path(title="The provider to list credentials for")], + api_key: APIKeyInfo = Security(require_permission(APIKeyPermission.READ_INTEGRATIONS)), +) -> list[CredentialSummary]: + """ + List credentials for a specific provider. + """ + credentials = await creds_manager.store.get_creds_by_provider( + api_key.user_id, provider + ) + return [ + CredentialSummary( + id=cred.id, + provider=cred.provider, + type=cred.type, + title=cred.title, + scopes=cred.scopes if isinstance(cred, OAuth2Credentials) else None, + username=cred.username if isinstance(cred, OAuth2Credentials) else None, + host=cred.host if isinstance(cred, HostScopedCredentials) else None, + ) + for cred in credentials + ] + + +@integrations_router.post( + "/{provider}/credentials", + response_model=CreateCredentialResponse, + status_code=status.HTTP_201_CREATED, + summary="Create credentials", +) +async def create_credential( + provider: Annotated[str, Path(title="The provider to create credentials for")], + request: Union[ + CreateAPIKeyCredentialRequest, + CreateUserPasswordCredentialRequest, + CreateHostScopedCredentialRequest, + ] = Body(..., discriminator="type"), + api_key: APIKeyInfo = Security( + require_permission(APIKeyPermission.MANAGE_INTEGRATIONS) + ), +) -> CreateCredentialResponse: + """ + Create non-OAuth credentials for a provider. + + Supports creating: + - API key credentials (type: "api_key") + - Username/password credentials (type: "user_password") + - Host-scoped credentials (type: "host_scoped") + + For OAuth credentials, use the OAuth initiate/complete flow instead. + """ + # Validate provider exists + all_providers = get_all_provider_names() + if provider not in all_providers: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=f"Provider '{provider}' not found", + ) + + # Create the appropriate credential type + credentials: Credentials + if request.type == "api_key": + credentials = APIKeyCredentials( + provider=provider, + api_key=SecretStr(request.api_key), + title=request.title, + expires_at=request.expires_at, + ) + elif request.type == "user_password": + credentials = UserPasswordCredentials( + provider=provider, + username=SecretStr(request.username), + password=SecretStr(request.password), + title=request.title, + ) + elif request.type == "host_scoped": + # Convert string headers to SecretStr + secret_headers = {k: SecretStr(v) for k, v in request.headers.items()} + credentials = HostScopedCredentials( + provider=provider, + host=request.host, + headers=secret_headers, + title=request.title, + ) + else: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=f"Unsupported credential type: {request.type}", + ) + + # Store credentials + try: + await creds_manager.create(api_key.user_id, credentials) + except Exception as e: + logger.error(f"Failed to store credentials: {e}") + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=f"Failed to store credentials: {str(e)}", + ) + + logger.info( + f"Created {request.type} credentials for user {api_key.user_id} " + f"and provider {provider}" + ) + + return CreateCredentialResponse( + id=credentials.id, + provider=provider, + type=credentials.type, + title=credentials.title, + ) + + +@integrations_router.delete("/{provider}/credentials/{cred_id}") +async def delete_credential( + provider: Annotated[str, Path(title="The provider")], + cred_id: Annotated[str, Path(title="The credential ID to delete")], + api_key: APIKeyInfo = Security( + require_permission(APIKeyPermission.DELETE_INTEGRATIONS) + ), +) -> dict: + """ + Delete a credential. + + Note: This does not revoke the tokens with the provider. For full cleanup, + use the main API's delete endpoint which handles webhook cleanup and + token revocation. + """ + creds = await creds_manager.store.get_creds_by_id(api_key.user_id, cred_id) + if not creds: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, detail="Credentials not found" + ) + if creds.provider != provider: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="Credentials do not match the specified provider", + ) + + await creds_manager.delete(api_key.user_id, cred_id) + + return {"deleted": True, "credentials_id": cred_id} diff --git a/autogpt_platform/backend/backend/util/settings.py b/autogpt_platform/backend/backend/util/settings.py index fcd009f820..3d8e28e8d4 100644 --- a/autogpt_platform/backend/backend/util/settings.py +++ b/autogpt_platform/backend/backend/util/settings.py @@ -439,6 +439,12 @@ class Config(UpdateTrackingModel["Config"], BaseSettings): '"regex:" to match via regular expression.', ) + external_oauth_callback_origins: List[str] = Field( + default=["http://localhost:3000"], + description="Allowed callback URL origins for external OAuth flows. " + "External apps (like Autopilot) must have their callback URLs start with one of these origins.", + ) + @field_validator("backend_cors_allow_origins") @classmethod def validate_cors_allow_origins(cls, v: List[str]) -> List[str]: diff --git a/autogpt_platform/backend/migrations/20251127144817_add_api_key_integration_permissions/migration.sql b/autogpt_platform/backend/migrations/20251127144817_add_api_key_integration_permissions/migration.sql new file mode 100644 index 0000000000..f4b1671add --- /dev/null +++ b/autogpt_platform/backend/migrations/20251127144817_add_api_key_integration_permissions/migration.sql @@ -0,0 +1,11 @@ +-- AlterEnum +-- This migration adds more than one value to an enum. +-- With PostgreSQL versions 11 and earlier, this is not possible +-- in a single migration. This can be worked around by creating +-- multiple migrations, each migration adding only one value to +-- the enum. + + +ALTER TYPE "APIKeyPermission" ADD VALUE 'MANAGE_INTEGRATIONS'; +ALTER TYPE "APIKeyPermission" ADD VALUE 'READ_INTEGRATIONS'; +ALTER TYPE "APIKeyPermission" ADD VALUE 'DELETE_INTEGRATIONS'; diff --git a/autogpt_platform/backend/schema.prisma b/autogpt_platform/backend/schema.prisma index 7ebfa54905..b3d0daf8e2 100644 --- a/autogpt_platform/backend/schema.prisma +++ b/autogpt_platform/backend/schema.prisma @@ -912,6 +912,9 @@ enum APIKeyPermission { READ_BLOCK // Can get block information READ_STORE // Can read store agents and creators USE_TOOLS // Can use chat tools via external API + MANAGE_INTEGRATIONS // Can initiate OAuth flows and complete them + READ_INTEGRATIONS // Can list credentials and providers + DELETE_INTEGRATIONS // Can delete credentials } model APIKey { diff --git a/autogpt_platform/frontend/src/app/api/openapi.json b/autogpt_platform/frontend/src/app/api/openapi.json index 5b4ebaa547..4f59e1668c 100644 --- a/autogpt_platform/frontend/src/app/api/openapi.json +++ b/autogpt_platform/frontend/src/app/api/openapi.json @@ -5297,7 +5297,10 @@ "EXECUTE_BLOCK", "READ_BLOCK", "READ_STORE", - "USE_TOOLS" + "USE_TOOLS", + "MANAGE_INTEGRATIONS", + "READ_INTEGRATIONS", + "DELETE_INTEGRATIONS" ], "title": "APIKeyPermission" },