| """ |
| 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") |
|
|
|
|
| |
|
|
| 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] |
|
|
|
|
| |
|
|
| @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)} |
|
|
|
|
| |
|
|
| @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", ""), |
| } |
|
|
|
|
| |
|
|
| @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) |
|
|
|
|
| |
|
|
| @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() |
|
|
|
|
| |
|
|
| 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, |
| ) |
|
|