mvp-factory-openhands/HEADLESS_MODE_APPROACH.md

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:

  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

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


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.