#!/bin/bash set -e AGENTS_PORT="${AGENTS_PORT:-8800}" PERSONALITIES_DIR="${PERSONALITIES_DIR:-/personalities}" WORKSPACES_DIR="${WORKSPACES_DIR:-/workspaces}" AGENTS_DATA_DIR="${AGENTS_DATA_DIR:-/data/agents}" CLAUDE_MODEL="${CLAUDE_MODEL:-claude-sonnet-4-20250514}" INSTANCES_FILE="/var/run/agents/instances.json" # Initialize instances file echo '[]' > "$INSTANCES_FILE" # Port allocation (start from 8900) NEXT_PORT=8900 # Generate short ID gen_id() { head -c 4 /dev/urandom | xxd -p } # Create HTTP handler create_handler() { cat > /tmp/agents_handler.sh << 'HANDLER' #!/bin/bash PERSONALITIES_DIR="${PERSONALITIES_DIR:-/personalities}" WORKSPACES_DIR="${WORKSPACES_DIR:-/workspaces}" AGENTS_DATA_DIR="${AGENTS_DATA_DIR:-/data/agents}" INSTANCES_FILE="/var/run/agents/instances.json" CLAUDE_MODEL="${CLAUDE_MODEL:-claude-sonnet-4-20250514}" # Read request read -r request_line method=$(echo "$request_line" | cut -d' ' -f1) full_path=$(echo "$request_line" | cut -d' ' -f2) path=$(echo "$full_path" | cut -d'?' -f1) content_length=0 while read -r header; do header=$(echo "$header" | tr -d '\r') [ -z "$header" ] && break if [[ "$header" =~ ^[Cc]ontent-[Ll]ength:\ *([0-9]+) ]]; then content_length="${BASH_REMATCH[1]}" fi done body="" if [ "$content_length" -gt 0 ]; then body=$(head -c "$content_length") fi send_response() { local status="$1" local content_type="$2" local body="$3" local body_length=${#body} printf "HTTP/1.1 %s\r\n" "$status" printf "Content-Type: %s\r\n" "$content_type" printf "Content-Length: %d\r\n" "$body_length" printf "Connection: close\r\n" printf "\r\n" printf "%s" "$body" } send_json() { send_response "200 OK" "application/json" "$1" } send_error() { send_response "$1" "application/json" "{\"error\":\"$2\"}" } # Generate ID gen_id() { head -c 4 /dev/urandom | xxd -p } # Find next available port find_port() { local port=8900 while [ $port -lt 9000 ]; do if ! netstat -tln 2>/dev/null | grep -q ":$port "; then echo $port return fi port=$((port + 1)) done echo "0" } # List personalities list_personalities() { local result="[]" for dir in "$PERSONALITIES_DIR"/*/; do [ ! -d "$dir" ] && continue local name=$(basename "$dir") local desc="" local config="$dir/config.yaml" if [ -f "$config" ]; then desc=$(yq -r '.description // ""' "$config" 2>/dev/null || echo "") fi result=$(echo "$result" | jq --arg n "$name" --arg d "$desc" \ '. + [{name: $n, description: $d}]') done echo "$result" } # Get personality details get_personality() { local name="$1" local dir="$PERSONALITIES_DIR/$name" if [ ! -d "$dir" ]; then echo "" return fi local claude_md="" local config="{}" if [ -f "$dir/CLAUDE.md" ]; then claude_md=$(cat "$dir/CLAUDE.md" | jq -Rs '.') fi if [ -f "$dir/config.yaml" ]; then config=$(yq -o=json '.' "$dir/config.yaml" 2>/dev/null || echo "{}") fi jq -n --arg name "$name" --argjson config "$config" --argjson claude_md "$claude_md" \ '{name: $name, config: $config, claude_md: $claude_md}' } # Spawn instance spawn_instance() { local personality=$(echo "$body" | jq -r '.personality // empty') local workspace=$(echo "$body" | jq -r '.workspace // empty') local name=$(echo "$body" | jq -r '.name // empty') if [ -z "$personality" ]; then send_error "400 Bad Request" "personality required" return fi local personality_dir="$PERSONALITIES_DIR/$personality" if [ ! -d "$personality_dir" ]; then send_error "404 Not Found" "personality not found: $personality" return fi # Generate ID and name local id=$(gen_id) [ -z "$name" ] && name="${personality}-${id}" # Find port local port=$(find_port) if [ "$port" = "0" ]; then send_error "503 Service Unavailable" "no ports available" return fi # Default workspace if [ -z "$workspace" ]; then if [ -f "$personality_dir/config.yaml" ]; then workspace=$(yq -r '.default_workspace // empty' "$personality_dir/config.yaml" 2>/dev/null) fi [ -z "$workspace" ] && workspace="$WORKSPACES_DIR/$name" fi mkdir -p "$workspace" # Get config local model="$CLAUDE_MODEL" local max_turns=10 local tools="Bash,Read,Write,Edit,Glob,Grep" if [ -f "$personality_dir/config.yaml" ]; then model=$(yq -r ".model // \"$CLAUDE_MODEL\"" "$personality_dir/config.yaml" 2>/dev/null) max_turns=$(yq -r '.max_turns // 10' "$personality_dir/config.yaml" 2>/dev/null) local config_tools=$(yq -r '.tools // [] | join(",")' "$personality_dir/config.yaml" 2>/dev/null) [ -n "$config_tools" ] && tools="$config_tools" fi # Build system prompt with peer info local system_prompt="" if [ -f "$personality_dir/CLAUDE.md" ]; then system_prompt=$(cat "$personality_dir/CLAUDE.md") fi # Add peer info local instances=$(cat "$INSTANCES_FILE") local peers=$(echo "$instances" | jq -r '.[] | "- \(.name): port \(.port), personality: \(.personality)"' 2>/dev/null || echo "") if [ -n "$peers" ]; then system_prompt="$system_prompt ## Running Peer Agents $peers You can communicate with peers via their HTTP API on localhost:{port}." fi # Start Claude instance local log_file="/var/log/agents/${name}.log" mkdir -p /var/log/agents cd "$workspace" # Create wrapper script for this instance cat > "/tmp/agent_${id}.sh" << WRAPPER #!/bin/bash cd "$workspace" exec claude --print \ --model "$model" \ --max-turns "$max_turns" \ --system-prompt "\$(cat << 'SYSPROMPT' $system_prompt SYSPROMPT )" \ --allowedTools "$tools" \ "\$@" WRAPPER chmod +x "/tmp/agent_${id}.sh" # For now, we don't start a long-running process # The instance is registered and can be invoked via the agents API # Record instance local instance=$(jq -n \ --arg id "$id" \ --arg name "$name" \ --arg personality "$personality" \ --arg workspace "$workspace" \ --argjson port "$port" \ --arg status "ready" \ --arg started_at "$(date -Iseconds)" \ '{id:$id, name:$name, personality:$personality, workspace:$workspace, port:$port, status:$status, started_at:$started_at}') # Update instances file local instances=$(cat "$INSTANCES_FILE") echo "$instances" | jq --argjson inst "$instance" '. + [$inst]' > "$INSTANCES_FILE" send_json "$instance" } # List instances list_instances() { cat "$INSTANCES_FILE" } # Get instance get_instance() { local id="$1" local instance=$(jq -r --arg id "$id" '.[] | select(.id == $id or .name == $id)' "$INSTANCES_FILE") if [ -z "$instance" ] || [ "$instance" = "null" ]; then send_error "404 Not Found" "instance not found" return fi echo "$instance" } # Delete instance delete_instance() { local id="$1" local instance=$(jq -r --arg id "$id" '.[] | select(.id == $id or .name == $id)' "$INSTANCES_FILE") if [ -z "$instance" ] || [ "$instance" = "null" ]; then send_error "404 Not Found" "instance not found" return fi # Kill process if running local pid=$(echo "$instance" | jq -r '.pid // empty') if [ -n "$pid" ] && [ "$pid" != "null" ]; then kill "$pid" 2>/dev/null || true fi # Remove from instances local instances=$(cat "$INSTANCES_FILE") echo "$instances" | jq --arg id "$id" '[.[] | select(.id != $id and .name != $id)]' > "$INSTANCES_FILE" send_json '{"deleted":true}' } # Invoke instance (run a prompt) invoke_instance() { local id="$1" local instance=$(jq -r --arg id "$id" '.[] | select(.id == $id or .name == $id)' "$INSTANCES_FILE") if [ -z "$instance" ] || [ "$instance" = "null" ]; then send_error "404 Not Found" "instance not found" return fi local prompt=$(echo "$body" | jq -r '.prompt // empty') if [ -z "$prompt" ]; then send_error "400 Bad Request" "prompt required" return fi local inst_id=$(echo "$instance" | jq -r '.id') local workspace=$(echo "$instance" | jq -r '.workspace') cd "$workspace" local result=$("/tmp/agent_${inst_id}.sh" "$prompt" 2>&1) || true local result_json=$(echo "$result" | jq -Rs '.') send_json "{\"result\":$result_json}" } # Route requests case "$method:$path" in GET:/health) send_json '{"status":"ok"}' ;; GET:/personalities) result=$(list_personalities) send_json "$result" ;; GET:/personalities/*) name="${path#/personalities/}" result=$(get_personality "$name") if [ -z "$result" ]; then send_error "404 Not Found" "personality not found" else send_json "$result" fi ;; POST:/instances) spawn_instance ;; GET:/instances) result=$(list_instances) send_json "$result" ;; GET:/instances/*) id="${path#/instances/}" result=$(get_instance "$id") [ -n "$result" ] && send_json "$result" ;; DELETE:/instances/*) id="${path#/instances/}" delete_instance "$id" ;; POST:/instances/*/invoke) id=$(echo "$path" | sed 's|/instances/\([^/]*\)/invoke|\1|') invoke_instance "$id" ;; POST:/spawn/*) personality="${path#/spawn/}" body="{\"personality\":\"$personality\"}" spawn_instance ;; *) send_error "404 Not Found" "Unknown endpoint. Try: GET /personalities, GET /instances, POST /instances" ;; esac HANDLER chmod +x /tmp/agents_handler.sh # Export vars export PERSONALITIES_DIR export WORKSPACES_DIR export AGENTS_DATA_DIR export INSTANCES_FILE export CLAUDE_MODEL } # Cleanup stale instances on startup cleanup_stale() { echo '[]' > "$INSTANCES_FILE" } # Serve serve() { echo "Starting Agents API on port $AGENTS_PORT..." echo "Personalities: $PERSONALITIES_DIR" echo "Workspaces: $WORKSPACES_DIR" exec socat TCP-LISTEN:$AGENTS_PORT,reuseaddr,fork EXEC:"/tmp/agents_handler.sh" } cleanup_stale create_handler serve