HTTP Server Mode
Run ERA Agent as an HTTP API server for programmatic access to all features. Perfect for integrations, custom frontends, and self-hosted deployments.
Quick Start
Section titled “Quick Start”Start the Server
Section titled “Start the Server”# Default port 8787./agent serve
# Custom portPORT=9000 ./agent serve
# With debug loggingAGENT_LOG_LEVEL=debug ./agent serveThe server will start and listen on http://localhost:8787
Test the Server
Section titled “Test the Server”curl http://localhost:8787/healthShould return:
{"status": "ok"}API Endpoints
Section titled “API Endpoints”The HTTP server exposes the same API as the Cloudflare Workers deployment. See the API Reference for complete documentation.
Quick Examples
Section titled “Quick Examples”Execute Code:
curl -X POST http://localhost:8787/api/execute \ -H "Content-Type: application/json" \ -d '{ "code": "print(\"Hello from ERA Agent!\")", "language": "python" }'Create Session:
curl -X POST http://localhost:8787/api/sessions \ -H "Content-Type: application/json" \ -d '{ "language": "python", "cpu_count": 2, "memory_mib": 512 }'Run in Session:
curl -X POST http://localhost:8787/api/sessions/{session_id}/run \ -H "Content-Type: application/json" \ -d '{ "code": "print(2 + 2)" }'Configuration
Section titled “Configuration”Environment Variables
Section titled “Environment Variables”# Server Configurationexport PORT=8787 # Server portexport AGENT_MODE=http # Auto-start as HTTP server
# Loggingexport AGENT_LOG_LEVEL=info # Log level: debug, info, warn, error
# Storageexport AGENT_STATE_DIR=/custom/path # State directory
# Then start./agentSystemd Service (Linux)
Section titled “Systemd Service (Linux)”Create /etc/systemd/system/era-agent.service:
[Unit]Description=ERA Agent HTTP ServerAfter=network.target
[Service]Type=simpleUser=era-agentWorkingDirectory=/opt/era-agentEnvironment="PORT=8787"Environment="AGENT_LOG_LEVEL=info"Environment="AGENT_STATE_DIR=/var/lib/era-agent"ExecStart=/opt/era-agent/agent serveRestart=on-failureRestartSec=5s
[Install]WantedBy=multi-user.targetEnable and start:
sudo systemctl daemon-reloadsudo systemctl enable era-agentsudo systemctl start era-agentsudo systemctl status era-agentLaunchd Service (macOS)
Section titled “Launchd Service (macOS)”Create ~/Library/LaunchAgents/com.era-agent.server.plist:
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"><plist version="1.0"><dict> <key>Label</key> <string>com.era-agent.server</string> <key>ProgramArguments</key> <array> <string>/usr/local/bin/agent</string> <string>serve</string> </array> <key>EnvironmentVariables</key> <dict> <key>PORT</key> <string>8787</string> <key>AGENT_LOG_LEVEL</key> <string>info</string> </dict> <key>RunAtLoad</key> <true/> <key>KeepAlive</key> <true/> <key>StandardOutPath</key> <string>/usr/local/var/log/era-agent.log</string> <key>StandardErrorPath</key> <string>/usr/local/var/log/era-agent-error.log</string></dict></plist>Load the service:
launchctl load ~/Library/LaunchAgents/com.era-agent.server.plistlaunchctl start com.era-agent.serverClient Libraries
Section titled “Client Libraries”Python
Section titled “Python”import requests
class ERAAgentClient: def __init__(self, base_url="http://localhost:8787"): self.base_url = base_url
def execute(self, code, language="python", **kwargs): """Execute code ephemerally.""" response = requests.post( f"{self.base_url}/api/execute", json={"code": code, "language": language, **kwargs} ) return response.json()
def create_session(self, language, **kwargs): """Create a persistent session.""" response = requests.post( f"{self.base_url}/api/sessions", json={"language": language, **kwargs} ) return response.json()
def run_in_session(self, session_id, code, **kwargs): """Run code in existing session.""" response = requests.post( f"{self.base_url}/api/sessions/{session_id}/run", json={"code": code, **kwargs} ) return response.json()
# Usageclient = ERAAgentClient()
# Quick executionresult = client.execute("print('Hello!')", "python")print(result)
# Session-based executionsession = client.create_session("python", cpu_count=2)session_id = session["id"]
result1 = client.run_in_session(session_id, "x = 42")result2 = client.run_in_session(session_id, "print(x * 2)")Node.js
Section titled “Node.js”class ERAAgentClient { constructor(baseURL = 'http://localhost:8787') { this.baseURL = baseURL; }
async execute(code, language = 'python', options = {}) { const response = await fetch(`${this.baseURL}/api/execute`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ code, language, ...options }) }); return response.json(); }
async createSession(language, options = {}) { const response = await fetch(`${this.baseURL}/api/sessions`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ language, ...options }) }); return response.json(); }
async runInSession(sessionId, code, options = {}) { const response = await fetch( `${this.baseURL}/api/sessions/${sessionId}/run`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ code, ...options }) } ); return response.json(); }}
// Usageconst client = new ERAAgentClient();
// Quick executionconst result = await client.execute("console.log('Hello!')", "node");console.log(result);
// Session-based executionconst session = await client.create Session("node");await client.runInSession(session.id, "let x = 42");await client.runInSession(session.id, "console.log(x * 2)");package main
import ( "bytes" "encoding/json" "fmt" "net/http")
type ERAAgentClient struct { BaseURL string}
func NewClient(baseURL string) *ERAAgentClient { return &ERAAgentClient{BaseURL: baseURL}}
func (c *ERAAgentClient) Execute(code, language string) (map[string]interface{}, error) { payload := map[string]interface{}{ "code": code, "language": language, }
data, _ := json.Marshal(payload) resp, err := http.Post( c.BaseURL+"/api/execute", "application/json", bytes.NewBuffer(data), ) if err != nil { return nil, err } defer resp.Body.Close()
var result map[string]interface{} json.NewDecoder(resp.Body).Decode(&result) return result, nil}
// Usagefunc main() { client := NewClient("http://localhost:8787")
result, _ := client.Execute("print('Hello!')", "python") fmt.Println(result)}Reverse Proxy Setup
Section titled “Reverse Proxy Setup”server { listen 80; server_name era-agent.example.com;
location / { proxy_pass http://localhost:8787; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection 'upgrade'; proxy_set_header Host $host; proxy_cache_bypass $http_upgrade;
# Timeouts for long-running code proxy_read_timeout 300s; proxy_connect_timeout 75s; }}era-agent.example.com { reverse_proxy localhost:8787}Apache
Section titled “Apache”<VirtualHost *:80> ServerName era-agent.example.com
ProxyPreserveHost On ProxyPass / http://localhost:8787/ ProxyPassReverse / http://localhost:8787/
ProxyTimeout 300</VirtualHost>Security
Section titled “Security”Authentication
Section titled “Authentication”The local HTTP server does not include built-in authentication. For production use, add authentication via:
- Reverse Proxy with Basic Auth:
server { location / { auth_basic "ERA Agent"; auth_basic_user_file /etc/nginx/.htpasswd; proxy_pass http://localhost:8787; }}-
API Gateway: Use Kong, Traefik, or AWS API Gateway for advanced auth.
-
Custom Wrapper: Add your own authentication layer:
from flask import Flask, requestimport requests
app = Flask(__name__)
@app.route('/api/<path:path>', methods=['GET', 'POST'])def proxy(path): # Your authentication logic if not is_authenticated(request): return {'error': 'Unauthorized'}, 401
# Forward to ERA Agent return requests.request( method=request.method, url=f'http://localhost:8787/api/{path}', json=request.json ).json()Firewall
Section titled “Firewall”Restrict access to localhost only:
# iptables (Linux)sudo iptables -A INPUT -p tcp --dport 8787 -s 127.0.0.1 -j ACCEPTsudo iptables -A INPUT -p tcp --dport 8787 -j DROP
# pf (macOS)# Add to /etc/pf.conf:block drop in proto tcp from any to any port 8787pass in proto tcp from 127.0.0.1 to any port 8787TLS/HTTPS
Section titled “TLS/HTTPS”Use a reverse proxy for TLS termination:
# Caddy (automatic HTTPS)caddy reverse-proxy --from era-agent.example.com --to localhost:8787
# Nginx with Let's Encryptsudo certbot --nginx -d era-agent.example.comMonitoring
Section titled “Monitoring”Health Checks
Section titled “Health Checks”#!/bin/bashRESPONSE=$(curl -s -o /dev/null -w "%{http_code}" http://localhost:8787/health)
if [ "$RESPONSE" -eq 200 ]; then echo "ERA Agent: OK" exit 0else echo "ERA Agent: FAILED (HTTP $RESPONSE)" exit 1fiPrometheus Metrics
Section titled “Prometheus Metrics”Add metrics endpoint wrapper:
from prometheus_client import Counter, Histogram, generate_latestfrom flask import Flask, Responseimport requestsimport time
app = Flask(__name__)
request_count = Counter('era_requests_total', 'Total requests')request_duration = Histogram('era_request_duration_seconds', 'Request duration')
@app.route('/api/<path:path>', methods=['POST'])def proxy(path): request_count.inc()
start = time.time() response = requests.post( f'http://localhost:8787/api/{path}', json=request.json ) request_duration.observe(time.time() - start)
return response.json()
@app.route('/metrics')def metrics(): return Response(generate_latest(), mimetype='text/plain')Logging
Section titled “Logging”Structured JSON logging:
# Run with JSON logsAGENT_LOG_LEVEL=info ./agent serve 2>&1 | jq -R 'fromjson? | .'
# Log to file./agent serve >> /var/log/era-agent.log 2>&1
# Rotate logslogrotate -f /etc/logrotate.d/era-agentPerformance Tuning
Section titled “Performance Tuning”Concurrent Connections
Section titled “Concurrent Connections”The server handles concurrent requests. Monitor with:
# Check open connectionsnetstat -an | grep 8787 | wc -l
# Check process resourcestop -p $(pgrep agent)Resource Limits
Section titled “Resource Limits”Set system limits:
era-agent soft nofile 4096era-agent hard nofile 8192Load Balancing
Section titled “Load Balancing”Run multiple instances behind a load balancer:
upstream era_agent { server 127.0.0.1:8787; server 127.0.0.1:8788; server 127.0.0.1:8789;}
server { location / { proxy_pass http://era_agent; }}Start multiple instances:
PORT=8787 ./agent serve &PORT=8788 ./agent serve &PORT=8789 ./agent serve &Troubleshooting
Section titled “Troubleshooting”Port Already in Use
Section titled “Port Already in Use”# Find what's using the portlsof -i :8787
# Kill the processkill $(lsof -t -i:8787)
# Or use different portPORT=9000 ./agent servePermission Denied
Section titled “Permission Denied”# Use port > 1024 (no sudo needed)PORT=8787 ./agent serve
# Or give binary permission to bind to port 80sudo setcap CAP_NET_BIND_SERVICE=+eip ./agent./agent serve # Can now use PORT=80Connection Refused
Section titled “Connection Refused”# Check if server is runningcurl http://localhost:8787/health
# Check firewallsudo iptables -L -n | grep 8787
# Check if listeningnetstat -tulpn | grep 8787Comparison: Local vs Cloudflare Workers
Section titled “Comparison: Local vs Cloudflare Workers”| Feature | Local HTTP Server | Cloudflare Workers |
|---|---|---|
| Setup | One command | Deploy with Wrangler |
| Cost | Free (your hardware) | Pay per request |
| Latency | Local (fastest) | Global edge network |
| Scaling | Manual | Automatic |
| Customization | Full control | Limited |
| Auth | DIY | Built-in options |
| Best For | Self-hosted, dev/test | Production, scale |
Next Steps
Section titled “Next Steps”- API Reference - Complete API documentation
- CLI Usage - Command-line interface
- MCP Server - Claude Desktop integration
- Docker Deployment - Containerized deployment