Skip to content

Commit 2f59c6b

Browse files
refactor: modularize code task model handling
1 parent a3f2595 commit 2f59c6b

File tree

5 files changed

+616
-405
lines changed

5 files changed

+616
-405
lines changed

server/utils/code_task/__init__.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
"""Model-specific helpers for AI code task execution."""
2+
3+
from . import claude, codex
4+
5+
MODEL_HANDLERS = {
6+
'claude': claude,
7+
'codex': codex,
8+
}
9+
10+
__all__ = ["MODEL_HANDLERS", "claude", "codex"]

server/utils/code_task/claude.py

Lines changed: 232 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,232 @@
1+
"""Claude-specific helpers for AI code tasks."""
2+
3+
import json
4+
import os
5+
from typing import Any, Dict, Iterable, List, Tuple
6+
7+
from .common import TaskContext, build_command as build_common_command
8+
9+
CONTAINER_IMAGE = "claude-code-automation:latest"
10+
11+
12+
def get_environment(user_preferences: Dict[str, Any]) -> Dict[str, str]:
13+
"""Return environment variables required for Claude execution."""
14+
15+
env = {
16+
"ANTHROPIC_API_KEY": os.getenv("ANTHROPIC_API_KEY"),
17+
"ANTHROPIC_NONINTERACTIVE": "1",
18+
}
19+
20+
claude_config = user_preferences.get("claudeCode", {})
21+
if isinstance(claude_config, dict):
22+
custom_env = claude_config.get("env", {})
23+
if isinstance(custom_env, dict):
24+
env.update(custom_env)
25+
26+
return {key: value for key, value in env.items() if value is not None}
27+
28+
29+
def extract_credentials(
30+
user_preferences: Dict[str, Any], task_id: int, logger
31+
) -> Tuple[str, str]:
32+
"""Load Claude credentials from user preferences."""
33+
34+
credentials_content = ""
35+
escaped_credentials = ""
36+
37+
claude_config = user_preferences.get("claudeCode", {})
38+
credentials_json = claude_config.get("credentials") if isinstance(claude_config, dict) else None
39+
40+
has_credentials = (
41+
credentials_json is not None
42+
and credentials_json != {}
43+
and credentials_json != ""
44+
and isinstance(credentials_json, dict)
45+
and len(credentials_json) > 0
46+
)
47+
48+
if has_credentials:
49+
try:
50+
credentials_content = json.dumps(credentials_json)
51+
logger.info(
52+
"📋 Successfully loaded Claude credentials from user preferences and stringified (%s characters) for task %s",
53+
len(credentials_content),
54+
task_id,
55+
)
56+
escaped_credentials = (
57+
credentials_content.replace("'", "'\"'\"'").replace("\n", "\\n")
58+
)
59+
logger.info("📋 Credentials content escaped for shell injection")
60+
except Exception as exc: # pylint: disable=broad-except
61+
logger.error("❌ Failed to process Claude credentials from user preferences: %s", exc)
62+
credentials_content = ""
63+
escaped_credentials = ""
64+
else:
65+
logger.info(
66+
"ℹ️ No meaningful Claude credentials found in user preferences for task %s - skipping credentials setup (credentials: %s)",
67+
task_id,
68+
credentials_json,
69+
)
70+
71+
return credentials_content, escaped_credentials
72+
73+
74+
def build_pre_model_lines(context: TaskContext) -> List[str]:
75+
"""Shell lines that configure Claude credentials."""
76+
77+
lines: List[str] = [
78+
"# Setup Claude credentials for Claude tasks",
79+
'echo "Setting up Claude credentials..."',
80+
"",
81+
"# Create ~/.claude directory if it doesn't exist",
82+
"mkdir -p ~/.claude",
83+
"",
84+
]
85+
86+
if context.credentials_content:
87+
lines.extend(
88+
[
89+
"# Write credentials content directly to file",
90+
'echo "📋 Writing credentials to ~/.claude/.credentials.json"',
91+
"cat << 'CREDENTIALS_EOF' > ~/.claude/.credentials.json",
92+
context.credentials_content,
93+
"CREDENTIALS_EOF",
94+
'echo "✅ Claude credentials configured"',
95+
]
96+
)
97+
else:
98+
lines.append('echo "⚠️ No credentials content available"')
99+
100+
lines.append("")
101+
return lines
102+
103+
104+
def build_model_lines(_: TaskContext) -> Iterable[str]:
105+
"""Claude CLI invocation logic."""
106+
107+
return [
108+
'echo "Using Claude CLI..."',
109+
"",
110+
"# Try different ways to invoke claude",
111+
'echo "Checking claude installation..."',
112+
"",
113+
"if [ -f /usr/local/bin/claude ]; then",
114+
' echo "Found claude at /usr/local/bin/claude"',
115+
' echo "File type:"',
116+
' file /usr/local/bin/claude || echo "file command not available"',
117+
' echo "First few lines:"',
118+
' head -5 /usr/local/bin/claude || echo "head command failed"',
119+
"",
120+
" # Check if it's a shell script",
121+
' if head -1 /usr/local/bin/claude | grep -q "#!/bin/sh\\|#!/bin/bash\\|#!/usr/bin/env bash"; then',
122+
' echo "Detected shell script, running with sh..."',
123+
' sh /usr/local/bin/claude < /tmp/prompt.txt',
124+
" # Check if it's a Node.js script (including env -S node pattern)",
125+
' elif head -1 /usr/local/bin/claude | grep -q "#!/usr/bin/env.*node\\|#!/usr/bin/node"; then',
126+
' echo "Detected Node.js script..."',
127+
' if command -v node >/dev/null 2>&1; then',
128+
' echo "Running with node..."',
129+
" # Try different approaches for Claude CLI",
130+
"",
131+
" # First try with --help to see available options",
132+
' echo "Checking claude options..."',
133+
' node /usr/local/bin/claude --help 2>/dev/null || echo "Help not available"',
134+
"",
135+
" # Try non-interactive approaches",
136+
' echo "Attempting non-interactive execution..."',
137+
"",
138+
" # Method 1: Use the official --print flag for non-interactive mode",
139+
' echo "Using --print flag for non-interactive mode..."',
140+
' cat /tmp/prompt.txt | node /usr/local/bin/claude --print --allowedTools "Edit,Bash"',
141+
' CLAUDE_EXIT_CODE=$?',
142+
' echo "Claude Code finished with exit code: $CLAUDE_EXIT_CODE"',
143+
"",
144+
' if [ $CLAUDE_EXIT_CODE -ne 0 ]; then',
145+
' echo "ERROR: Claude Code failed with exit code $CLAUDE_EXIT_CODE"',
146+
' exit $CLAUDE_EXIT_CODE',
147+
" fi",
148+
"",
149+
' echo "✅ Claude Code completed successfully"',
150+
" else",
151+
' echo "Node.js not found, trying direct execution..."',
152+
' /usr/local/bin/claude < /tmp/prompt.txt',
153+
' CLAUDE_EXIT_CODE=$?',
154+
' echo "Claude Code finished with exit code: $CLAUDE_EXIT_CODE"',
155+
' if [ $CLAUDE_EXIT_CODE -ne 0 ]; then',
156+
' echo "ERROR: Claude Code failed with exit code $CLAUDE_EXIT_CODE"',
157+
' exit $CLAUDE_EXIT_CODE',
158+
" fi",
159+
' echo "✅ Claude Code completed successfully"',
160+
" fi",
161+
" # Check if it's a Python script",
162+
' elif head -1 /usr/local/bin/claude | grep -q "#!/usr/bin/env python\\|#!/usr/bin/python"; then',
163+
' echo "Detected Python script..."',
164+
' if command -v python3 >/dev/null 2>&1; then',
165+
' echo "Running with python3..."',
166+
' python3 /usr/local/bin/claude < /tmp/prompt.txt',
167+
' CLAUDE_EXIT_CODE=$?',
168+
' elif command -v python >/dev/null 2>&1; then',
169+
' echo "Running with python..."',
170+
' python /usr/local/bin/claude < /tmp/prompt.txt',
171+
' CLAUDE_EXIT_CODE=$?',
172+
" else",
173+
' echo "Python not found, trying direct execution..."',
174+
' /usr/local/bin/claude < /tmp/prompt.txt',
175+
' CLAUDE_EXIT_CODE=$?',
176+
" fi",
177+
' echo "Claude Code finished with exit code: $CLAUDE_EXIT_CODE"',
178+
' if [ $CLAUDE_EXIT_CODE -ne 0 ]; then',
179+
' echo "ERROR: Claude Code failed with exit code $CLAUDE_EXIT_CODE"',
180+
' exit $CLAUDE_EXIT_CODE',
181+
" fi",
182+
' echo "✅ Claude Code completed successfully"',
183+
" else",
184+
' echo "Unknown script type, trying direct execution..."',
185+
' /usr/local/bin/claude < /tmp/prompt.txt',
186+
' CLAUDE_EXIT_CODE=$?',
187+
' echo "Claude Code finished with exit code: $CLAUDE_EXIT_CODE"',
188+
' if [ $CLAUDE_EXIT_CODE -ne 0 ]; then',
189+
' echo "ERROR: Claude Code failed with exit code $CLAUDE_EXIT_CODE"',
190+
' exit $CLAUDE_EXIT_CODE',
191+
" fi",
192+
' echo "✅ Claude Code completed successfully"',
193+
" fi",
194+
"elif command -v claude >/dev/null 2>&1; then",
195+
' echo "Using claude from PATH..."',
196+
' CLAUDE_PATH=$(which claude)',
197+
' echo "Claude found at: $CLAUDE_PATH"',
198+
' claude < /tmp/prompt.txt',
199+
' CLAUDE_EXIT_CODE=$?',
200+
' echo "Claude Code finished with exit code: $CLAUDE_EXIT_CODE"',
201+
' if [ $CLAUDE_EXIT_CODE -ne 0 ]; then',
202+
' echo "ERROR: Claude Code failed with exit code $CLAUDE_EXIT_CODE"',
203+
' exit $CLAUDE_EXIT_CODE',
204+
" fi",
205+
' echo "✅ Claude Code completed successfully"',
206+
"else",
207+
' echo "ERROR: claude command not found anywhere"',
208+
' echo "Checking available interpreters:"',
209+
' which python3 2>/dev/null && echo "python3: available" || echo "python3: not found"',
210+
' which python 2>/dev/null && echo "python: available" || echo "python: not found"',
211+
' which node 2>/dev/null && echo "node: available" || echo "node: not found"',
212+
' which sh 2>/dev/null && echo "sh: available" || echo "sh: not found"',
213+
" exit 1",
214+
"fi",
215+
"",
216+
]
217+
218+
219+
def get_container_overrides() -> Dict[str, Any]: # pragma: no cover - simple data
220+
"""Return Claude-specific container overrides (none required)."""
221+
222+
return {}
223+
224+
225+
def build_command(context: TaskContext) -> str:
226+
"""Build the Claude-specific container command."""
227+
228+
return build_common_command(
229+
context,
230+
pre_model_lines=build_pre_model_lines(context),
231+
model_lines=build_model_lines(context),
232+
)

server/utils/code_task/codex.py

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
"""Codex-specific helpers for AI code tasks."""
2+
3+
import fcntl
4+
import os
5+
import random
6+
import time
7+
from typing import Any, Dict, Iterable
8+
9+
from .common import TaskContext, build_command as build_common_command
10+
11+
CONTAINER_IMAGE = "codex-automation:latest"
12+
13+
14+
def get_environment(user_preferences: Dict[str, Any]) -> Dict[str, str]:
15+
"""Return environment variables required for Codex execution."""
16+
17+
env = {
18+
"OPENAI_API_KEY": os.getenv("OPENAI_API_KEY"),
19+
"OPENAI_NONINTERACTIVE": "1",
20+
"CODEX_QUIET_MODE": "1",
21+
"CODEX_UNSAFE_ALLOW_NO_SANDBOX": "1",
22+
"CODEX_DISABLE_SANDBOX": "1",
23+
"CODEX_NO_SANDBOX": "1",
24+
}
25+
26+
codex_config = user_preferences.get("codex", {})
27+
if isinstance(codex_config, dict):
28+
custom_env = codex_config.get("env", {})
29+
if isinstance(custom_env, dict):
30+
env.update(custom_env)
31+
32+
return {key: value for key, value in env.items() if value is not None}
33+
34+
35+
def prepare_for_run(task_id: int, logger) -> None:
36+
"""Mitigate Codex concurrency issues with delays and locking."""
37+
38+
stagger_delay = random.uniform(0.5, 2.0)
39+
logger.info("🕐 Adding %.1fs staggered start delay for Codex task %s", stagger_delay, task_id)
40+
time.sleep(stagger_delay)
41+
42+
lock_file_path = "/tmp/codex_execution_lock"
43+
try:
44+
logger.info("🔒 Acquiring Codex execution lock for task %s", task_id)
45+
with open(lock_file_path, "w", encoding="utf-8") as lock_file:
46+
fcntl.flock(lock_file.fileno(), fcntl.LOCK_EX | fcntl.LOCK_NB)
47+
logger.info("✅ Codex execution lock acquired for task %s", task_id)
48+
except (OSError, BlockingIOError) as exc: # pylint: disable=broad-except
49+
logger.warning("⚠️ Could not acquire Codex execution lock for task %s: %s", task_id, exc)
50+
additional_delay = random.uniform(1.0, 3.0)
51+
logger.info("🕐 Adding additional %.1fs delay due to lock conflict", additional_delay)
52+
time.sleep(additional_delay)
53+
54+
55+
def build_model_lines(_: TaskContext) -> Iterable[str]:
56+
"""Codex CLI invocation logic."""
57+
58+
return [
59+
'echo "Using Codex (OpenAI Codex) CLI..."',
60+
"",
61+
"# Set environment variables for non-interactive mode",
62+
"export CODEX_QUIET_MODE=1",
63+
"export CODEX_UNSAFE_ALLOW_NO_SANDBOX=1",
64+
"export CODEX_DISABLE_SANDBOX=1",
65+
"export CODEX_NO_SANDBOX=1",
66+
"",
67+
"# Debug: Verify environment variables are set",
68+
'echo "=== CODEX DEBUG INFO ==="',
69+
'echo "CODEX_QUIET_MODE: $CODEX_QUIET_MODE"',
70+
'echo "CODEX_UNSAFE_ALLOW_NO_SANDBOX: $CODEX_UNSAFE_ALLOW_NO_SANDBOX"',
71+
'echo "OPENAI_API_KEY: $(echo $OPENAI_API_KEY | head -c 8)..."',
72+
'echo "USING OFFICIAL CODEX FLAGS: --approval-mode full-auto --quiet for non-interactive operation"',
73+
'echo "======================="',
74+
"",
75+
"# Read the prompt from file",
76+
'PROMPT_TEXT=$(cat /tmp/prompt.txt)',
77+
"",
78+
"# Check for codex installation",
79+
'if [ -f /usr/local/bin/codex ]; then',
80+
' echo "Found codex at /usr/local/bin/codex"',
81+
' echo "Running Codex in non-interactive mode..."',
82+
"",
83+
" # Use official non-interactive flags for Docker environment",
84+
" # Using --approval-mode full-auto as per official Codex documentation",
85+
" # Also disable Codex's internal sandboxing to prevent conflicts with Docker",
86+
' /usr/local/bin/codex --approval-mode full-auto --quiet "$PROMPT_TEXT"',
87+
' CODEX_EXIT_CODE=$?',
88+
' echo "Codex finished with exit code: $CODEX_EXIT_CODE"',
89+
"",
90+
' if [ $CODEX_EXIT_CODE -ne 0 ]; then',
91+
' echo "ERROR: Codex failed with exit code $CODEX_EXIT_CODE"',
92+
' exit $CODEX_EXIT_CODE',
93+
" fi",
94+
"",
95+
' echo "✅ Codex completed successfully"',
96+
"elif command -v codex >/dev/null 2>&1; then",
97+
' echo "Using codex from PATH..."',
98+
' echo "Running Codex in non-interactive mode..."',
99+
"",
100+
" # Use official non-interactive flags for Docker environment",
101+
" # Using --approval-mode full-auto as per official Codex documentation",
102+
" # Also disable Codex's internal sandboxing to prevent conflicts with Docker",
103+
' codex --approval-mode full-auto --quiet "$PROMPT_TEXT"',
104+
' CODEX_EXIT_CODE=$?',
105+
' echo "Codex finished with exit code: $CODEX_EXIT_CODE"',
106+
"",
107+
' if [ $CODEX_EXIT_CODE -ne 0 ]; then',
108+
' echo "ERROR: Codex failed with exit code $CODEX_EXIT_CODE"',
109+
' exit $CODEX_EXIT_CODE',
110+
" fi",
111+
"",
112+
' echo "✅ Codex completed successfully"',
113+
"else",
114+
' echo "ERROR: codex command not found anywhere"',
115+
' echo "Please ensure Codex CLI is installed in the container"',
116+
" exit 1",
117+
"fi",
118+
"",
119+
]
120+
121+
122+
def get_container_overrides() -> Dict[str, Any]: # pragma: no cover - simple data
123+
"""Return Docker configuration overrides required for Codex."""
124+
125+
return {
126+
"security_opt": [
127+
"seccomp=unconfined",
128+
"apparmor=unconfined",
129+
"no-new-privileges=false",
130+
],
131+
"cap_add": ["ALL"],
132+
"privileged": True,
133+
"pid_mode": "host",
134+
}
135+
136+
137+
def build_command(context: TaskContext) -> str:
138+
"""Build the Codex-specific container command."""
139+
140+
return build_common_command(context, model_lines=build_model_lines(context))

0 commit comments

Comments
 (0)