|
| 1 | +#!/bin/sh |
| 2 | +# Universal Agent Runtime — LLM-driven plan+execute+fix loop |
| 3 | +# |
| 4 | +# This is the ONLY script that runs inside microVMs. It reads the contract |
| 5 | +# instructions from /context/brief.md, asks the LLM to plan which tools |
| 6 | +# to use, executes the plan step by step, and self-repairs on failure. |
| 7 | +# |
| 8 | +# The contract defines WHAT. The LLM decides HOW. |
| 9 | +set -e |
| 10 | + |
| 11 | +# Install base tools (Alpine, ~3 seconds) |
| 12 | +apk add --no-cache -q curl jq git bash nodejs npm python3 2>/dev/null || true |
| 13 | + |
| 14 | +MAX_ITERATIONS=10 |
| 15 | +LLM_PROXY="http://localhost:4000/api/v1/vm/llm/chat" |
| 16 | + |
| 17 | +echo "=== Agent Runtime Starting ===" |
| 18 | + |
| 19 | +# 1. Read the brief (contract instructions for this stage) |
| 20 | +BRIEF="" |
| 21 | +if [ -f /context/brief.md ]; then |
| 22 | + BRIEF=$(cat /context/brief.md) |
| 23 | +else |
| 24 | + echo "ERROR: No /context/brief.md found" |
| 25 | + exit 1 |
| 26 | +fi |
| 27 | + |
| 28 | +# 2. Discover available context files |
| 29 | +CONTEXT_FILES=$(ls -1 /context/ 2>/dev/null | grep -v brief.md | head -20) |
| 30 | +CONTEXT_CONTENTS="" |
| 31 | +for f in $CONTEXT_FILES; do |
| 32 | + if [ -f "/context/$f" ]; then |
| 33 | + SIZE=$(wc -c < "/context/$f" | tr -d ' ') |
| 34 | + if [ "$SIZE" -lt 8000 ]; then |
| 35 | + CONTEXT_CONTENTS="$CONTEXT_CONTENTS |
| 36 | +--- /context/$f --- |
| 37 | +$(cat /context/$f)" |
| 38 | + else |
| 39 | + CONTEXT_CONTENTS="$CONTEXT_CONTENTS |
| 40 | +--- /context/$f (${SIZE} bytes, showing first 2000) --- |
| 41 | +$(head -c 2000 /context/$f)" |
| 42 | + fi |
| 43 | + fi |
| 44 | +done |
| 45 | + |
| 46 | +# 3. Discover available tools |
| 47 | +TOOLS="curl, jq, git, bash, node, npm, python3" |
| 48 | +if command -v vercel > /dev/null 2>&1; then TOOLS="$TOOLS, vercel"; fi |
| 49 | +if command -v pdflatex > /dev/null 2>&1; then TOOLS="$TOOLS, pdflatex"; fi |
| 50 | + |
| 51 | +# Check for injected credentials |
| 52 | +CREDS="" |
| 53 | +if [ -n "$GH_TOKEN" ]; then |
| 54 | + CREDS="$CREDS GitHub (GH_TOKEN set)," |
| 55 | + git config --global user.email "agent-os@agenthero.work" |
| 56 | + git config --global user.name "Agent-OS Pipeline" |
| 57 | + git config --global init.defaultBranch main |
| 58 | + git config --global url."https://x-access-token:${GH_TOKEN}@github.com/".insteadOf "https://github.com/" |
| 59 | +fi |
| 60 | +if [ -n "$VERCEL_TOKEN" ]; then |
| 61 | + CREDS="$CREDS Vercel (VERCEL_TOKEN set)," |
| 62 | + npm install -g vercel 2>/dev/null || true |
| 63 | +fi |
| 64 | + |
| 65 | +echo "Tools: $TOOLS" |
| 66 | +echo "Credentials: ${CREDS:-none}" |
| 67 | +echo "Context files: $(echo $CONTEXT_FILES | tr '\n' ', ')" |
| 68 | + |
| 69 | +# 4. Ask LLM to plan the approach |
| 70 | +echo "" |
| 71 | +echo "=== Planning ===" |
| 72 | + |
| 73 | +PLAN_PROMPT="You are an autonomous agent running inside an isolated Linux container. |
| 74 | +
|
| 75 | +YOUR TASK: |
| 76 | +$BRIEF |
| 77 | +
|
| 78 | +AVAILABLE TOOLS: $TOOLS |
| 79 | +AVAILABLE CREDENTIALS: ${CREDS:-none} |
| 80 | +OUTPUT DIRECTORY: /shared/output/ (write ALL output files here) |
| 81 | +
|
| 82 | +CONTEXT FILES AVAILABLE: |
| 83 | +$CONTEXT_CONTENTS |
| 84 | +
|
| 85 | +CRITICAL RULES: |
| 86 | +- You have NO access to external APIs or websites. Do NOT use curl to fetch data from APIs. |
| 87 | +- Generate all content from YOUR OWN KNOWLEDGE. You are an expert — write the data yourself. |
| 88 | +- For JSON data files, write them directly using cat/heredoc or a python/node script. |
| 89 | +- For markdown files, write them directly using cat/heredoc. |
| 90 | +- For HTML/CSS/JS dashboards, write a python script that generates the complete file. |
| 91 | +- Use real, specific, accurate data from your training knowledge. |
| 92 | +- If credentials are available (GH_TOKEN, VERCEL_TOKEN), use git/vercel CLI tools for deployment. |
| 93 | +
|
| 94 | +INSTRUCTIONS: |
| 95 | +1. Plan your approach step by step |
| 96 | +2. For each step, provide the EXACT shell command to run |
| 97 | +3. Use the context files to inform your work |
| 98 | +4. Write all output to /shared/output/ |
| 99 | +5. If you need to create directories, use mkdir -p |
| 100 | +6. For large file generation, write a python3 script to /tmp/gen.py then run it |
| 101 | +
|
| 102 | +OUTPUT FORMAT — respond with ONLY a JSON array of steps: |
| 103 | +[ |
| 104 | + {\"description\": \"what this step does\", \"command\": \"the exact shell command\"}, |
| 105 | + {\"description\": \"next step\", \"command\": \"next command\"}, |
| 106 | + ... |
| 107 | +] |
| 108 | +
|
| 109 | +IMPORTANT: |
| 110 | +- Output ONLY the JSON array, no markdown fences, no explanation |
| 111 | +- Each command must be a single shell command (use && to chain if needed) |
| 112 | +- For multi-line file writes, use heredoc: cat > /shared/output/file.md << 'HEREDOC' ... HEREDOC |
| 113 | +- For complex file generation, write a python3 or node script to /tmp/ first, then execute it |
| 114 | +- NEVER try to curl/fetch external URLs for data — generate everything from your knowledge" |
| 115 | + |
| 116 | +PLAN_RESPONSE=$(curl -s -X POST "$LLM_PROXY" \ |
| 117 | + -H "Content-Type: application/json" \ |
| 118 | + -H "Authorization: Bearer $JOB_TOKEN" \ |
| 119 | + -d "$(jq -n \ |
| 120 | + --arg prompt "$PLAN_PROMPT" \ |
| 121 | + '{messages: [{role: "user", content: $prompt}], model: "gpt-4o", max_tokens: 8192, temperature: 0.2}')" 2>&1) |
| 122 | + |
| 123 | +# Extract content from LLM response |
| 124 | +PLAN="" |
| 125 | +if echo "$PLAN_RESPONSE" | jq -e '.content' > /dev/null 2>&1; then |
| 126 | + PLAN=$(echo "$PLAN_RESPONSE" | jq -r '.content') |
| 127 | +else |
| 128 | + echo "ERROR: LLM proxy not available or returned invalid response" |
| 129 | + echo "Response: $(echo "$PLAN_RESPONSE" | head -c 500)" |
| 130 | + exit 1 |
| 131 | +fi |
| 132 | + |
| 133 | +# Strip markdown code fences if present |
| 134 | +PLAN=$(echo "$PLAN" | sed 's/^```json//' | sed 's/^```//' | sed 's/```$//') |
| 135 | + |
| 136 | +# Validate JSON |
| 137 | +if ! echo "$PLAN" | jq -e '.' > /dev/null 2>&1; then |
| 138 | + echo "ERROR: LLM returned invalid JSON plan" |
| 139 | + echo "Plan: $(echo "$PLAN" | head -c 500)" |
| 140 | + exit 1 |
| 141 | +fi |
| 142 | + |
| 143 | +STEP_COUNT=$(echo "$PLAN" | jq 'length') |
| 144 | +echo "Plan: $STEP_COUNT steps" |
| 145 | + |
| 146 | +# 5. Execute plan step by step |
| 147 | +echo "" |
| 148 | +echo "=== Executing ===" |
| 149 | + |
| 150 | +ITERATION=0 |
| 151 | +STEP_INDEX=0 |
| 152 | + |
| 153 | +while [ "$STEP_INDEX" -lt "$STEP_COUNT" ] && [ "$ITERATION" -lt "$MAX_ITERATIONS" ]; do |
| 154 | + ITERATION=$((ITERATION + 1)) |
| 155 | + DESC=$(echo "$PLAN" | jq -r ".[$STEP_INDEX].description") |
| 156 | + CMD=$(echo "$PLAN" | jq -r ".[$STEP_INDEX].command") |
| 157 | + |
| 158 | + echo "" |
| 159 | + echo "--- Step $((STEP_INDEX + 1))/$STEP_COUNT: $DESC ---" |
| 160 | + echo "\$ $CMD" |
| 161 | + |
| 162 | + # Execute the command |
| 163 | + OUTPUT=$(eval "$CMD" 2>&1) || { |
| 164 | + EXIT_CODE=$? |
| 165 | + echo "FAILED (exit $EXIT_CODE): $OUTPUT" |
| 166 | + |
| 167 | + # Self-repair: ask LLM to fix |
| 168 | + echo "Asking LLM to fix..." |
| 169 | + |
| 170 | + FIX_PROMPT="A command failed during autonomous execution. |
| 171 | +
|
| 172 | +Step description: $DESC |
| 173 | +Command: $CMD |
| 174 | +Error output: $OUTPUT |
| 175 | +Exit code: $EXIT_CODE |
| 176 | +
|
| 177 | +Available tools: $TOOLS |
| 178 | +Working directory: $(pwd) |
| 179 | +
|
| 180 | +Provide a FIXED command that accomplishes the same goal. Output ONLY the shell command, nothing else." |
| 181 | + |
| 182 | + FIX_RESPONSE=$(curl -s -X POST "$LLM_PROXY" \ |
| 183 | + -H "Content-Type: application/json" \ |
| 184 | + -H "Authorization: Bearer $JOB_TOKEN" \ |
| 185 | + -d "$(jq -n \ |
| 186 | + --arg prompt "$FIX_PROMPT" \ |
| 187 | + '{messages: [{role: "user", content: $prompt}], model: "gpt-4o", max_tokens: 2048, temperature: 0.1}')" 2>&1) |
| 188 | + |
| 189 | + if echo "$FIX_RESPONSE" | jq -e '.content' > /dev/null 2>&1; then |
| 190 | + FIXED_CMD=$(echo "$FIX_RESPONSE" | jq -r '.content' | sed 's/^```sh//' | sed 's/^```bash//' | sed 's/^```//' | sed 's/```$//' | tr -d '\n') |
| 191 | + echo "Fix: $FIXED_CMD" |
| 192 | + eval "$FIXED_CMD" 2>&1 || echo "Fix also failed — continuing" |
| 193 | + else |
| 194 | + echo "Could not get fix from LLM — continuing" |
| 195 | + fi |
| 196 | + } |
| 197 | + |
| 198 | + # Show truncated output |
| 199 | + if [ -n "$OUTPUT" ]; then |
| 200 | + echo "$(echo "$OUTPUT" | head -5)" |
| 201 | + LINES=$(echo "$OUTPUT" | wc -l | tr -d ' ') |
| 202 | + if [ "$LINES" -gt 5 ]; then echo "... ($LINES lines total)"; fi |
| 203 | + fi |
| 204 | + |
| 205 | + STEP_INDEX=$((STEP_INDEX + 1)) |
| 206 | +done |
| 207 | + |
| 208 | +# 6. Verify output |
| 209 | +echo "" |
| 210 | +echo "=== Output Verification ===" |
| 211 | +if [ -d /shared/output ]; then |
| 212 | + echo "Files in /shared/output/:" |
| 213 | + find /shared/output -type f | while read f; do |
| 214 | + SIZE=$(wc -c < "$f" | tr -d ' ') |
| 215 | + echo " $f ($SIZE bytes)" |
| 216 | + done |
| 217 | +else |
| 218 | + echo "WARNING: /shared/output/ is empty" |
| 219 | +fi |
| 220 | + |
| 221 | +echo "" |
| 222 | +echo "=== Agent Runtime Complete ($ITERATION iterations) ===" |
0 commit comments