Skip to content

Commit ad9ea52

Browse files
authored
feat(adapters): add Codex engine adapter with app-server JSON-RPC protocol (#412)
## Summary - CodexAdapter implementing AgentAdapter protocol with bidirectional JSON-RPC over stdio - Full 4-step handshake, turn streaming with event routing, auto-approval (configurable) - Timeout enforcement via selectors (turn, read, stall — no blocking readline deadlocks) - Token usage tracking, sandbox_mode forwarding, git-based modified file detection - Registered in engine_registry, CLI help text updated, OPENAI_API_KEY validator added - Shared git_utils.detect_modified_files() extracted from duplicated code ## Validation - Review feedback: All addressed (3 rounds — code reviewer, CodeRabbit, claude-review) - Demo: All 8 acceptance criteria verified - Tests: 24 unit tests, 1681 total core tests passing - CI: All checks green (Backend Tests, Lint, GitGuardian, claude-review, CodeRabbit) - Linting: Clean Closes #412
1 parent 81fe3fc commit ad9ea52

8 files changed

Lines changed: 887 additions & 42 deletions

File tree

codeframe/cli/app.py

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1998,7 +1998,7 @@ def work_start(
19981998
engine: Optional[str] = typer.Option(
19991999
None,
20002000
"--engine",
2001-
help="Agent engine: react (default), plan (legacy), claude-code, opencode, or built-in",
2001+
help="Agent engine: react (default), plan (legacy), claude-code, codex, opencode, or built-in",
20022002
),
20032003
stall_timeout: int = typer.Option(
20042004
300,
@@ -2059,10 +2059,12 @@ def work_start(
20592059
task = matching[0]
20602060

20612061
# Validate API key before creating run record (avoids dangling IN_PROGRESS state)
2062-
# External engines (claude-code, opencode) manage their own authentication
20632062
if execute:
20642063
from codeframe.core.engine_registry import is_external_engine
2065-
if not is_external_engine(engine):
2064+
if engine == "codex":
2065+
from codeframe.cli.validators import require_openai_api_key
2066+
require_openai_api_key()
2067+
elif not is_external_engine(engine):
20662068
from codeframe.cli.validators import require_anthropic_api_key
20672069
require_anthropic_api_key()
20682070

@@ -2888,7 +2890,7 @@ def batch_run(
28882890
engine: Optional[str] = typer.Option(
28892891
None,
28902892
"--engine",
2891-
help="Agent engine: react (default), plan (legacy), claude-code, opencode, or built-in",
2893+
help="Agent engine: react (default), plan (legacy), claude-code, codex, opencode, or built-in",
28922894
),
28932895
stall_timeout: int = typer.Option(
28942896
300,
@@ -2989,9 +2991,11 @@ def batch_run(
29892991
return
29902992

29912993
# Validate API key before batch execution
2992-
# External engines (claude-code, opencode) manage their own authentication
29932994
from codeframe.core.engine_registry import is_external_engine
2994-
if not is_external_engine(engine):
2995+
if engine == "codex":
2996+
from codeframe.cli.validators import require_openai_api_key
2997+
require_openai_api_key()
2998+
elif not is_external_engine(engine):
29952999
from codeframe.cli.validators import require_anthropic_api_key
29963000
require_anthropic_api_key()
29973001

codeframe/cli/validators.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,3 +46,40 @@ def require_anthropic_api_key() -> str:
4646
"Set it in your environment or add it to a .env file."
4747
)
4848
raise typer.Exit(1)
49+
50+
51+
def require_openai_api_key() -> str:
52+
"""Ensure OPENAI_API_KEY is available, loading from .env if needed.
53+
54+
Checks os.environ first. If not found, attempts to load from .env files
55+
(~/.env as base, then cwd/.env with override). If found after loading,
56+
sets in os.environ so subprocesses inherit it.
57+
58+
Returns:
59+
The API key string.
60+
61+
Raises:
62+
typer.Exit: If the key cannot be found anywhere.
63+
"""
64+
key = os.getenv("OPENAI_API_KEY")
65+
if key:
66+
return key
67+
68+
cwd_env = Path.cwd() / ".env"
69+
home_env = Path.home() / ".env"
70+
71+
if home_env.exists():
72+
load_dotenv(home_env)
73+
if cwd_env.exists():
74+
load_dotenv(cwd_env, override=True)
75+
76+
key = os.getenv("OPENAI_API_KEY")
77+
if key:
78+
os.environ["OPENAI_API_KEY"] = key
79+
return key
80+
81+
console.print(
82+
"[red]Error:[/red] OPENAI_API_KEY is not set. "
83+
"Set it in your environment or add it to a .env file."
84+
)
85+
raise typer.Exit(1)

codeframe/core/adapters/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
BuiltinReactAdapter,
1212
)
1313
from codeframe.core.adapters.claude_code import ClaudeCodeAdapter
14+
from codeframe.core.adapters.codex import CodexAdapter
1415
from codeframe.core.adapters.opencode import OpenCodeAdapter
1516
from codeframe.core.adapters.subprocess_adapter import SubprocessAdapter
1617
from codeframe.core.adapters.verification_wrapper import VerificationWrapper
@@ -25,6 +26,7 @@
2526
"BuiltinPlanAdapter",
2627
"BuiltinReactAdapter",
2728
"ClaudeCodeAdapter",
29+
"CodexAdapter",
2830
"OpenCodeAdapter",
2931
"SubprocessAdapter",
3032
"VerificationWrapper",

0 commit comments

Comments
 (0)