Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
10 changes: 10 additions & 0 deletions server/utils/code_task/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
"""Model-specific helpers for AI code task execution."""

from . import claude, codex

MODEL_HANDLERS = {
'claude': claude,
'codex': codex,
}

__all__ = ["MODEL_HANDLERS", "claude", "codex"]
232 changes: 232 additions & 0 deletions server/utils/code_task/claude.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,232 @@
"""Claude-specific helpers for AI code tasks."""

import json
import os
from typing import Any, Dict, Iterable, List, Tuple

from .common import TaskContext, build_command as build_common_command

CONTAINER_IMAGE = "claude-code-automation:latest"


def get_environment(user_preferences: Dict[str, Any]) -> Dict[str, str]:
"""Return environment variables required for Claude execution."""

env = {
"ANTHROPIC_API_KEY": os.getenv("ANTHROPIC_API_KEY"),
"ANTHROPIC_NONINTERACTIVE": "1",
}

claude_config = user_preferences.get("claudeCode", {})
if isinstance(claude_config, dict):
custom_env = claude_config.get("env", {})
if isinstance(custom_env, dict):
env.update(custom_env)

return {key: value for key, value in env.items() if value is not None}


def extract_credentials(
user_preferences: Dict[str, Any], task_id: int, logger
) -> Tuple[str, str]:
"""Load Claude credentials from user preferences."""

credentials_content = ""
escaped_credentials = ""

claude_config = user_preferences.get("claudeCode", {})
credentials_json = claude_config.get("credentials") if isinstance(claude_config, dict) else None

has_credentials = (
credentials_json is not None
and credentials_json != {}
and credentials_json != ""
and isinstance(credentials_json, dict)
and len(credentials_json) > 0
)

if has_credentials:
try:
credentials_content = json.dumps(credentials_json)
logger.info(
"📋 Successfully loaded Claude credentials from user preferences and stringified (%s characters) for task %s",
len(credentials_content),
task_id,
)
escaped_credentials = (
credentials_content.replace("'", "'\"'\"'").replace("\n", "\\n")
)
logger.info("📋 Credentials content escaped for shell injection")
except Exception as exc: # pylint: disable=broad-except
logger.error("❌ Failed to process Claude credentials from user preferences: %s", exc)
credentials_content = ""
escaped_credentials = ""
else:
logger.info(
"ℹ️ No meaningful Claude credentials found in user preferences for task %s - skipping credentials setup (credentials: %s)",
task_id,
credentials_json,
)

return credentials_content, escaped_credentials


def build_pre_model_lines(context: TaskContext) -> List[str]:
"""Shell lines that configure Claude credentials."""

lines: List[str] = [
"# Setup Claude credentials for Claude tasks",
'echo "Setting up Claude credentials..."',
"",
"# Create ~/.claude directory if it doesn't exist",
"mkdir -p ~/.claude",
"",
]

if context.credentials_content:
lines.extend(
[
"# Write credentials content directly to file",
'echo "📋 Writing credentials to ~/.claude/.credentials.json"',
"cat << 'CREDENTIALS_EOF' > ~/.claude/.credentials.json",
context.credentials_content,
"CREDENTIALS_EOF",
'echo "✅ Claude credentials configured"',
]
)
else:
lines.append('echo "⚠️ No credentials content available"')

lines.append("")
return lines


def build_model_lines(_: TaskContext) -> Iterable[str]:
"""Claude CLI invocation logic."""

return [
'echo "Using Claude CLI..."',
"",
"# Try different ways to invoke claude",
'echo "Checking claude installation..."',
"",
"if [ -f /usr/local/bin/claude ]; then",
' echo "Found claude at /usr/local/bin/claude"',
' echo "File type:"',
' file /usr/local/bin/claude || echo "file command not available"',
' echo "First few lines:"',
' head -5 /usr/local/bin/claude || echo "head command failed"',
"",
" # Check if it's a shell script",
' if head -1 /usr/local/bin/claude | grep -q "#!/bin/sh\\|#!/bin/bash\\|#!/usr/bin/env bash"; then',
' echo "Detected shell script, running with sh..."',
' sh /usr/local/bin/claude < /tmp/prompt.txt',
" # Check if it's a Node.js script (including env -S node pattern)",
' elif head -1 /usr/local/bin/claude | grep -q "#!/usr/bin/env.*node\\|#!/usr/bin/node"; then',
' echo "Detected Node.js script..."',
' if command -v node >/dev/null 2>&1; then',
' echo "Running with node..."',
" # Try different approaches for Claude CLI",
"",
" # First try with --help to see available options",
' echo "Checking claude options..."',
' node /usr/local/bin/claude --help 2>/dev/null || echo "Help not available"',
"",
" # Try non-interactive approaches",
' echo "Attempting non-interactive execution..."',
"",
" # Method 1: Use the official --print flag for non-interactive mode",
' echo "Using --print flag for non-interactive mode..."',
' cat /tmp/prompt.txt | node /usr/local/bin/claude --print --allowedTools "Edit,Bash"',
' CLAUDE_EXIT_CODE=$?',
' echo "Claude Code finished with exit code: $CLAUDE_EXIT_CODE"',
"",
' if [ $CLAUDE_EXIT_CODE -ne 0 ]; then',
' echo "ERROR: Claude Code failed with exit code $CLAUDE_EXIT_CODE"',
' exit $CLAUDE_EXIT_CODE',
" fi",
"",
' echo "✅ Claude Code completed successfully"',
" else",
' echo "Node.js not found, trying direct execution..."',
' /usr/local/bin/claude < /tmp/prompt.txt',
' CLAUDE_EXIT_CODE=$?',
' echo "Claude Code finished with exit code: $CLAUDE_EXIT_CODE"',
' if [ $CLAUDE_EXIT_CODE -ne 0 ]; then',
' echo "ERROR: Claude Code failed with exit code $CLAUDE_EXIT_CODE"',
' exit $CLAUDE_EXIT_CODE',
" fi",
' echo "✅ Claude Code completed successfully"',
" fi",
" # Check if it's a Python script",
' elif head -1 /usr/local/bin/claude | grep -q "#!/usr/bin/env python\\|#!/usr/bin/python"; then',
' echo "Detected Python script..."',
' if command -v python3 >/dev/null 2>&1; then',
' echo "Running with python3..."',
' python3 /usr/local/bin/claude < /tmp/prompt.txt',
' CLAUDE_EXIT_CODE=$?',
' elif command -v python >/dev/null 2>&1; then',
' echo "Running with python..."',
' python /usr/local/bin/claude < /tmp/prompt.txt',
' CLAUDE_EXIT_CODE=$?',
" else",
' echo "Python not found, trying direct execution..."',
' /usr/local/bin/claude < /tmp/prompt.txt',
' CLAUDE_EXIT_CODE=$?',
" fi",
' echo "Claude Code finished with exit code: $CLAUDE_EXIT_CODE"',
' if [ $CLAUDE_EXIT_CODE -ne 0 ]; then',
' echo "ERROR: Claude Code failed with exit code $CLAUDE_EXIT_CODE"',
' exit $CLAUDE_EXIT_CODE',
" fi",
' echo "✅ Claude Code completed successfully"',
" else",
' echo "Unknown script type, trying direct execution..."',
' /usr/local/bin/claude < /tmp/prompt.txt',
' CLAUDE_EXIT_CODE=$?',
' echo "Claude Code finished with exit code: $CLAUDE_EXIT_CODE"',
' if [ $CLAUDE_EXIT_CODE -ne 0 ]; then',
' echo "ERROR: Claude Code failed with exit code $CLAUDE_EXIT_CODE"',
' exit $CLAUDE_EXIT_CODE',
" fi",
' echo "✅ Claude Code completed successfully"',
" fi",
"elif command -v claude >/dev/null 2>&1; then",
' echo "Using claude from PATH..."',
' CLAUDE_PATH=$(which claude)',
' echo "Claude found at: $CLAUDE_PATH"',
' claude < /tmp/prompt.txt',
' CLAUDE_EXIT_CODE=$?',
' echo "Claude Code finished with exit code: $CLAUDE_EXIT_CODE"',
' if [ $CLAUDE_EXIT_CODE -ne 0 ]; then',
' echo "ERROR: Claude Code failed with exit code $CLAUDE_EXIT_CODE"',
' exit $CLAUDE_EXIT_CODE',
" fi",
' echo "✅ Claude Code completed successfully"',
"else",
' echo "ERROR: claude command not found anywhere"',
' echo "Checking available interpreters:"',
' which python3 2>/dev/null && echo "python3: available" || echo "python3: not found"',
' which python 2>/dev/null && echo "python: available" || echo "python: not found"',
' which node 2>/dev/null && echo "node: available" || echo "node: not found"',
' which sh 2>/dev/null && echo "sh: available" || echo "sh: not found"',
" exit 1",
"fi",
"",
]


def get_container_overrides() -> Dict[str, Any]: # pragma: no cover - simple data
"""Return Claude-specific container overrides (none required)."""

return {}


def build_command(context: TaskContext) -> str:
"""Build the Claude-specific container command."""

return build_common_command(
context,
pre_model_lines=build_pre_model_lines(context),
model_lines=build_model_lines(context),
)
140 changes: 140 additions & 0 deletions server/utils/code_task/codex.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
"""Codex-specific helpers for AI code tasks."""

import fcntl
import os
import random
import time
from typing import Any, Dict, Iterable

from .common import TaskContext, build_command as build_common_command

CONTAINER_IMAGE = "codex-automation:latest"


def get_environment(user_preferences: Dict[str, Any]) -> Dict[str, str]:
"""Return environment variables required for Codex execution."""

env = {
"OPENAI_API_KEY": os.getenv("OPENAI_API_KEY"),
"OPENAI_NONINTERACTIVE": "1",
"CODEX_QUIET_MODE": "1",
"CODEX_UNSAFE_ALLOW_NO_SANDBOX": "1",
"CODEX_DISABLE_SANDBOX": "1",
"CODEX_NO_SANDBOX": "1",
}

codex_config = user_preferences.get("codex", {})
if isinstance(codex_config, dict):
custom_env = codex_config.get("env", {})
if isinstance(custom_env, dict):
env.update(custom_env)

return {key: value for key, value in env.items() if value is not None}


def prepare_for_run(task_id: int, logger) -> None:
"""Mitigate Codex concurrency issues with delays and locking."""

stagger_delay = random.uniform(0.5, 2.0)
logger.info("🕐 Adding %.1fs staggered start delay for Codex task %s", stagger_delay, task_id)
time.sleep(stagger_delay)

lock_file_path = "/tmp/codex_execution_lock"
try:
logger.info("🔒 Acquiring Codex execution lock for task %s", task_id)
with open(lock_file_path, "w", encoding="utf-8") as lock_file:
fcntl.flock(lock_file.fileno(), fcntl.LOCK_EX | fcntl.LOCK_NB)
logger.info("✅ Codex execution lock acquired for task %s", task_id)
except (OSError, BlockingIOError) as exc: # pylint: disable=broad-except
logger.warning("⚠️ Could not acquire Codex execution lock for task %s: %s", task_id, exc)
additional_delay = random.uniform(1.0, 3.0)
logger.info("🕐 Adding additional %.1fs delay due to lock conflict", additional_delay)
time.sleep(additional_delay)


def build_model_lines(_: TaskContext) -> Iterable[str]:
"""Codex CLI invocation logic."""

return [
'echo "Using Codex (OpenAI Codex) CLI..."',
"",
"# Set environment variables for non-interactive mode",
"export CODEX_QUIET_MODE=1",
"export CODEX_UNSAFE_ALLOW_NO_SANDBOX=1",
"export CODEX_DISABLE_SANDBOX=1",
"export CODEX_NO_SANDBOX=1",
"",
"# Debug: Verify environment variables are set",
'echo "=== CODEX DEBUG INFO ==="',
'echo "CODEX_QUIET_MODE: $CODEX_QUIET_MODE"',
'echo "CODEX_UNSAFE_ALLOW_NO_SANDBOX: $CODEX_UNSAFE_ALLOW_NO_SANDBOX"',
'echo "OPENAI_API_KEY: $(echo $OPENAI_API_KEY | head -c 8)..."',
'echo "USING OFFICIAL CODEX FLAGS: --approval-mode full-auto --quiet for non-interactive operation"',
'echo "======================="',
"",
"# Read the prompt from file",
'PROMPT_TEXT=$(cat /tmp/prompt.txt)',
"",
"# Check for codex installation",
'if [ -f /usr/local/bin/codex ]; then',
' echo "Found codex at /usr/local/bin/codex"',
' echo "Running Codex in non-interactive mode..."',
"",
" # Use official non-interactive flags for Docker environment",
" # Using --approval-mode full-auto as per official Codex documentation",
" # Also disable Codex's internal sandboxing to prevent conflicts with Docker",
' /usr/local/bin/codex --approval-mode full-auto --quiet "$PROMPT_TEXT"',
' CODEX_EXIT_CODE=$?',
' echo "Codex finished with exit code: $CODEX_EXIT_CODE"',
"",
' if [ $CODEX_EXIT_CODE -ne 0 ]; then',
' echo "ERROR: Codex failed with exit code $CODEX_EXIT_CODE"',
' exit $CODEX_EXIT_CODE',
" fi",
"",
' echo "✅ Codex completed successfully"',
"elif command -v codex >/dev/null 2>&1; then",
' echo "Using codex from PATH..."',
' echo "Running Codex in non-interactive mode..."',
"",
" # Use official non-interactive flags for Docker environment",
" # Using --approval-mode full-auto as per official Codex documentation",
" # Also disable Codex's internal sandboxing to prevent conflicts with Docker",
' codex --approval-mode full-auto --quiet "$PROMPT_TEXT"',
' CODEX_EXIT_CODE=$?',
' echo "Codex finished with exit code: $CODEX_EXIT_CODE"',
"",
' if [ $CODEX_EXIT_CODE -ne 0 ]; then',
' echo "ERROR: Codex failed with exit code $CODEX_EXIT_CODE"',
' exit $CODEX_EXIT_CODE',
" fi",
"",
' echo "✅ Codex completed successfully"',
"else",
' echo "ERROR: codex command not found anywhere"',
' echo "Please ensure Codex CLI is installed in the container"',
" exit 1",
"fi",
"",
]


def get_container_overrides() -> Dict[str, Any]: # pragma: no cover - simple data
"""Return Docker configuration overrides required for Codex."""

return {
"security_opt": [
"seccomp=unconfined",
"apparmor=unconfined",
"no-new-privileges=false",
],
"cap_add": ["ALL"],
"privileged": True,
"pid_mode": "host",
}


def build_command(context: TaskContext) -> str:
"""Build the Codex-specific container command."""

return build_common_command(context, model_lines=build_model_lines(context))
Loading