Files
blackroad/scripts/blackroad-agent-daemon.py
Alexa Amundson 78fbe80f2a Initial monorepo — everything BlackRoad in one place
bin/       230 CLI tools (ask-*, br-*, agent-*, roadid, carpool)
scripts/   99 automation scripts
fleet/     Node configs and deployment
workers/   Cloudflare Worker sources (roadpay, road-search, squad webhooks)
roadc/     RoadC programming language
roadnet/   Mesh network (5 APs, WireGuard)
operator/  Memory system scripts
config/    System configs
dotfiles/  Shell configs
docs/      Documentation

BlackRoad OS — Pave Tomorrow.

RoadChain-SHA2048: d1a24f55318d338b
RoadChain-Identity: alexa@sovereign
RoadChain-Full: d1a24f55318d338b24b60bad7be39286379c76ae5470817482100cb0ddbbcb97e147d07ac7243da0a9f0363e4e5c833d612b9c0df3a3cd20802465420278ef74875a5b77f55af6fe42a931b8b635b3d0d0b6bde9abf33dc42eea52bc03c951406d8cbe49f1a3d29b26a94dade05e9477f34a7d4d4c6ec4005c3c2ac54e73a68440c512c8e83fd9b1fe234750b898ef8f4032c23db173961fe225e67a0432b5293a9714f76c5c57ed5fdf35b9fb40fd73c03ebf88b7253c6a0575f5afb6a6b49b3bda310602fb1ef676859962dad2aebbb2875814b30eee0a8ba195e482d4cbc91d8819e7f38f6db53e8063401649c77bb994371473cabfb917fb53e8cbe73d60
2026-03-14 17:08:41 -05:00

311 lines
12 KiB
Python

#!/usr/bin/env python3
"""BlackRoad Agent Daemon — runs on Alice Pi
Lightweight HTTP API for remote shell, file ops, git, and code search.
Called by chat.blackroad.io Worker to provide Claude Code-like capabilities.
Usage: python3 blackroad-agent-daemon.py [port]
"""
import http.server
import json
import subprocess
import os
import sys
import glob as globmod
import traceback
from urllib.parse import urlparse
PORT = int(sys.argv[1]) if len(sys.argv) > 1 else 8080
SANDBOX_ROOT = os.path.expanduser("~/projects")
MAX_OUTPUT = 16000
TIMEOUT = 30
os.makedirs(SANDBOX_ROOT, exist_ok=True)
class AgentHandler(http.server.BaseHTTPRequestHandler):
def log_message(self, fmt, *args):
sys.stderr.write(f"[agent] {fmt % args}\n")
def _cors(self):
self.send_header("Access-Control-Allow-Origin", "*")
self.send_header("Access-Control-Allow-Methods", "GET,POST,OPTIONS")
self.send_header("Access-Control-Allow-Headers", "Content-Type")
def _json(self, code, data):
body = json.dumps(data).encode()
self.send_response(code)
self.send_header("Content-Type", "application/json")
self._cors()
self.send_header("Content-Length", len(body))
self.end_headers()
self.wfile.write(body)
def _body(self):
length = int(self.headers.get("Content-Length", 0))
return json.loads(self.rfile.read(length)) if length else {}
def do_OPTIONS(self):
self.send_response(200)
self._cors()
self.send_header("Content-Length", "0")
self.end_headers()
def do_GET(self):
path = urlparse(self.path).path
if path == "/health":
self._json(200, {"status": "ok", "node": "alice", "pid": os.getpid(), "sandbox": SANDBOX_ROOT})
elif path == "/projects":
projects = []
for root, dirs, files in os.walk(SANDBOX_ROOT):
if ".git" in dirs:
projects.append(root)
dirs.remove(".git") # don't recurse into .git
if len(projects) >= 50:
break
self._json(200, {"projects": projects})
else:
self._json(404, {"error": "unknown endpoint"})
def do_POST(self):
path = urlparse(self.path).path
try:
body = self._body()
except:
self._json(400, {"error": "invalid JSON"})
return
try:
if path == "/exec":
self._handle_exec(body)
elif path == "/file/read":
self._handle_file_read(body)
elif path == "/file/write":
self._handle_file_write(body)
elif path == "/file/edit":
self._handle_file_edit(body)
elif path == "/search":
self._handle_search(body)
elif path == "/glob":
self._handle_glob(body)
elif path == "/git":
self._handle_git(body)
elif path == "/agent":
self._handle_agent(body)
else:
self._json(404, {"error": "unknown endpoint"})
except Exception as e:
self._json(500, {"error": str(e), "trace": traceback.format_exc()})
def _run(self, cmd, cwd=None, timeout=TIMEOUT):
"""Run a command and return (output, exit_code)"""
try:
r = subprocess.run(cmd, shell=True, capture_output=True, text=True,
timeout=timeout, cwd=cwd)
output = r.stdout + r.stderr
if len(output) > MAX_OUTPUT:
output = output[:MAX_OUTPUT] + "\n... (truncated)"
return output, r.returncode
except subprocess.TimeoutExpired:
return f"Command timed out after {timeout}s", 124
except Exception as e:
return str(e), 1
def _handle_exec(self, body):
"""Execute shell command or code snippet"""
cmd = body.get("command", "")
lang = body.get("language", "bash")
code = body.get("code", "")
timeout = min(body.get("timeout", TIMEOUT), 60)
cwd = body.get("cwd", SANDBOX_ROOT)
if cmd:
output, exit_code = self._run(cmd, cwd=cwd, timeout=timeout)
elif code:
if lang in ("python", "python3"):
output, exit_code = self._run(f"python3 -c {repr(code)}", cwd=cwd, timeout=timeout)
elif lang in ("js", "javascript", "node"):
output, exit_code = self._run(f"node -e {repr(code)}", cwd=cwd, timeout=timeout)
elif lang in ("bash", "sh"):
output, exit_code = self._run(code, cwd=cwd, timeout=timeout)
else:
self._json(400, {"error": f"Unsupported language: {lang}"})
return
else:
self._json(400, {"error": "No command or code provided"})
return
self._json(200, {"output": output, "exitCode": exit_code, "language": lang})
def _handle_file_read(self, body):
"""Read a file or list a directory"""
filepath = body.get("path", "")
if not filepath:
self._json(400, {"error": "No path provided"})
return
filepath = os.path.expanduser(filepath)
if not os.path.exists(filepath):
self._json(404, {"error": f"Not found: {filepath}"})
return
if os.path.isdir(filepath):
entries = []
for name in sorted(os.listdir(filepath))[:100]:
full = os.path.join(filepath, name)
entry = {"name": name, "type": "dir" if os.path.isdir(full) else "file"}
if not os.path.isdir(full):
entry["size"] = os.path.getsize(full)
entries.append(entry)
self._json(200, {"type": "directory", "path": filepath, "entries": entries})
else:
try:
with open(filepath, "r") as f:
content = f.read(100000)
lines = content.count("\n") + 1
size = os.path.getsize(filepath)
self._json(200, {"type": "file", "path": filepath, "content": content, "lines": lines, "size": size})
except UnicodeDecodeError:
self._json(200, {"type": "binary", "path": filepath, "size": os.path.getsize(filepath)})
def _handle_file_write(self, body):
"""Write content to a file"""
filepath = body.get("path", "")
content = body.get("content", "")
if not filepath:
self._json(400, {"error": "No path provided"})
return
filepath = os.path.expanduser(filepath)
os.makedirs(os.path.dirname(filepath), exist_ok=True)
with open(filepath, "w") as f:
f.write(content)
self._json(200, {"written": filepath, "size": len(content)})
def _handle_file_edit(self, body):
"""Edit a file: find and replace"""
filepath = body.get("path", "")
old = body.get("old_string", "")
new = body.get("new_string", "")
if not filepath or not old:
self._json(400, {"error": "Need path, old_string, new_string"})
return
filepath = os.path.expanduser(filepath)
if not os.path.exists(filepath):
self._json(404, {"error": f"Not found: {filepath}"})
return
with open(filepath, "r") as f:
content = f.read()
if old not in content:
self._json(400, {"error": "old_string not found in file"})
return
count = content.count(old)
if body.get("replace_all"):
content = content.replace(old, new)
else:
content = content.replace(old, new, 1)
with open(filepath, "w") as f:
f.write(content)
self._json(200, {"edited": filepath, "replacements": count if body.get("replace_all") else 1})
def _handle_search(self, body):
"""Search file contents (like grep)"""
pattern = body.get("pattern", "")
directory = body.get("directory", SANDBOX_ROOT)
max_results = min(body.get("max", 30), 100)
if not pattern:
self._json(400, {"error": "No pattern provided"})
return
output, _ = self._run(
f"grep -rn --include='*.py' --include='*.js' --include='*.ts' --include='*.sh' "
f"--include='*.go' --include='*.rs' --include='*.md' --include='*.json' "
f"--include='*.yaml' --include='*.yml' --include='*.toml' --include='*.html' "
f"--include='*.css' {repr(pattern)} {repr(directory)} | head -{max_results}",
timeout=10
)
matches = [l for l in output.strip().split("\n") if l]
self._json(200, {"pattern": pattern, "directory": directory, "count": len(matches), "matches": matches})
def _handle_glob(self, body):
"""Find files by pattern"""
pattern = body.get("pattern", "")
directory = body.get("directory", SANDBOX_ROOT)
if not pattern:
self._json(400, {"error": "No pattern provided"})
return
full_pattern = os.path.join(directory, "**", pattern)
files = sorted(globmod.glob(full_pattern, recursive=True))[:50]
self._json(200, {"pattern": pattern, "files": files})
def _handle_git(self, body):
"""Git operations"""
repo = body.get("repo", "")
op = body.get("op", "status")
args = body.get("args", "")
if not repo:
self._json(400, {"error": "No repo path provided"})
return
repo = os.path.expanduser(repo)
# Clone is special — doesn't need existing repo
if op == "clone":
output, code = self._run(f"git clone {args}", cwd=SANDBOX_ROOT, timeout=60)
self._json(200, {"op": "clone", "output": output, "exitCode": code})
return
if not os.path.isdir(os.path.join(repo, ".git")):
self._json(400, {"error": f"Not a git repo: {repo}"})
return
git_commands = {
"status": "git status --short",
"log": "git log --oneline -20",
"diff": "git diff | head -300",
"diff-staged": "git diff --staged | head -300",
"branch": "git branch -a",
"add": f"git add {args}",
"commit": f"git commit -m {repr(args)}",
"pull": "git pull",
"push": "git push",
"stash": "git stash",
"stash-pop": "git stash pop",
"remote": "git remote -v",
"blame": f"git blame {args} | head -50",
}
cmd = git_commands.get(op)
if not cmd:
self._json(400, {"error": f"Unknown git op: {op}. Available: {', '.join(git_commands.keys())}"})
return
output, code = self._run(cmd, cwd=repo)
self._json(200, {"op": op, "repo": repo, "output": output, "exitCode": code})
def _handle_agent(self, body):
"""Agentic loop: AI plans steps, executes them sequentially"""
task = body.get("task", "")
if not task:
self._json(400, {"error": "No task provided"})
return
# Just returns the task for now — the Worker's AI will plan and call individual endpoints
self._json(200, {"task": task, "status": "ready", "endpoints": [
"POST /exec — run shell/code",
"POST /file/read — read files",
"POST /file/write — write files",
"POST /file/edit — find & replace",
"POST /search — grep codebase",
"POST /glob — find files",
"POST /git — git operations",
"GET /projects — list git repos",
]})
if __name__ == "__main__":
server = http.server.HTTPServer(("0.0.0.0", PORT), AgentHandler)
print(f"[agent] BlackRoad Agent Daemon on port {PORT}")
print(f"[agent] Sandbox: {SANDBOX_ROOT}")
try:
server.serve_forever()
except KeyboardInterrupt:
print("\n[agent] Shutting down")
server.shutdown()