""" Execution API Routes — Terminal, FileSystem, GitHub, DAG Real agent execution endpoints """ import asyncio import json import os import time import uuid from typing import Dict, List, Optional import structlog from fastapi import APIRouter, HTTPException, Request, WebSocket, WebSocketDisconnect from pydantic import BaseModel log = structlog.get_logger() router = APIRouter() WORKSPACE = os.environ.get("WORKSPACE_DIR", "/tmp/god_workspace") # ─── Request Models ─────────────────────────────────────────────────────────── class ExecRequest(BaseModel): command: str session_id: str = "" task_id: str = "" cwd: str = "" timeout: int = 120 class ExecChainRequest(BaseModel): commands: List[str] session_id: str = "" task_id: str = "" cwd: str = "" stop_on_error: bool = True class FileReadRequest(BaseModel): filename: str class FileWriteRequest(BaseModel): filename: str content: str class FilePatchRequest(BaseModel): filename: str old_str: str new_str: str class FileDeleteRequest(BaseModel): filename: str class FileMoveRequest(BaseModel): src: str dst: str class FileSearchRequest(BaseModel): query: str path: str = "" pattern: str = "*" class GitHubCloneRequest(BaseModel): repo_url: str dest: str = "" branch: str = "" class GitHubCreateRepoRequest(BaseModel): name: str description: str = "" private: bool = False class GitHubCommitRequest(BaseModel): message: str cwd: str = "" files: Optional[List[str]] = None class GitHubPushRequest(BaseModel): branch: str = "" cwd: str = "" force: bool = False class GitHubPRRequest(BaseModel): owner: str repo: str title: str body: str = "" head: str = "main" base: str = "main" class DAGCreateRequest(BaseModel): goal: str steps: List[Dict] session_id: str = "" class FilesWriteRequest(BaseModel): files: List[Dict] # ─── Terminal Routes ────────────────────────────────────────────────────────── @router.post("/execute") async def execute_command(req: ExecRequest, request: Request): """Execute a shell command in the sandbox.""" terminal = getattr(request.app.state, "terminal_engine", None) if not terminal: return {"success": False, "error": "Terminal engine not available"} result = await terminal.execute( req.command, session_id=req.session_id, task_id=req.task_id, cwd=req.cwd or WORKSPACE, timeout=req.timeout, ) return result @router.post("/execute/chain") async def execute_chain(req: ExecChainRequest, request: Request): """Execute a chain of shell commands.""" terminal = getattr(request.app.state, "terminal_engine", None) if not terminal: return {"success": False, "error": "Terminal engine not available"} result = await terminal.execute_chain( req.commands, session_id=req.session_id, task_id=req.task_id, cwd=req.cwd or WORKSPACE, stop_on_error=req.stop_on_error, ) return result @router.post("/execute/kill/{session_id}") async def kill_process(session_id: str, request: Request): """Kill a running process.""" terminal = getattr(request.app.state, "terminal_engine", None) if not terminal: return {"success": False, "error": "Terminal engine not available"} return await terminal.kill(session_id) @router.get("/execute/history/{session_id}") async def get_history(session_id: str, request: Request): """Get command history for a session.""" terminal = getattr(request.app.state, "terminal_engine", None) if not terminal: return {"history": []} return {"history": terminal.get_session_history(session_id)} # ─── FileSystem Routes ──────────────────────────────────────────────────────── @router.post("/fs/read") async def read_file(req: FileReadRequest, request: Request): fs = getattr(request.app.state, "fs_tool", None) if not fs: return {"success": False, "error": "FileSystem tool not available"} return await fs.read_file(req.filename) @router.post("/fs/write") async def write_file(req: FileWriteRequest, request: Request): fs = getattr(request.app.state, "fs_tool", None) if not fs: return {"success": False, "error": "FileSystem tool not available"} return await fs.write_file(req.filename, req.content) @router.post("/fs/write-many") async def write_files(req: FilesWriteRequest, request: Request): fs = getattr(request.app.state, "fs_tool", None) if not fs: return {"success": False, "error": "FileSystem tool not available"} return await fs.write_files(req.files) @router.post("/fs/patch") async def patch_file(req: FilePatchRequest, request: Request): fs = getattr(request.app.state, "fs_tool", None) if not fs: return {"success": False, "error": "FileSystem tool not available"} return await fs.patch_file(req.filename, req.old_str, req.new_str) @router.post("/fs/delete") async def delete_file(req: FileDeleteRequest, request: Request): fs = getattr(request.app.state, "fs_tool", None) if not fs: return {"success": False, "error": "FileSystem tool not available"} return await fs.delete_file(req.filename) @router.post("/fs/move") async def move_file(req: FileMoveRequest, request: Request): fs = getattr(request.app.state, "fs_tool", None) if not fs: return {"success": False, "error": "FileSystem tool not available"} return await fs.move_file(req.src, req.dst) @router.post("/fs/search") async def search_files(req: FileSearchRequest, request: Request): fs = getattr(request.app.state, "fs_tool", None) if not fs: return {"success": False, "error": "FileSystem tool not available"} return await fs.search_files(req.query, req.path, req.pattern) @router.get("/fs/tree") async def get_tree(path: str = "", request: Request = None): fs = getattr(request.app.state, "fs_tool", None) if not fs: return {"success": False, "error": "FileSystem tool not available"} return await fs.tree(path) @router.get("/fs/list") async def list_dir(path: str = "", request: Request = None): fs = getattr(request.app.state, "fs_tool", None) if not fs: return {"success": False, "error": "FileSystem tool not available"} return await fs.list_dir(path) @router.get("/workspace") async def get_workspace_info(request: Request): """Get workspace info and file tree.""" fs = getattr(request.app.state, "fs_tool", None) if not fs: return {"path": WORKSPACE, "files": [], "file_count": 0} tree = await fs.tree() files_list = [] for root, dirs, files in os.walk(WORKSPACE): dirs[:] = [d for d in dirs if not d.startswith(".") and d not in ("node_modules", "__pycache__")] for f in files: files_list.append(os.path.relpath(os.path.join(root, f), WORKSPACE)) if len(files_list) > 200: break return { "path": WORKSPACE, "file_count": len(files_list), "files": files_list[:100], "tree": tree.get("tree", ""), } # ─── GitHub Routes ──────────────────────────────────────────────────────────── @router.post("/github/clone") async def github_clone(req: GitHubCloneRequest, request: Request): gh = getattr(request.app.state, "github_tool", None) if not gh: return {"success": False, "error": "GitHub tool not available"} return await gh.clone_repo(req.repo_url, req.dest, req.branch) @router.post("/github/create-repo") async def github_create_repo(req: GitHubCreateRepoRequest, request: Request): gh = getattr(request.app.state, "github_tool", None) if not gh: return {"success": False, "error": "GitHub tool not available"} return await gh.create_repo(req.name, req.description, req.private) @router.post("/github/commit") async def github_commit(req: GitHubCommitRequest, request: Request): gh = getattr(request.app.state, "github_tool", None) if not gh: return {"success": False, "error": "GitHub tool not available"} return await gh.commit_changes(req.message, req.cwd or WORKSPACE, req.files) @router.post("/github/push") async def github_push(req: GitHubPushRequest, request: Request): gh = getattr(request.app.state, "github_tool", None) if not gh: return {"success": False, "error": "GitHub tool not available"} return await gh.push_changes(req.branch, req.cwd or WORKSPACE, req.force) @router.post("/github/pr") async def github_open_pr(req: GitHubPRRequest, request: Request): gh = getattr(request.app.state, "github_tool", None) if not gh: return {"success": False, "error": "GitHub tool not available"} return await gh.open_pr(req.owner, req.repo, req.title, req.body, req.head, req.base) @router.get("/github/status") async def github_status(cwd: str = "", request: Request = None): gh = getattr(request.app.state, "github_tool", None) if not gh: return {"success": False, "error": "GitHub tool not available"} return await gh.status(cwd or WORKSPACE) @router.get("/github/issues/{owner}/{repo}") async def github_issues(owner: str, repo: str, request: Request): gh = getattr(request.app.state, "github_tool", None) if not gh: return {"success": False, "error": "GitHub tool not available"} return await gh.read_issues(owner, repo) # ─── DAG Routes ─────────────────────────────────────────────────────────────── @router.post("/dag/create") async def create_dag(req: DAGCreateRequest, request: Request): dag_engine = getattr(request.app.state, "dag_engine", None) if not dag_engine: return {"success": False, "error": "DAG engine not available"} dag = dag_engine.build_from_steps(req.steps, req.goal) return {"success": True, "dag_id": dag.id, "dag": dag.to_dict()} @router.get("/dag/list") async def list_dags(request: Request): dag_engine = getattr(request.app.state, "dag_engine", None) if not dag_engine: return {"dags": []} return {"dags": dag_engine.get_all_dags()} @router.get("/dag/{dag_id}") async def get_dag(dag_id: str, request: Request): dag_engine = getattr(request.app.state, "dag_engine", None) if not dag_engine: return {"success": False, "error": "DAG engine not available"} dag = dag_engine.get_dag(dag_id) if not dag: raise HTTPException(status_code=404, detail="DAG not found") return dag.to_dict() # ─── Self-Repair Route ──────────────────────────────────────────────────────── class SelfRepairRequest(BaseModel): command: str error_output: str session_id: str = "" task_id: str = "" max_attempts: int = 3 @router.post("/self-repair") async def self_repair(req: SelfRepairRequest, request: Request): terminal = getattr(request.app.state, "terminal_engine", None) ai_router = getattr(request.app.state, "ai_router", None) if not terminal: return {"success": False, "error": "Terminal engine not available"} return await terminal.self_repair( req.command, req.error_output, ai_router=ai_router, session_id=req.session_id, task_id=req.task_id, max_attempts=req.max_attempts, )