diff --git a/.gitignore b/.gitignore
index 47e039a..d92a6d2 100644
--- a/.gitignore
+++ b/.gitignore
@@ -177,4 +177,5 @@ storybook-static/
coverage/
.DS_Store
*.pem
-tasks_backup.json
\ No newline at end of file
+tasks_backup.json
+.credentials.json
\ No newline at end of file
diff --git a/async-code-web/app/settings/page.tsx b/async-code-web/app/settings/page.tsx
index e0e51a9..ac4a2b2 100644
--- a/async-code-web/app/settings/page.tsx
+++ b/async-code-web/app/settings/page.tsx
@@ -1,12 +1,13 @@
"use client";
import { useState, useEffect } from "react";
-import { Github, CheckCircle, ArrowLeft, Settings, Key, Shield, Info } from "lucide-react";
+import { Github, CheckCircle, ArrowLeft, Settings, Key, Shield, Info, Code } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Label } from "@/components/ui/label";
import { toast } from "sonner";
+import { CodeAgentSettings } from "@/components/code-agent-settings";
import Link from "next/link";
@@ -106,7 +107,7 @@ export default function SettingsPage() {
Settings
-
Configure your GitHub authentication
+
Configure GitHub authentication and code agent environments
@@ -236,6 +237,9 @@ export default function SettingsPage() {
+ {/* Code Agent Settings */}
+
+
{/* Token Creation Instructions */}
diff --git a/async-code-web/components/code-agent-settings.tsx b/async-code-web/components/code-agent-settings.tsx
new file mode 100644
index 0000000..90cb31b
--- /dev/null
+++ b/async-code-web/components/code-agent-settings.tsx
@@ -0,0 +1,180 @@
+"use client";
+
+import React, { useState, useEffect } from "react";
+import CodeMirror from "@uiw/react-codemirror";
+import { javascript } from "@codemirror/lang-javascript";
+import { githubLight } from "@uiw/codemirror-theme-github";
+import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
+import { Button } from "@/components/ui/button";
+import { Label } from "@/components/ui/label";
+import { Alert, AlertDescription } from "@/components/ui/alert";
+import { AlertCircle, Save } from "lucide-react";
+import { toast } from "sonner";
+import { SupabaseService } from "@/lib/supabase-service";
+import { useUserProfile } from "@/hooks/useUserProfile";
+
+interface CodeAgentConfig {
+ claudeCode?: Record;
+ codexCLI?: Record;
+}
+
+const DEFAULT_CLAUDE_ENV = {
+ ANTHROPIC_API_KEY: "",
+ // Add other Claude-specific env vars here if needed
+};
+
+const DEFAULT_CODEX_ENV = {
+ OPENAI_API_KEY: "",
+ DISABLE_SANDBOX: "yes",
+ CONTINUE_ON_BROWSER: "no",
+ // Add other Codex-specific env vars here if needed
+};
+
+export function CodeAgentSettings() {
+ const { profile, refreshProfile } = useUserProfile();
+ const [claudeEnv, setClaudeEnv] = useState("");
+ const [codexEnv, setCodexEnv] = useState("");
+ const [isLoading, setIsLoading] = useState(false);
+ const [errors, setErrors] = useState<{ claude?: string; codex?: string }>({});
+
+ // Load settings from profile on mount
+ useEffect(() => {
+ if (profile?.preferences) {
+ const prefs = profile.preferences as CodeAgentConfig;
+ setClaudeEnv(JSON.stringify(prefs.claudeCode || DEFAULT_CLAUDE_ENV, null, 2));
+ setCodexEnv(JSON.stringify(prefs.codexCLI || DEFAULT_CODEX_ENV, null, 2));
+ } else {
+ setClaudeEnv(JSON.stringify(DEFAULT_CLAUDE_ENV, null, 2));
+ setCodexEnv(JSON.stringify(DEFAULT_CODEX_ENV, null, 2));
+ }
+ }, [profile]);
+
+ const validateJSON = (value: string, agent: "claude" | "codex") => {
+ try {
+ JSON.parse(value);
+ setErrors(prev => ({ ...prev, [agent]: undefined }));
+ return true;
+ } catch (e) {
+ setErrors(prev => ({ ...prev, [agent]: "Invalid JSON format" }));
+ return false;
+ }
+ };
+
+ const handleSave = async () => {
+ // Validate both JSONs
+ const isClaudeValid = validateJSON(claudeEnv, "claude");
+ const isCodexValid = validateJSON(codexEnv, "codex");
+
+ if (!isClaudeValid || !isCodexValid) {
+ toast.error("Please fix JSON errors before saving");
+ return;
+ }
+
+ setIsLoading(true);
+ try {
+ const claudeConfig = JSON.parse(claudeEnv);
+ const codexConfig = JSON.parse(codexEnv);
+
+ const preferences: CodeAgentConfig = {
+ claudeCode: claudeConfig,
+ codexCLI: codexConfig,
+ };
+
+ // Merge with existing preferences if any
+ const existingPrefs = (profile?.preferences || {}) as Record;
+ const mergedPrefs = {
+ ...existingPrefs,
+ ...preferences,
+ };
+
+ await SupabaseService.updateUserProfile({ preferences: mergedPrefs });
+ await refreshProfile();
+ toast.success("Code agent settings saved successfully");
+ } catch (error) {
+ console.error("Failed to save settings:", error);
+ toast.error("Failed to save settings");
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ return (
+
+
+
+ Code Agent Settings
+
+ Configure environment variables for each code agent. These settings will be used when creating containers.
+
+
+
+
+
+
+ Important: Store sensitive API keys here instead of hardcoding them. Personal settings will override default environment variables.
+
+
+
+ {/* Claude Code Settings */}
+
+
Claude Code Environment Variables
+
+ {
+ setClaudeEnv(value);
+ validateJSON(value, "claude");
+ }}
+ placeholder={JSON.stringify(DEFAULT_CLAUDE_ENV, null, 2)}
+ />
+
+ {errors.claude && (
+
{errors.claude}
+ )}
+
+ Configure environment variables for Claude Code CLI (@anthropic-ai/claude-code)
+
+
+
+ {/* Codex CLI Settings */}
+
+
Codex CLI Environment Variables
+
+ {
+ setCodexEnv(value);
+ validateJSON(value, "codex");
+ }}
+ placeholder={JSON.stringify(DEFAULT_CODEX_ENV, null, 2)}
+ />
+
+ {errors.codex && (
+
{errors.codex}
+ )}
+
+ Configure environment variables for Codex CLI (@openai/codex)
+
+
+
+
+
+ {isLoading ? "Saving..." : "Save Settings"}
+
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/async-code-web/components/ui/alert.tsx b/async-code-web/components/ui/alert.tsx
new file mode 100644
index 0000000..5a7ba0f
--- /dev/null
+++ b/async-code-web/components/ui/alert.tsx
@@ -0,0 +1,59 @@
+import * as React from "react"
+import { cva, type VariantProps } from "class-variance-authority"
+
+import { cn } from "@/lib/utils"
+
+const alertVariants = cva(
+ "relative w-full rounded-lg border p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground",
+ {
+ variants: {
+ variant: {
+ default: "bg-background text-foreground",
+ destructive:
+ "border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive",
+ },
+ },
+ defaultVariants: {
+ variant: "default",
+ },
+ }
+)
+
+const Alert = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes & VariantProps
+>(({ className, variant, ...props }, ref) => (
+
+))
+Alert.displayName = "Alert"
+
+const AlertTitle = React.forwardRef<
+ HTMLParagraphElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+))
+AlertTitle.displayName = "AlertTitle"
+
+const AlertDescription = React.forwardRef<
+ HTMLParagraphElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+))
+AlertDescription.displayName = "AlertDescription"
+
+export { Alert, AlertTitle, AlertDescription }
\ No newline at end of file
diff --git a/async-code-web/hooks/useUserProfile.tsx b/async-code-web/hooks/useUserProfile.tsx
new file mode 100644
index 0000000..8092fcf
--- /dev/null
+++ b/async-code-web/hooks/useUserProfile.tsx
@@ -0,0 +1,40 @@
+"use client";
+
+import { useState, useEffect, useCallback } from "react";
+import { SupabaseService } from "@/lib/supabase-service";
+import { User } from "@/types";
+
+export function useUserProfile() {
+ const [profile, setProfile] = useState(null);
+ const [isLoading, setIsLoading] = useState(true);
+ const [error, setError] = useState(null);
+
+ const fetchProfile = useCallback(async () => {
+ try {
+ setIsLoading(true);
+ const data = await SupabaseService.getUserProfile();
+ setProfile(data);
+ setError(null);
+ } catch (err) {
+ setError(err as Error);
+ console.error("Failed to fetch user profile:", err);
+ } finally {
+ setIsLoading(false);
+ }
+ }, []);
+
+ useEffect(() => {
+ fetchProfile();
+ }, [fetchProfile]);
+
+ const refreshProfile = useCallback(async () => {
+ await fetchProfile();
+ }, [fetchProfile]);
+
+ return {
+ profile,
+ isLoading,
+ error,
+ refreshProfile,
+ };
+}
\ No newline at end of file
diff --git a/server/database.py b/server/database.py
index e44b8a3..cad3937 100644
--- a/server/database.py
+++ b/server/database.py
@@ -216,4 +216,14 @@ def migrate_legacy_task(legacy_task: Dict, user_id: str) -> Optional[Dict]:
return result.data[0] if result.data else None
except Exception as e:
logger.error(f"Error migrating legacy task: {e}")
- raise
\ No newline at end of file
+ raise
+
+ @staticmethod
+ def get_user_by_id(user_id: str) -> Optional[Dict]:
+ """Get user by ID"""
+ try:
+ result = supabase.table('users').select('*').eq('id', user_id).single().execute()
+ return result.data
+ except Exception as e:
+ logger.error(f"Error getting user: {e}")
+ return None
\ No newline at end of file
diff --git a/server/utils/code_task_v2.py b/server/utils/code_task_v2.py
index 3461f8b..449b8b3 100644
--- a/server/utils/code_task_v2.py
+++ b/server/utils/code_task_v2.py
@@ -142,20 +142,38 @@ def _run_ai_code_task_v2_internal(task_id: int, user_id: str, github_token: str)
# Add model-specific API keys and environment variables
model_cli = task.get('agent', 'claude')
+
+ # Get user preferences for custom environment variables
+ user = DatabaseOperations.get_user_by_id(user_id)
+ user_preferences = user.get('preferences', {}) if user else {}
+
+ if user_preferences:
+ logger.info(f"đ§ Found user preferences for {model_cli}: {list(user_preferences.keys())}")
+
if model_cli == 'claude':
- env_vars.update({
+ # Start with default Claude environment
+ claude_env = {
'ANTHROPIC_API_KEY': os.getenv('ANTHROPIC_API_KEY'),
'ANTHROPIC_NONINTERACTIVE': '1' # Custom flag for Anthropic tools
- })
+ }
+ # Merge with user's custom Claude environment variables
+ if user_preferences.get('claudeCode'):
+ claude_env.update(user_preferences['claudeCode'])
+ env_vars.update(claude_env)
elif model_cli == 'codex':
- env_vars.update({
+ # Start with default Codex environment
+ codex_env = {
'OPENAI_API_KEY': os.getenv('OPENAI_API_KEY'),
'OPENAI_NONINTERACTIVE': '1', # Custom flag for OpenAI tools
'CODEX_QUIET_MODE': '1', # Official Codex non-interactive flag
'CODEX_UNSAFE_ALLOW_NO_SANDBOX': '1', # Disable Codex internal sandboxing to prevent Docker conflicts
'CODEX_DISABLE_SANDBOX': '1', # Alternative sandbox disable flag
'CODEX_NO_SANDBOX': '1' # Another potential sandbox disable flag
- })
+ }
+ # Merge with user's custom Codex environment variables
+ if user_preferences.get('codexCLI'):
+ codex_env.update(user_preferences['codexCLI'])
+ env_vars.update(codex_env)
# Use specialized container images based on model
if model_cli == 'codex':
@@ -185,6 +203,33 @@ def _run_ai_code_task_v2_internal(task_id: int, user_id: str, github_token: str)
logger.info(f"đ Adding additional {additional_delay:.1f}s delay due to lock conflict")
time.sleep(additional_delay)
+ # Load Claude credentials from user preferences in Supabase
+ credentials_content = ""
+ escaped_credentials = ""
+ if model_cli == 'claude':
+ logger.info(f"đ Looking for Claude credentials in user preferences for task {task_id}")
+
+ # Check if user has Claude credentials in their preferences
+ claude_preferences = user_preferences.get('claudeCode', {})
+ if claude_preferences and 'credentials' in claude_preferences:
+ try:
+ credentials_json = claude_preferences['credentials']
+ if credentials_json:
+ # Convert JSON object to string for writing to container
+ credentials_content = json.dumps(credentials_json)
+ logger.info(f"đ Successfully loaded Claude credentials from user preferences and stringified ({len(credentials_content)} characters) for task {task_id}")
+ # Escape credentials content for shell
+ escaped_credentials = credentials_content.replace("'", "'\"'\"'").replace('\n', '\\n')
+ logger.info(f"đ Credentials content escaped for shell injection")
+ else:
+ logger.warning(f"â ī¸ Claude credentials field exists but is empty in user preferences for task {task_id}")
+ except Exception as e:
+ logger.error(f"â Failed to process Claude credentials from user preferences: {e}")
+ credentials_content = ""
+ escaped_credentials = ""
+ else:
+ logger.info(f"âšī¸ No Claude credentials found in user preferences for task {task_id} - skipping credentials setup")
+
# Create the command to run in container (v2 function)
container_command = f'''
set -e
@@ -203,8 +248,29 @@ def _run_ai_code_task_v2_internal(task_id: int, user_id: str, github_token: str)
echo "Starting {model_cli.upper()} Code with prompt..."
-# Create a temporary file with the prompt
-echo "{escaped_prompt}" > /tmp/prompt.txt
+# Create a temporary file with the prompt using heredoc for proper handling
+cat << 'PROMPT_EOF' > /tmp/prompt.txt
+{prompt}
+PROMPT_EOF
+
+# Setup Claude credentials for Claude tasks
+if [ "{model_cli}" = "claude" ]; then
+ echo "Setting up Claude credentials..."
+
+ # Create ~/.claude directory if it doesn't exist
+ mkdir -p ~/.claude
+
+ # Write credentials content directly to file
+ if [ ! -z '{escaped_credentials}' ]; then
+ echo "đ Writing credentials to ~/.claude/.credentials.json"
+ cat << 'CREDENTIALS_EOF' > ~/.claude/.credentials.json
+{credentials_content}
+CREDENTIALS_EOF
+ echo "â
Claude credentials configured"
+ else
+ echo "â ī¸ No credentials content available"
+ fi
+fi
# Check which CLI tool to use based on model selection
if [ "{model_cli}" = "codex" ]; then