Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
7650a89
feat: add CodexOAuthManager for OpenAI Codex PKCE OAuth flow
eureka928 Feb 2, 2026
c273996
feat: add Codex install/uninstall/save-token/status endpoints
eureka928 Feb 2, 2026
2d4c4bb
feat: add Codex config group and OPENAI_API_KEY env var mapping
eureka928 Feb 2, 2026
145b6a3
feat: add Codex installed detection, uninstall, and install handling
eureka928 Feb 2, 2026
79ef592
feat: add Codex OAuth install flow in Settings and AddWorker
eureka928 Feb 2, 2026
88d2f15
style: fix pre-commit lint errors (E501, yapf formatting)
eureka928 Feb 3, 2026
87339a4
fix: replace undefined Optional with int | None in CodexTokenRequest
eureka928 Feb 4, 2026
cd0544d
Update backend/app/controller/tool_controller.py
eureka928 Feb 5, 2026
ba140fe
refactor: address PR #1129 review comments
eureka928 Feb 5, 2026
54ba1a6
style: apply ruff formatting fixes
eureka928 Feb 5, 2026
2fa8833
feat: encrypt OAuth tokens using machine-derived key
eureka928 Feb 5, 2026
aa29c41
refactor: move imports to top of codex_toolkit.py
eureka928 Feb 5, 2026
1497f1a
test: add unit tests for codex_toolkit.py
eureka928 Feb 5, 2026
2699f6e
feat: add OAuth state parameter for CSRF protection
eureka928 Feb 5, 2026
2601f19
refactor: save Codex OAuth token as OpenAI provider instead of toolki…
eureka928 Feb 6, 2026
009e8e2
fix: address Codex OAuth review feedback
eureka928 Feb 7, 2026
be38ca5
fix: add missing blank line to pass ruff lint
eureka928 Feb 7, 2026
bf4d525
refactor: remove Codex from MCP config registry and integration hooks
eureka928 Feb 8, 2026
2884909
refactor: remove Codex OAuth flow from MCP and ToolSelect pages
eureka928 Feb 8, 2026
c245fb8
feat: add Codex OAuth connect to Model/Provider settings page
eureka928 Feb 8, 2026
1fc69bb
fix: remove unused Optional import to pass ruff lint
eureka928 Feb 8, 2026
d09ccba
refactor: move Codex OAuth manager out of agent/toolkit to utils
eureka928 Feb 9, 2026
b895160
refactor: move Codex OAuth endpoints from tool_controller to codex_co…
eureka928 Feb 10, 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
234 changes: 234 additions & 0 deletions backend/app/controller/codex_controller.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,234 @@
# ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. =========
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. =========

import logging
import time

from fastapi import APIRouter, HTTPException
from pydantic import BaseModel

from app.utils.codex_oauth import CodexOAuthManager
from app.utils.oauth_state_manager import oauth_state_manager


class CodexTokenRequest(BaseModel):
r"""Request model for saving Codex/OpenAI API token."""

access_token: str
expires_in: int | None = None


logger = logging.getLogger("codex_controller")
router = APIRouter()


@router.post("/codex/connect", name="connect codex")
async def connect_codex():
r"""Connect to Codex/OpenAI via OAuth PKCE flow.

Initiates or completes the Codex OAuth authorization flow
to obtain an OpenAI API key.

Returns:
Connection result with access token and provider info
"""
try:
if CodexOAuthManager.is_authenticated():
if CodexOAuthManager.is_token_expired():
# Try refreshing first
if CodexOAuthManager.refresh_token_if_needed():
return {
"success": True,
"message": "Codex token refreshed successfully",
"toolkit_name": "CodexOAuthManager",
"access_token": CodexOAuthManager.get_access_token(),
"provider_name": "openai",
"endpoint_url": "https://api.openai.com/v1",
}
# Refresh failed, start new auth
logger.info(
"Codex token expired and refresh failed, starting re-auth"
)
CodexOAuthManager.start_background_auth()
return {
"success": False,
"status": "authorizing",
"message": "Token expired. Browser should"
" open for re-authorization.",
"toolkit_name": "CodexOAuthManager",
"requires_auth": True,
}

return {
"success": True,
"message": "Codex/OpenAI is already authenticated",
"toolkit_name": "CodexOAuthManager",
"access_token": CodexOAuthManager.get_access_token(),
"provider_name": "openai",
"endpoint_url": "https://api.openai.com/v1",
}
else:
logger.info("No Codex credentials found, starting OAuth flow")
CodexOAuthManager.start_background_auth()
return {
"success": False,
"status": "authorizing",
"message": "Authorization required. Browser"
" should open automatically.",
"toolkit_name": "CodexOAuthManager",
"requires_auth": True,
}
except Exception as e:
logger.error(f"Failed to connect Codex: {e}")
raise HTTPException(
status_code=500, detail=f"Failed to connect Codex: {str(e)}"
)


@router.post("/codex/disconnect", name="disconnect codex")
async def disconnect_codex():
r"""Disconnect Codex/OpenAI and clean up authentication data.

Cancels any active OAuth flow and clears stored tokens.

Returns:
Disconnection result
"""
try:
# Cancel any active OAuth flow
state = oauth_state_manager.get_state("codex")
if state and state.status in ["pending", "authorizing"]:
state.cancel()
if hasattr(state, "server") and state.server:
try:
state.server.shutdown()
except Exception:
pass
oauth_state_manager._states.pop("codex", None)

success = CodexOAuthManager.clear_token()

if success:
return {
"success": True,
"message": (
"Successfully disconnected Codex"
" and cleaned up"
" authentication tokens"
),
}
else:
return {
"success": True,
"message": "Disconnected Codex (no tokens found to clean up)",
}
except Exception as e:
logger.error(f"Failed to disconnect Codex: {e}")
raise HTTPException(
status_code=500, detail=f"Failed to disconnect Codex: {str(e)}"
)


@router.post("/codex/save-token", name="save codex token")
async def save_codex_token(token_request: CodexTokenRequest):
r"""Save Codex/OpenAI API token (manual API key entry fallback).

Args:
token_request: Token data containing access_token
and optionally expires_in

Returns:
Save result
"""
try:
token_data = token_request.model_dump(exclude_none=True)
token_data["manual"] = True

success = CodexOAuthManager.save_token(token_data)

if success:
return {
"success": True,
"message": "Codex token saved successfully",
}
else:
raise HTTPException(
status_code=500, detail="Failed to save Codex token"
)
except HTTPException:
raise
except Exception as e:
logger.error(f"Failed to save Codex token: {e}")
raise HTTPException(
status_code=500, detail=f"Failed to save token: {str(e)}"
)


@router.get("/codex/status", name="get codex status")
async def get_codex_status():
r"""Get current Codex/OpenAI authentication status and token info.

Returns:
Status information including authentication state and token expiry
"""
try:
is_authenticated = CodexOAuthManager.is_authenticated()

if not is_authenticated:
return {
"authenticated": False,
"status": "not_configured",
"message": "Codex not configured. OAuth or API key required.",
}

token_info = CodexOAuthManager.get_token_info()
is_expired = CodexOAuthManager.is_token_expired()
is_expiring_soon = CodexOAuthManager.is_token_expiring_soon()

result = {
"authenticated": True,
"status": "expired"
if is_expired
else ("expiring_soon" if is_expiring_soon else "valid"),
}

if token_info:
if token_info.get("expires_at"):
current_time = int(time.time())
expires_at = token_info["expires_at"]
seconds_remaining = max(0, expires_at - current_time)
result["expires_at"] = expires_at
result["seconds_remaining"] = seconds_remaining

if token_info.get("saved_at"):
result["saved_at"] = token_info["saved_at"]

if token_info.get("manual"):
result["manual"] = True

if is_expired:
result["message"] = "Token has expired. Please re-authenticate."
elif is_expiring_soon:
result["message"] = (
"Token is expiring soon. Consider re-authenticating."
)
else:
result["message"] = "Codex/OpenAI is connected and token is valid."

return result
except Exception as e:
logger.error(f"Failed to get Codex status: {e}")
raise HTTPException(
status_code=500, detail=f"Failed to get status: {str(e)}"
)
6 changes: 6 additions & 0 deletions backend/app/router.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@

from app.controller import (
chat_controller,
codex_controller,
health_controller,
model_controller,
task_controller,
Expand Down Expand Up @@ -71,6 +72,11 @@ def register_routers(app: FastAPI, prefix: str = "") -> None:
"tags": ["tool"],
"description": "Tool installation and management",
},
{
"router": codex_controller.router,
"tags": ["codex"],
"description": "Codex OAuth provider authentication",
},
]

for config in routers_config:
Expand Down
Loading