12 KiB
12 KiB
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:
- n8n receives Gitea webhook
- SSH node executes Docker command
- OpenHands runs headless in container
- Task completes automatically
- Verification node checks results
- Response sent to Gitea (optional)
Implementation Steps
Step 1: Test Headless Mode (30 min)
Test 1: Direct Docker Execution
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:
{
"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
#!/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
# 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
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
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
# 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
{
"name": "Webhook Trigger",
"parameters": {
"path": "openhands-headless",
"httpMethod": "POST",
"responseMode": "responseNode"
}
}
SSH Execute Node
{
"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
{
"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
{
"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
{
"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:
// 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
[
{
"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
# 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
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
# 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
- Docker Hub: openhands/openhands
- Current API: http://localhost:3000 (for reference)
Conclusion
Headless mode is the optimal solution for OpenHands integration with n8n because it:
- Eliminates all technical blockers from CLI and API approaches
- Provides true automation without interactive prompts
- Integrates seamlessly with n8n SSH nodes
- Supports production use with proper error handling
- Enables CI/CD integration for automated workflows
Recommendation: Proceed immediately with headless mode implementation.