# OpenHands Headless Mode Approach **Date:** 2025-11-30 **Approach:** Headless Mode (Recommended) **Priority:** HIGH - Replaces CLI and API approaches --- ## Why Headless Mode? ✅ **Perfect for Automation:** - No interactive prompts - No TTY requirements - Direct command-line execution - File-based task loading - CI/CD pipeline ready ✅ **No Technical Blockers:** - Bypasses CLI interactive confirmation - Avoids API runtime connectivity issues - Clean Docker-based execution - Proper error handling ✅ **Production Ready:** - Built for batch processing - Environment variable configuration - Repository integration support - Budget and iteration controls --- ## Architecture ``` Gitea Push → n8n Webhook → SSH Command → OpenHands Headless (Docker) → Result ↓ Verification & Response ``` ### Flow: 1. **n8n receives Gitea webhook** 2. **SSH node executes Docker command** 3. **OpenHands runs headless in container** 4. **Task completes automatically** 5. **Verification node checks results** 6. **Response sent to Gitea (optional)** --- ## Implementation Steps ### Step 1: Test Headless Mode (30 min) **Test 1: Direct Docker Execution** ```bash docker run --rm \ -e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.openhands.dev/openhands/runtime:0.62-nikolaik \ -e LLM_MODEL="openai/MiniMax-M2" \ -e LLM_API_KEY="${MINIMAX_API_KEY}" \ -e LOG_ALL_EVENTS=true \ -e SANDBOX_USER_ID=1000 \ -e SANDBOX_VOLUMES="/home/bam:/workspace:rw" \ -v /var/run/docker.sock:/var/run/docker.sock \ -v /home/bam/.openhands:/.openhands \ --name openhands-test \ docker.openhands.dev/openhands/openhands:0.62 \ python -m openhands.core.main -t "Create a file named headless-test.txt with content: Testing headless mode" ``` **Expected:** File created successfully without prompts ### Step 2: Create n8n Workflow (45 min) **Workflow Node Structure:** ```json { "nodes": [ { "name": "Webhook Trigger", "type": "n8n-nodes-base.webhook", "parameters": { "path": "openhands-headless", "httpMethod": "POST" } }, { "name": "Execute Headless Mode", "type": "n8n-nodes-base.ssh", "parameters": { "command": "cd /home/bam && /home/bam/run-headless.sh \"{{ $json.repository.full_name }}: {{ $json.commits[0].message }}\"" } }, { "name": "Verify Results", "type": "n8n-nodes-base.ssh", "parameters": { "command": "ls -la /home/bam/*.txt 2>/dev/null | tail -10" } } ] } ``` ### Step 3: Create Wrapper Script (15 min) **File:** `/home/bam/run-headless.sh` ```bash #!/bin/bash # OpenHands Headless Mode Wrapper TASK="$1" CONTAINER_NAME="openhands-$(date +%s)" docker run --rm \ --name "$CONTAINER_NAME" \ -e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.openhands.dev/openhands/runtime:0.62-nikolaik \ -e LLM_MODEL="openai/MiniMax-M2" \ -e LLM_API_KEY="${MINIMAX_API_KEY}" \ -e LOG_ALL_EVENTS=true \ -e SANDBOX_USER_ID=1000 \ -e SANDBOX_VOLUMES="/home/bam:/workspace:rw" \ -v /var/run/docker.sock:/var/run/docker.sock \ -v /home/bam/.openhands:/.openhands \ --add-host host.docker.internal:host-gateway \ docker.openhands.dev/openhands/openhands:0.62 \ python -m openhands.core.main -t "$TASK" echo "Task completed: $TASK" ``` --- ## Command Reference ### Basic Task Execution ```bash # Inline task python -m openhands.core.main -t "Create a hello world script" # Task from file echo "Review this codebase" > task.txt python -m openhands.core.main -f task.txt # With repository python -m openhands.core.main -t "Fix linting issues" --selected-repo "owner/repo" ``` ### Docker Execution ```bash docker run --rm \ -e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.openhands.dev/openhands/runtime:0.62-nikolaik \ -e LLM_API_KEY="${MINIMAX_API_KEY}" \ -e LLM_MODEL="openai/MiniMax-M2" \ -e SANDBOX_USER_ID=$(id -u) \ -e SANDBOX_VOLUMES="/path/to/workspace:/workspace:rw" \ -e LOG_ALL_EVENTS=true \ -v /var/run/docker.sock:/var/run/docker.sock \ -v ~/.openhands:/.openhands \ --name openhands-exec \ docker.openhands.dev/openhands/openhands:0.62 \ python -m openhands.core.main -t "Your task here" ``` ### Environment Variables ```bash export LLM_MODEL="openai/MiniMax-M2" export LLM_API_KEY="your-minimax-key" export SANDBOX_USER_ID=1000 export SANDBOX_VOLUMES="/home/bam:/workspace:rw" export LOG_ALL_EVENTS=true export GITHUB_TOKEN="your-github-token" # For repo operations ``` ### Advanced Options ```bash # Set working directory python -m openhands.core.main -t "Task" -d "/workspace" # Limit iterations python -m openhands.core.main -t "Task" -i 50 # Set budget limit (USD) python -m openhands.core.main -t "Task" -b 10.0 # Load from file python -m openhands.core.main -f task.txt # Repository operation python -m openhands.core.main -t "Analyze and suggest improvements" --selected-repo "owner/repo-name" ``` --- ## n8n Workflow Configuration ### Webhook Trigger ```json { "name": "Webhook Trigger", "parameters": { "path": "openhands-headless", "httpMethod": "POST", "responseMode": "responseNode" } } ``` ### SSH Execute Node ```json { "name": "Execute OpenHands Headless", "type": "n8n-nodes-base.ssh", "parameters": { "command": "cd /home/bam && bash run-headless.sh \"Repository: {{ $json.repository.full_name }}, Commit: {{ $json.commits[0].message }}\"", "sessionId": "headless-session" }, "credentials": { "sshPassword": { "id": "ai-dev-localhost", "name": "ai-dev-localhost" } } } ``` ### Verification Node ```json { "name": "Verify Files Created", "type": "n8n-nodes-base.ssh", "parameters": { "command": "ls -la /home/bam/*.txt 2>/dev/null | tail -15 && echo \"=== Checking for recent files ===\" && find /home/bam -name \"*.txt\" -newermt '5 minutes ago' 2>/dev/null", "sessionId": "headless-session" } } ``` ### Response Node ```json { "name": "Send Response", "type": "n8n-nodes-base.respondToWebhook", "parameters": { "respondWith": "json", "responseBody": { "status": "success", "message": "OpenHands headless task completed", "timestamp": "{{ $now }}", "task": "{{ $json.task }}" } } } ``` --- ## Testing Checklist ### Phase 1: Direct Testing (Day 1) - [ ] Test Docker headless execution manually - [ ] Verify file creation without prompts - [ ] Test task with repository cloning - [ ] Test error handling - [ ] Measure execution time ### Phase 2: Wrapper Script (Day 1) - [ ] Create `/home/bam/run-headless.sh` - [ ] Test wrapper script execution - [ ] Test concurrent executions - [ ] Test cleanup after completion - [ ] Add logging ### Phase 3: n8n Integration (Day 2) - [ ] Import workflow to n8n - [ ] Configure SSH credentials - [ ] Test webhook manually - [ ] Check execution logs - [ ] Verify file creation ### Phase 4: Gitea Integration (Day 2) - [ ] Configure Gitea webhook - [ ] Test with repository push - [ ] Verify end-to-end flow - [ ] Test error scenarios - [ ] Document setup --- ## Advantages Over Previous Approaches ### vs CLI Approach | Aspect | CLI | Headless | |--------|-----|----------| | TTY Required | ❌ Yes | ✅ No | | Interactive Prompts | ❌ Yes | ✅ None | | Automation | ❌ Difficult | ✅ Easy | | n8n Compatibility | ❌ Poor | ✅ Excellent | | Reliability | ❌ Unstable | ✅ Stable | ### vs API Approach | Aspect | API | Headless | |--------|-----|----------| | Runtime Startup | ❌ Fails | ✅ Works | | Network Issues | ❌ Complex | ✅ None | | Status Monitoring | ✅ Good | ⚠️ Via logs | | Error Handling | ✅ Good | ⚠️ Via exit code | | Setup Complexity | ❌ High | ✅ Simple | --- ## Gitea Webhook Configuration ### Webhook Settings ``` URL: https://n8n.oky.sh/webhook/openhands-headless Method: POST Content-Type: application/json Secret: [generate secure random string] Events: Push events Active: ✓ ``` ### Payload Example ```json { "repository": { "full_name": "owner/repo-name", "name": "repo-name", "clone_url": "https://git.oky.sh/owner/repo-name.git" }, "commits": [ { "message": "Add new feature", "id": "abc123", "url": "https://git.oky.sh/owner/repo-name/commit/abc123" } ], "ref": "refs/heads/main" } ``` ### Task Generation **In n8n workflow:** ```javascript // Extract repository and commit info const repo = $json.repository.full_name; const commitMsg = $json.commits[0].message; const commitSha = $json.commits[0].id; // Generate task for OpenHands const task = `Build and test repository ${repo} after commit: ${commitMsg} (${commitSha.substring(0,7)})`; // Pass to execution node return { task }; ``` --- ## Error Handling ### n8n Workflow Error Paths ```json [ { "condition": "Execution failed", "action": "Send error notification", "node": "Error Handler" }, { "condition": "File not created", "action": "Retry task", "node": "Retry Logic" }, { "condition": "Timeout", "action": "Kill container and retry", "node": "Timeout Handler" } ] ``` ### Container Cleanup ```bash # Ensure cleanup after execution trap "docker rm -f openhands-exec 2>/dev/null || true" EXIT # Or use --rm flag (recommended) docker run --rm --name openhands-exec ... ``` ### Retry Logic ```bash MAX_RETRIES=3 RETRY_COUNT=0 while [ $RETRY_COUNT -lt $MAX_RETRIES ]; do if docker run --rm ...; then echo "Success!" break else RETRY_COUNT=$((RETRY_COUNT + 1)) echo "Retry $RETRY_COUNT/$MAX_RETRIES" sleep 5 fi done ``` --- ## Performance Considerations ### Container Startup Time - Initial pull: ~30 seconds - Subsequent runs: ~5 seconds - Task execution: Varies (30s - 5min) ### Resource Usage - CPU: 1-2 cores during execution - Memory: 1-2GB - Disk: Minimal (ephemeral) ### Optimization ```bash # Pre-pull image to reduce startup time docker pull docker.openhands.dev/openhands/openhands:0.62 # Use specific runtime image export SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.openhands.dev/openhands/runtime:0.62-nikolaik # Reuse container with persistent volume # (Trade-off: faster startup vs resource usage) ``` --- ## Security Notes ### Container Isolation - Runs in Docker container - Limited filesystem access - Network sandboxed - User permissions respected (SANDBOX_USER_ID) ### Credential Management - API keys via environment variables - GitHub tokens for repository access - Rotate tokens regularly - Use n8n credential store ### File Access - Limited to SANDBOX_VOLUMES - Default: `/home/bam:/workspace:rw` - Can restrict to specific directories --- ## Files to Create ### 1. Wrapper Script **File:** `/home/bam/run-headless.sh` - Docker execution wrapper - Environment setup - Error handling - Logging ### 2. n8n Workflow **File:** `/home/bam/claude/mvp-factory/openhands-headless-workflow.json` - Webhook trigger - SSH execution node - Verification node - Response node ### 3. Test Script **File:** `/home/bam/test-headless.sh` - Direct Docker test - Wrapper script test - Verification script - Performance measurement --- ## Success Criteria - [ ] Headless mode executes without prompts - [ ] Files are created successfully - [ ] n8n workflow imports and runs - [ ] Webhook triggers workflow - [ ] Gitea push initiates task - [ ] Results are verified - [ ] Error handling works - [ ] Documentation complete --- ## Timeline | Day | Task | Duration | |-----|------|----------| | 1 | Test headless mode | 30 min | | 1 | Create wrapper script | 15 min | | 1 | Create n8n workflow | 45 min | | 1 | Test end-to-end | 60 min | | 2 | Configure Gitea webhook | 30 min | | 2 | Production testing | 120 min | | 2 | Documentation | 30 min | **Total: 5-6 hours** --- ## References - [OpenHands Headless Mode Docs](https://docs.openhands.dev/openhands/usage/run-openhands/headless-mode) - Docker Hub: [openhands/openhands](https://hub.docker.com/r/docker.openhands.dev/openhands/openhands/tags) - Current API: http://localhost:3000 (for reference) --- ## Conclusion Headless mode is the **optimal solution** for OpenHands integration with n8n because it: 1. **Eliminates all technical blockers** from CLI and API approaches 2. **Provides true automation** without interactive prompts 3. **Integrates seamlessly** with n8n SSH nodes 4. **Supports production use** with proper error handling 5. **Enables CI/CD integration** for automated workflows **Recommendation:** Proceed immediately with headless mode implementation.