mirror of
https://github.com/leonvanzyl/autocoder.git
synced 2026-01-30 06:12:06 +00:00
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:
@@ -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
|
||||||
|
|||||||
71
start_ui.py
71
start_ui.py
@@ -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)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user