mvp-factory-openhands/HEADLESS_MODE_APPROACH.md

530 lines
12 KiB
Markdown

# 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.