feat: add --host argument for WebUI remote access (#81)

Users can now access the WebUI remotely (e.g., via VS Code tunnels,
remote servers) by specifying a host address:

    python start_ui.py --host 0.0.0.0
    python start_ui.py --host 0.0.0.0 --port 8888

Changes:
- Added --host and --port CLI arguments to start_ui.py
- Security warning displayed when remote access is enabled
- AUTOCODER_ALLOW_REMOTE env var passed to server
- server/main.py conditionally disables localhost middleware
- CORS updated to allow all origins when remote access is enabled
- Browser auto-open disabled for remote hosts

Security considerations documented in warning:
- File system access to project directories
- API can start/stop agents and modify files
- Recommend firewall or VPN for protection

Fixes: leonvanzyl/autocoder#81

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
cabana8471
2026-01-25 12:14:23 +01:00
parent 486979c3d9
commit be20c8a3ef
2 changed files with 90 additions and 41 deletions

View File

@@ -88,8 +88,21 @@ app = FastAPI(
lifespan=lifespan, lifespan=lifespan,
) )
# CORS - allow only localhost origins for security # Check if remote access is enabled via environment variable
app.add_middleware( # 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")
# 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, CORSMiddleware,
allow_origins=[ allow_origins=[
"http://localhost:5173", # Vite dev server "http://localhost:5173", # Vite dev server
@@ -100,16 +113,17 @@ app.add_middleware(
allow_credentials=True, allow_credentials=True,
allow_methods=["*"], allow_methods=["*"],
allow_headers=["*"], allow_headers=["*"],
) )
# ============================================================================ # ============================================================================
# Security Middleware # Security Middleware
# ============================================================================ # ============================================================================
@app.middleware("http") if not ALLOW_REMOTE:
async def require_localhost(request: Request, call_next): @app.middleware("http")
"""Only allow requests from localhost.""" 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 client_host = request.client.host if request.client else None
# Allow localhost connections # Allow localhost connections

View File

@@ -13,12 +13,16 @@ Automated launcher that handles all setup:
7. Opens browser to the UI 7. Opens browser to the UI
Usage: Usage:
python start_ui.py [--dev] python start_ui.py [--dev] [--host HOST] [--port PORT]
Options: Options:
--dev Run in development mode with Vite hot reload --dev Run in development mode with Vite hot reload
--host HOST Host to bind to (default: 127.0.0.1)
Use 0.0.0.0 for remote access (security warning will be shown)
--port PORT Port to bind to (default: 8888)
""" """
import argparse
import asyncio import asyncio
import os import os
import shutil import shutil
@@ -235,26 +239,31 @@ def build_frontend() -> bool:
return run_command([npm_cmd, "run", "build"], cwd=UI_DIR) return run_command([npm_cmd, "run", "build"], cwd=UI_DIR)
def start_dev_server(port: int) -> tuple: def start_dev_server(port: int, host: str = "127.0.0.1") -> tuple:
"""Start both Vite and FastAPI in development mode.""" """Start both Vite and FastAPI in development mode."""
venv_python = get_venv_python() venv_python = get_venv_python()
print("\n Starting development servers...") print("\n Starting development servers...")
print(f" - FastAPI backend: http://127.0.0.1:{port}") print(f" - FastAPI backend: http://{host}:{port}")
print(" - Vite frontend: http://127.0.0.1:5173") print(" - Vite frontend: http://127.0.0.1:5173")
# Set environment for remote access if needed
env = os.environ.copy()
if host != "127.0.0.1":
env["AUTOCODER_ALLOW_REMOTE"] = "1"
# Start FastAPI # Start FastAPI
backend = subprocess.Popen([ backend = subprocess.Popen([
str(venv_python), "-m", "uvicorn", str(venv_python), "-m", "uvicorn",
"server.main:app", "server.main:app",
"--host", "127.0.0.1", "--host", host,
"--port", str(port), "--port", str(port),
"--reload" "--reload"
], cwd=str(ROOT)) ], cwd=str(ROOT), env=env)
# Start Vite with API port env var for proxy configuration # Start Vite with API port env var for proxy configuration
npm_cmd = "npm.cmd" if sys.platform == "win32" else "npm" npm_cmd = "npm.cmd" if sys.platform == "win32" else "npm"
vite_env = os.environ.copy() vite_env = env.copy()
vite_env["VITE_API_PORT"] = str(port) vite_env["VITE_API_PORT"] = str(port)
frontend = subprocess.Popen([ frontend = subprocess.Popen([
npm_cmd, "run", "dev" npm_cmd, "run", "dev"
@@ -263,15 +272,18 @@ def start_dev_server(port: int) -> tuple:
return backend, frontend return backend, frontend
def start_production_server(port: int): def start_production_server(port: int, host: str = "127.0.0.1"):
"""Start FastAPI server in production mode with hot reload.""" """Start FastAPI server in production mode."""
venv_python = get_venv_python() venv_python = get_venv_python()
print(f"\n Starting server at http://127.0.0.1:{port} (with hot reload)") print(f"\n Starting server at http://{host}:{port}")
# Set PYTHONASYNCIODEBUG to help with Windows subprocess issues
env = os.environ.copy() env = os.environ.copy()
# Enable remote access in server if not localhost
if host != "127.0.0.1":
env["AUTOCODER_ALLOW_REMOTE"] = "1"
# NOTE: --reload is NOT used because on Windows it breaks asyncio subprocess # NOTE: --reload is NOT used because on Windows it breaks asyncio subprocess
# support (uvicorn's reload worker doesn't inherit the ProactorEventLoop policy). # support (uvicorn's reload worker doesn't inherit the ProactorEventLoop policy).
# This affects Claude SDK which uses asyncio.create_subprocess_exec. # This affects Claude SDK which uses asyncio.create_subprocess_exec.
@@ -279,14 +291,34 @@ def start_production_server(port: int):
return subprocess.Popen([ return subprocess.Popen([
str(venv_python), "-m", "uvicorn", str(venv_python), "-m", "uvicorn",
"server.main:app", "server.main:app",
"--host", "127.0.0.1", "--host", host,
"--port", str(port), "--port", str(port),
], cwd=str(ROOT), env=env) ], cwd=str(ROOT), env=env)
def main() -> None: def main() -> None:
"""Main entry point.""" """Main entry point."""
dev_mode = "--dev" in sys.argv parser = argparse.ArgumentParser(description="AutoCoder UI Launcher")
parser.add_argument("--dev", action="store_true", help="Run in development mode with Vite hot reload")
parser.add_argument("--host", default="127.0.0.1", help="Host to bind to (default: 127.0.0.1)")
parser.add_argument("--port", type=int, default=None, help="Port to bind to (default: auto-detect from 8888)")
args = parser.parse_args()
dev_mode = args.dev
host = args.host
# Security warning for remote access
if host != "127.0.0.1":
print("\n" + "!" * 50)
print(" SECURITY WARNING")
print("!" * 50)
print(f" Remote access enabled on host: {host}")
print(" The AutoCoder UI will be accessible from other machines.")
print(" Ensure you understand the security implications:")
print(" - The agent has file system access to project directories")
print(" - The API can start/stop agents and modify files")
print(" - Consider using a firewall or VPN for protection")
print("!" * 50 + "\n")
print("=" * 50) print("=" * 50)
print(" AutoCoder UI Setup") print(" AutoCoder UI Setup")
@@ -335,18 +367,20 @@ def main() -> None:
step = 5 if dev_mode else 6 step = 5 if dev_mode else 6
print_step(step, total_steps, "Starting server") print_step(step, total_steps, "Starting server")
port = find_available_port() port = args.port if args.port else find_available_port()
try: try:
if dev_mode: if dev_mode:
backend, frontend = start_dev_server(port) backend, frontend = start_dev_server(port, host)
# Open browser to Vite dev server # Open browser to Vite dev server (always localhost for Vite)
time.sleep(3) time.sleep(3)
webbrowser.open("http://127.0.0.1:5173") webbrowser.open("http://127.0.0.1:5173")
print("\n" + "=" * 50) print("\n" + "=" * 50)
print(" Development mode active") print(" Development mode active")
if host != "127.0.0.1":
print(f" Backend accessible at: http://{host}:{port}")
print(" Press Ctrl+C to stop") print(" Press Ctrl+C to stop")
print("=" * 50) print("=" * 50)
@@ -362,14 +396,15 @@ def main() -> None:
backend.wait() backend.wait()
frontend.wait() frontend.wait()
else: else:
server = start_production_server(port) server = start_production_server(port, host)
# Open browser # Open browser (only if localhost)
time.sleep(2) time.sleep(2)
if host == "127.0.0.1":
webbrowser.open(f"http://127.0.0.1:{port}") webbrowser.open(f"http://127.0.0.1:{port}")
print("\n" + "=" * 50) print("\n" + "=" * 50)
print(f" Server running at http://127.0.0.1:{port}") print(f" Server running at http://{host}:{port}")
print(" Press Ctrl+C to stop") print(" Press Ctrl+C to stop")
print("=" * 50) print("=" * 50)