mirror of
https://github.com/leonvanzyl/autocoder.git
synced 2026-02-01 15:03:36 +00:00
Token reduction (~40% per session, ~2.3M fewer tokens per 200-feature project): - Agent-type-specific tool lists: coding 9, testing 5, init 5 (was 19 for all) - Right-sized max_turns: coding 300, testing 100 (was 1000 for all) - Trimmed coding prompt template (~150 lines removed) - Streamlined testing prompt with batch support - YOLO mode now strips browser testing instructions from prompt - Added Grep, WebFetch, WebSearch to expand project session Performance improvements: - Rate limit retries start at ~15s with jitter (was fixed 60s) - Post-spawn delay reduced to 0.5s (was 2s) - Orchestrator consolidated to 1 DB query per loop (was 5-7) - Testing agents batch 3 features per session (was 1) - Smart context compaction preserves critical state, discards noise Bug fixes: - Removed ghost feature_release_testing MCP tool (wasted tokens every test session) - Forward all 9 Vertex AI env vars to chat sessions (was missing 3) - Fix DetachedInstanceError risk in test batch ORM access - Prevent duplicate testing of same features in parallel mode Code deduplication: - _get_project_path(): 9 copies -> 1 shared utility (project_helpers.py) - validate_project_name(): 9 copies -> 2 variants in 1 file (validation.py) - ROOT_DIR: 10 copies -> 1 definition (chat_constants.py) - API_ENV_VARS: 4 copies -> 1 source of truth (env_constants.py) Security hardening: - Unified sensitive directory blocklist (14 dirs, was two divergent lists) - Cached get_blocked_paths() for O(1) directory listing checks - Terminal security warning when ALLOW_REMOTE=1 exposes WebSocket - 20 new security tests for EXTRA_READ_PATHS blocking - Extracted _validate_command_list() and _validate_pkill_processes() helpers Type safety: - 87 mypy errors -> 0 across 58 source files - Installed types-PyYAML for proper yaml stub types - Fixed SQLAlchemy Column[T] coercions across all routers Dead code removed: - 13 files deleted (~2,679 lines): unused UI components, debug logs, outdated docs - 7 unused npm packages removed (Radix UI components with 0 imports) - AgentAvatar.tsx reduced from 615 -> 119 lines (SVGs extracted to mascotData.tsx) New CLI options: - --testing-batch-size (1-5) for parallel mode test batching - --testing-feature-ids for direct multi-feature testing Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
262 lines
8.3 KiB
Python
262 lines
8.3 KiB
Python
"""
|
|
FastAPI Main Application
|
|
========================
|
|
|
|
Main entry point for the Autonomous Coding UI server.
|
|
Provides REST API, WebSocket, and static file serving.
|
|
"""
|
|
|
|
import asyncio
|
|
import logging
|
|
import os
|
|
import shutil
|
|
import sys
|
|
from contextlib import asynccontextmanager
|
|
from pathlib import Path
|
|
|
|
# Fix for Windows subprocess support in asyncio
|
|
if sys.platform == "win32":
|
|
asyncio.set_event_loop_policy(asyncio.WindowsProactorEventLoopPolicy())
|
|
|
|
from dotenv import load_dotenv
|
|
|
|
# Load environment variables from .env file if present
|
|
load_dotenv()
|
|
|
|
from fastapi import FastAPI, HTTPException, Request, WebSocket
|
|
from fastapi.middleware.cors import CORSMiddleware
|
|
from fastapi.responses import FileResponse
|
|
from fastapi.staticfiles import StaticFiles
|
|
|
|
from .routers import (
|
|
agent_router,
|
|
assistant_chat_router,
|
|
devserver_router,
|
|
expand_project_router,
|
|
features_router,
|
|
filesystem_router,
|
|
projects_router,
|
|
schedules_router,
|
|
settings_router,
|
|
spec_creation_router,
|
|
terminal_router,
|
|
)
|
|
from .schemas import SetupStatus
|
|
from .services.assistant_chat_session import cleanup_all_sessions as cleanup_assistant_sessions
|
|
from .services.chat_constants import ROOT_DIR
|
|
from .services.dev_server_manager import (
|
|
cleanup_all_devservers,
|
|
cleanup_orphaned_devserver_locks,
|
|
)
|
|
from .services.expand_chat_session import cleanup_all_expand_sessions
|
|
from .services.process_manager import cleanup_all_managers, cleanup_orphaned_locks
|
|
from .services.scheduler_service import cleanup_scheduler, get_scheduler
|
|
from .services.terminal_manager import cleanup_all_terminals
|
|
from .websocket import project_websocket
|
|
|
|
# Paths
|
|
UI_DIST_DIR = ROOT_DIR / "ui" / "dist"
|
|
|
|
|
|
@asynccontextmanager
|
|
async def lifespan(app: FastAPI):
|
|
"""Lifespan context manager for startup and shutdown."""
|
|
# Startup - clean up orphaned lock files from previous runs
|
|
cleanup_orphaned_locks()
|
|
cleanup_orphaned_devserver_locks()
|
|
|
|
# Start the scheduler service
|
|
scheduler = get_scheduler()
|
|
await scheduler.start()
|
|
|
|
yield
|
|
|
|
# Shutdown - cleanup scheduler first to stop triggering new starts
|
|
await cleanup_scheduler()
|
|
# Then cleanup all running agents, sessions, terminals, and dev servers
|
|
await cleanup_all_managers()
|
|
await cleanup_assistant_sessions()
|
|
await cleanup_all_expand_sessions()
|
|
await cleanup_all_terminals()
|
|
await cleanup_all_devservers()
|
|
|
|
|
|
# Create FastAPI app
|
|
app = FastAPI(
|
|
title="Autonomous Coding UI",
|
|
description="Web UI for the Autonomous Coding Agent",
|
|
version="1.0.0",
|
|
lifespan=lifespan,
|
|
)
|
|
|
|
# Module logger
|
|
logger = logging.getLogger(__name__)
|
|
|
|
# Check if remote access is enabled via environment variable
|
|
# Set by start_ui.py when --host is not 127.0.0.1
|
|
ALLOW_REMOTE = os.environ.get("AUTOCODER_ALLOW_REMOTE", "").lower() in ("1", "true", "yes")
|
|
|
|
if ALLOW_REMOTE:
|
|
logger.warning(
|
|
"ALLOW_REMOTE is enabled. Terminal WebSocket is exposed without sandboxing. "
|
|
"Only use this in trusted network environments."
|
|
)
|
|
|
|
# CORS - allow all origins when remote access is enabled, otherwise localhost only
|
|
if ALLOW_REMOTE:
|
|
app.add_middleware(
|
|
CORSMiddleware,
|
|
allow_origins=["*"], # Allow all origins for remote access
|
|
allow_credentials=True,
|
|
allow_methods=["*"],
|
|
allow_headers=["*"],
|
|
)
|
|
else:
|
|
app.add_middleware(
|
|
CORSMiddleware,
|
|
allow_origins=[
|
|
"http://localhost:5173", # Vite dev server
|
|
"http://127.0.0.1:5173",
|
|
"http://localhost:8888", # Production
|
|
"http://127.0.0.1:8888",
|
|
],
|
|
allow_credentials=True,
|
|
allow_methods=["*"],
|
|
allow_headers=["*"],
|
|
)
|
|
|
|
|
|
# ============================================================================
|
|
# Security Middleware
|
|
# ============================================================================
|
|
|
|
if not ALLOW_REMOTE:
|
|
@app.middleware("http")
|
|
async def require_localhost(request: Request, call_next):
|
|
"""Only allow requests from localhost (disabled when AUTOCODER_ALLOW_REMOTE=1)."""
|
|
client_host = request.client.host if request.client else None
|
|
|
|
# Allow localhost connections
|
|
if client_host not in ("127.0.0.1", "::1", "localhost", None):
|
|
raise HTTPException(status_code=403, detail="Localhost access only")
|
|
|
|
return await call_next(request)
|
|
|
|
|
|
# ============================================================================
|
|
# Include Routers
|
|
# ============================================================================
|
|
|
|
app.include_router(projects_router)
|
|
app.include_router(features_router)
|
|
app.include_router(agent_router)
|
|
app.include_router(schedules_router)
|
|
app.include_router(devserver_router)
|
|
app.include_router(spec_creation_router)
|
|
app.include_router(expand_project_router)
|
|
app.include_router(filesystem_router)
|
|
app.include_router(assistant_chat_router)
|
|
app.include_router(settings_router)
|
|
app.include_router(terminal_router)
|
|
|
|
|
|
# ============================================================================
|
|
# WebSocket Endpoint
|
|
# ============================================================================
|
|
|
|
@app.websocket("/ws/projects/{project_name}")
|
|
async def websocket_endpoint(websocket: WebSocket, project_name: str):
|
|
"""WebSocket endpoint for real-time project updates."""
|
|
await project_websocket(websocket, project_name)
|
|
|
|
|
|
# ============================================================================
|
|
# Setup & Health Endpoints
|
|
# ============================================================================
|
|
|
|
@app.get("/api/health")
|
|
async def health_check():
|
|
"""Health check endpoint."""
|
|
return {"status": "healthy"}
|
|
|
|
|
|
@app.get("/api/setup/status", response_model=SetupStatus)
|
|
async def setup_status():
|
|
"""Check system setup status."""
|
|
# Check for Claude CLI
|
|
claude_cli = shutil.which("claude") is not None
|
|
|
|
# Check for CLI configuration directory
|
|
# Note: CLI no longer stores credentials in ~/.claude/.credentials.json
|
|
# The existence of ~/.claude indicates the CLI has been configured
|
|
claude_dir = Path.home() / ".claude"
|
|
has_claude_config = claude_dir.exists() and claude_dir.is_dir()
|
|
|
|
# If GLM mode is configured via .env, we have alternative credentials
|
|
glm_configured = bool(os.getenv("ANTHROPIC_BASE_URL") and os.getenv("ANTHROPIC_AUTH_TOKEN"))
|
|
credentials = has_claude_config or glm_configured
|
|
|
|
# Check for Node.js and npm
|
|
node = shutil.which("node") is not None
|
|
npm = shutil.which("npm") is not None
|
|
|
|
return SetupStatus(
|
|
claude_cli=claude_cli,
|
|
credentials=credentials,
|
|
node=node,
|
|
npm=npm,
|
|
)
|
|
|
|
|
|
# ============================================================================
|
|
# Static File Serving (Production)
|
|
# ============================================================================
|
|
|
|
# Serve React build files if they exist
|
|
if UI_DIST_DIR.exists():
|
|
# Mount static assets
|
|
app.mount("/assets", StaticFiles(directory=UI_DIST_DIR / "assets"), name="assets")
|
|
|
|
@app.get("/")
|
|
async def serve_index():
|
|
"""Serve the React app index.html."""
|
|
return FileResponse(UI_DIST_DIR / "index.html")
|
|
|
|
@app.get("/{path:path}")
|
|
async def serve_spa(path: str):
|
|
"""
|
|
Serve static files or fall back to index.html for SPA routing.
|
|
"""
|
|
# Check if the path is an API route (shouldn't hit this due to router ordering)
|
|
if path.startswith("api/") or path.startswith("ws/"):
|
|
raise HTTPException(status_code=404)
|
|
|
|
# Try to serve the file directly
|
|
file_path = (UI_DIST_DIR / path).resolve()
|
|
|
|
# Ensure resolved path is within UI_DIST_DIR (prevent path traversal)
|
|
try:
|
|
file_path.relative_to(UI_DIST_DIR.resolve())
|
|
except ValueError:
|
|
raise HTTPException(status_code=404)
|
|
|
|
if file_path.exists() and file_path.is_file():
|
|
return FileResponse(file_path)
|
|
|
|
# Fall back to index.html for SPA routing
|
|
return FileResponse(UI_DIST_DIR / "index.html")
|
|
|
|
|
|
# ============================================================================
|
|
# Main Entry Point
|
|
# ============================================================================
|
|
|
|
if __name__ == "__main__":
|
|
import uvicorn
|
|
uvicorn.run(
|
|
"server.main:app",
|
|
host="127.0.0.1", # Localhost only for security
|
|
port=8888,
|
|
reload=True,
|
|
)
|