diff --git a/.gitignore b/.gitignore index f54bea383..38dd95fe3 100644 --- a/.gitignore +++ b/.gitignore @@ -189,10 +189,7 @@ dist agent_logs.txt workspace/ tmp/ -data/file_store -data/workspace -data/logs -data/events.db +data/ output/ .vscode/ @@ -200,3 +197,5 @@ output/ # local only scripts start_tool_server.sh +docker/.stack.env.local +scripts/local/ diff --git a/README.md b/README.md index 5823a9f03..1c5305c5d 100644 --- a/README.md +++ b/README.md @@ -29,19 +29,157 @@ II-Agent Chat also feature within II-Agent that lets you work across multiple mo -## Key Features: -* Full-Stack Development: Complete web app scaffolding and iterative development. From initial setup to deployment, II-Agent handles the entire development lifecycle with intelligent code generation and optimization. -* Slide Creation: Transform short briefs into polished presentations. Create professional slides and decks with intelligent content structuring, design suggestions, and automated formatting. -* Deep Research: Comprehensive research capabilities through tight integration with II-Researcher. Conduct thorough investigations, analyze data, and generate detailed reports with our specialized research agent. +## Key Features + +* **Full-Stack Development**: Complete web app scaffolding and iterative development. From initial setup to deployment, II-Agent handles the entire development lifecycle with intelligent code generation and optimization. +* **Slide Creation**: Transform short briefs into polished presentations. Create professional slides and decks with intelligent content structuring, design suggestions, and automated formatting. +* **Deep Research**: Comprehensive research capabilities through tight integration with II-Researcher. Conduct thorough investigations, analyze data, and generate detailed reports with our specialized research agent. +* **Local Docker Sandbox**: Run sandboxes entirely on your own machine with Docker — no cloud dependencies, no data leaving your network. Ideal for air-gapped, NDA-protected, or self-hosted environments. +* **Agent-Human Handoff**: When the agent encounters CAPTCHAs, login forms, or 2FA, it opens a browser-based noVNC session so you can interact directly, then resumes automation once you're done. +* **Media Generation**: Built-in DALL-E 3 image generation and Sora video generation with cost tracking and multiple aspect ratio support. +* **Extended Thinking**: Claude 4 extended thinking with configurable token budgets, interleaved thinking during tool use, and optional 1M context window. +* **Tool Execution Safety**: Tiered timeout system (120s tool-level, 300s MCP backstop) with 2-second interrupt polling — prevents indefinitely hung sessions and supports mid-execution cancellation. ## SWE-Bench Pro swepro +## Deployment Models + +II-Agent supports two sandbox deployment modes through a pluggable provider architecture: + +| | Cloud (E2B) | Local (Docker) | +|---|---|---| +| **Isolation** | Firecracker micro-VMs | Docker containers | +| **Network** | Public (ngrok tunnel) | Localhost only | +| **Startup** | ~150ms (pre-warmed) | 2–5s (cold start) | +| **Data location** | E2B infrastructure | Your machine | +| **Cost** | Per-use billing | Free (your hardware) | +| **Best for** | Production, quick start | Privacy, air-gapped, self-hosted | + +Set `SANDBOX_PROVIDER=docker` or `SANDBOX_PROVIDER=e2b` in your environment to choose. + ## Installation For the latest installation and deployment instructions, please refer to our [official guide](https://intelligent-internet.github.io/ii-agent-prod/) [![Installation Guide](https://img.youtube.com/vi/wPpeJMbdGi4/maxresdefault.jpg)](https://www.youtube.com/watch?v=wPpeJMbdGi4) + +### Local Docker Quick Start + +Run II-Agent entirely on your own machine with no cloud dependencies: + +```bash +# 1. Build the sandbox image (Python, Node.js, Playwright, noVNC, code-server) +docker build -t ii-agent-sandbox:latest -f e2b.Dockerfile . + +# 2. Configure environment +cp docker/.stack.env.local.example docker/.stack.env.local +# Edit docker/.stack.env.local — set JWT_SECRET_KEY and at least one LLM API key + +# 3. Start the stack +scripts/stack_control.sh start --local + +# 4. Access +# Frontend: http://localhost:1420 +# Backend API: http://localhost:8000 +# Sandbox Server: http://localhost:8100 +``` + +For detailed setup, see [docs/docs/local-docker-sandbox.md](docs/docs/local-docker-sandbox.md). + +### Architecture (Local Mode) + +``` +┌─────────────┐ +│ Frontend │ +│ (:1420) │ +└──────┬───────┘ + │ WebSocket + ▼ +┌─────────────┐ ┌───────────┐ ┌───────────┐ +│ Backend │◄───►│ Redis │ │ Postgres │ +│ (:8000) │ │ (:6379) │ │ (:5433) │ +└──────┬───────┘ └───────────┘ └───────────┘ + │ + ┌────┴─────┐ + ▼ ▼ +┌──────────┐ ┌─────────────┐ +│ Sandbox │ │ Tool Server │ +│ Server │ │ (:1236) │ +│ (:8100) │ └─────────────┘ +└────┬─────┘ + │ Docker API + ▼ +┌──────────────────────────────────┐ +│ Sandbox Containers │ +│ ┌──────────┐ ┌──────────┐ │ +│ │ Sandbox1 │ │ Sandbox2 │ ... │ +│ │ Playwright│ │ noVNC │ │ +│ │ noVNC │ │ code-svr │ │ +│ └──────────┘ └──────────┘ │ +└──────────────────────────────────┘ +``` + +Each sandbox container gets 6 host ports allocated from a configurable pool (default 30000–30999), exposing noVNC (6080), code-server (9000), MCP (6060), and dev-server ports. + + +## Stack Management + +The unified `scripts/stack_control.sh` script manages the full Docker stack: + +```bash +scripts/stack_control.sh start [--local] # Start services +scripts/stack_control.sh stop [--local] # Stop services +scripts/stack_control.sh restart [--local] [service] # Restart (picks up env changes) +scripts/stack_control.sh rebuild [--local] [service] # Rebuild image + restart +scripts/stack_control.sh status [--local] # Show service status and URLs +scripts/stack_control.sh logs [--local] [service] [-f]# View/follow logs +scripts/stack_control.sh build [--local] # Build sandbox image +scripts/stack_control.sh setup [--local] # Create .stack.env from template +scripts/stack_control.sh wake [--local] [uuid] # Wake stopped sandbox containers +``` + +The `--local` flag switches between cloud (E2B) and local (Docker) compose configurations. + + +## Agent-Human-Agent Handoff + +When the agent's browser encounters a CAPTCHA, login form, or 2FA challenge: + +1. The agent calls `expose_port(6080)` to obtain a noVNC URL +2. The agent shares the URL and pauses +3. You open the noVNC link in your browser and interact with the sandbox's Chromium directly +4. You confirm completion via the chat UI +5. The agent resumes automation from a fresh screenshot + +This works because both Playwright (agent) and noVNC (you) share the same X11 display (`:99`) inside the sandbox via `x11vnc --shared`. + + +## Configuration + +### Key Environment Variables + +| Variable | Default | Description | +|---|---|---| +| `SANDBOX_PROVIDER` | `e2b` | Sandbox backend: `e2b` or `docker` | +| `SANDBOX_DOCKER_IMAGE` | `ii-agent-sandbox:latest` | Docker image for local sandboxes | +| `SANDBOX_PORT_RANGE_START` | `30000` | Start of host port pool | +| `SANDBOX_PORT_RANGE_END` | `30999` | End of host port pool | +| `LOCAL_MODE` | `false` | Enable local-only features | +| `STORAGE_PROVIDER` | `gcs` | Storage backend: `gcs` or `local` | +| `ORPHAN_CLEANUP_ENABLED` | `true` | Auto-remove sandboxes with no active sessions | +| `ORPHAN_CLEANUP_INTERVAL_SECONDS` | `300` | Cleanup check interval | + +See `docker/.stack.env.local.example` for the full list of configurable variables. + + +## Utility Scripts + +| Script | Description | +|---|---| +| `scripts/stack_control.sh` | Unified Docker stack lifecycle management | +| `scripts/admin_credits.sh` | Query and manage user credits in PostgreSQL | +| `scripts/html_to_pdf.py` | Convert HTML slides/pages to multi-page PDF via Playwright | diff --git a/docker/.stack.env.local.example b/docker/.stack.env.local.example new file mode 100644 index 000000000..4c1acc76a --- /dev/null +++ b/docker/.stack.env.local.example @@ -0,0 +1,154 @@ +# ============================================================================ +# ii-agent Local-Only Environment Configuration +# ============================================================================ +# This configuration is for running ii-agent with LOCAL Docker sandboxes +# instead of E2B cloud. All data stays on your machine - suitable for +# privileged/NDA-protected data. +# +# Copy this file to .stack.env.local and configure the required values. +# ============================================================================ + +# ============================================================================ +# SANDBOX PROVIDER (NEW - Docker instead of E2B) +# ============================================================================ +# Use "docker" for local sandboxes or "e2b" for E2B cloud +SANDBOX_PROVIDER=docker + +# Docker image to use for local sandboxes (build with: docker build -t ii-agent-sandbox:latest -f e2b.Dockerfile .) +SANDBOX_DOCKER_IMAGE=ii-agent-sandbox:latest + +# Optional: Docker network for sandboxes to join (useful if MCP server is in a container) +# SANDBOX_DOCKER_NETWORK=ii-agent-network + +# ============================================================================ +# DATABASE CONFIGURATION +# ============================================================================ +# Use a different port if native PostgreSQL is running on 5432 +POSTGRES_PORT=5433 +POSTGRES_USER=iiagent +POSTGRES_PASSWORD=iiagent +POSTGRES_DB=iiagentdev + +# Database URLs for services (using internal docker hostname) +DATABASE_URL=postgresql://iiagent:iiagent@postgres:5432/iiagentdev + +# Sandbox server database +SANDBOX_DB_NAME=ii_sandbox +SANDBOX_DATABASE_URL=postgresql+asyncpg://iiagent:iiagent@postgres:5432/ii_sandbox + +# ============================================================================ +# REDIS CONFIGURATION +# ============================================================================ +REDIS_PORT=6379 +REDIS_URL=redis://redis:6379/0 +REDIS_SESSION_URL=redis://redis:6379/1 + +# ============================================================================ +# SERVICE PORTS +# ============================================================================ +FRONTEND_PORT=1420 +BACKEND_PORT=8000 +TOOL_SERVER_PORT=1236 +SANDBOX_SERVER_PORT=8100 + +# Port for MCP server inside sandboxes +MCP_PORT=6060 + +# ============================================================================ +# FRONTEND CONFIGURATION +# ============================================================================ +FRONTEND_BUILD_MODE=production + +# API URL that the frontend uses to reach the backend. +# IMPORTANT: For mobile/remote device access, use your machine's IP address +# (e.g., http://192.168.x.x:8000) instead of localhost. +# This MUST match LOCAL_STORAGE_URL_BASE below (same host) for file uploads to work. +VITE_API_URL=http://localhost:8000 + +# Disable Google OAuth for local setup (optional - set to enable) +VITE_GOOGLE_CLIENT_ID= + +# Disable Stripe for local setup +VITE_STRIPE_PUBLISHABLE_KEY= + +# Disable Sentry for local setup +VITE_SENTRY_DSN= + +# ============================================================================ +# AUTHENTICATION (Required) +# ============================================================================ +# Generate with: openssl rand -hex 32 +JWT_SECRET_KEY=CHANGE_ME_USE_openssl_rand_hex_32 + +# For local-only mode, you can use the demo user +# Enable demo mode to skip OAuth +DEMO_MODE=true + +# ============================================================================ +# LLM PROVIDER API KEYS (At least one required) +# ============================================================================ +# OpenAI +OPENAI_API_KEY= + +# Anthropic Claude +ANTHROPIC_API_KEY= + +# Google Gemini +GEMINI_API_KEY= + +# Groq +GROQ_API_KEY= + +# Fireworks +FIREWORKS_API_KEY= + +# OpenRouter (access to multiple models) +OPENROUTER_API_KEY= + +# ============================================================================ +# MCP SERVER CONFIGURATION (Optional - for your local MCP server) +# ============================================================================ +# If you have a local MCP server running, configure it here +# This URL is accessible from within sandbox containers + +# For MCP server running on host machine: +# MCP_SERVER_URL=http://host.docker.internal:6060 + +# For MCP server running in a Docker container on the same network: +# MCP_SERVER_URL=http://mcp-server:6060 + +# ============================================================================ +# OPTIONAL SERVICES +# ============================================================================ +# These are not required for local-only mode + +# ============================================================================ +# LOCAL FILE STORAGE (for uploads and assets) +# ============================================================================ +# URL base for serving uploaded files to browsers. +# IMPORTANT: This MUST use the same host as VITE_API_URL above. +# - Use localhost for local-only access +# - Use your machine's IP (e.g., http://192.168.x.x:8000/files) for mobile/remote access +# If mismatched, file uploads will fail on mobile devices because the browser +# tries to upload to a URL it can't reach. +LOCAL_STORAGE_URL_BASE=http://localhost:8000/files + +# Internal URL for container-to-container file access (usually doesn't need changing) +LOCAL_STORAGE_INTERNAL_URL_BASE=http://backend:8000/files + +# Image search (Serper) +# SERPER_API_KEY= + +# Web search (Tavily) +# TAVILY_API_KEY= + +# Cloud storage (not needed for local mode) +# GCS_BUCKET_NAME= +# GOOGLE_APPLICATION_CREDENTIALS= + +# ============================================================================ +# E2B CONFIGURATION (NOT NEEDED for local Docker mode) +# ============================================================================ +# Leave these empty when using SANDBOX_PROVIDER=docker +# E2B_API_KEY= +# NGROK_AUTHTOKEN= diff --git a/docker/backend/Dockerfile b/docker/backend/Dockerfile index 62bdd33d1..3058adf37 100644 --- a/docker/backend/Dockerfile +++ b/docker/backend/Dockerfile @@ -30,7 +30,7 @@ RUN fc-cache -fv RUN --mount=type=cache,target=/root/.cache/uv \ --mount=type=bind,source=uv.lock,target=uv.lock \ --mount=type=bind,source=pyproject.toml,target=pyproject.toml \ - uv sync --locked --no-install-project --no-dev + uv sync --locked --prerelease=allow --no-install-project --no-dev # Install Playwright in a single layer RUN uv run playwright install --with-deps chromium @@ -39,7 +39,7 @@ RUN uv run playwright install --with-deps chromium # Installing separately from its dependencies allows optimal layer caching COPY . /app RUN --mount=type=cache,target=/root/.cache/uv \ - uv sync --locked --no-dev + uv sync --locked --prerelease=allow --no-dev RUN chmod +x /app/start.sh RUN chmod +x /app/scripts/run_sandbox_timeout_extension.sh diff --git a/docker/docker-compose.local-only.yaml b/docker/docker-compose.local-only.yaml new file mode 100644 index 000000000..5543f4e37 --- /dev/null +++ b/docker/docker-compose.local-only.yaml @@ -0,0 +1,194 @@ +# Local-only docker-compose for ii-agent WITHOUT E2B cloud/ngrok +# This setup uses local Docker containers for sandboxes instead of E2B. +# +# Usage: +# 1. Build the sandbox image first: +# docker build -t ii-agent-sandbox:latest -f e2b.Dockerfile . +# +# 2. Copy and configure environment: +# cp docker/.stack.env.local.example docker/.stack.env.local +# +# 3. Start the stack: +# docker compose -f docker/docker-compose.local-only.yaml --env-file docker/.stack.env.local up -d +# +# This configuration: +# - Uses Docker provider instead of E2B (all data stays local) +# - No ngrok tunnel (no public exposure) +# - Suitable for privileged/NDA-protected data +# - Works in air-gapped environments + +services: + postgres: + image: postgres:15 + restart: unless-stopped + ports: + - "${POSTGRES_PORT:-5432}:5432" + environment: + POSTGRES_USER: ${POSTGRES_USER:-iiagent} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-iiagent} + POSTGRES_DB: ${POSTGRES_DB:-iiagentdev} + SANDBOX_DB_NAME: ${SANDBOX_DB_NAME:-ii_sandbox} + env_file: + - .stack.env.local + volumes: + - postgres-data-local:/var/lib/postgresql/data + - ./postgres-init/create-databases.sh:/docker-entrypoint-initdb.d/create-databases.sh:ro + healthcheck: + test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-iiagent} -d ${POSTGRES_DB:-iiagentdev}"] + interval: 10s + timeout: 5s + retries: 5 + + redis: + image: redis:7-alpine + restart: unless-stopped + ports: + - "${REDIS_PORT:-6379}:6379" + command: ["redis-server", "--save", "60", "1", "--loglevel", "warning"] + volumes: + - redis-data-local:/data + healthcheck: + test: ["CMD", "redis-cli", "ping"] + interval: 10s + timeout: 5s + retries: 5 + + frontend: + build: + context: .. + dockerfile: docker/frontend/Dockerfile + args: + BUILD_MODE: ${FRONTEND_BUILD_MODE:-production} + VITE_API_URL: ${VITE_API_URL:-http://localhost:8000} + VITE_GOOGLE_CLIENT_ID: ${VITE_GOOGLE_CLIENT_ID:-} + VITE_STRIPE_PUBLISHABLE_KEY: ${VITE_STRIPE_PUBLISHABLE_KEY:-} + VITE_SENTRY_DSN: ${VITE_SENTRY_DSN:-} + VITE_DISABLE_CHAT_MODE: ${VITE_DISABLE_CHAT_MODE:-false} + restart: unless-stopped + env_file: + - .stack.env.local + environment: + NODE_ENV: production + ports: + - "${FRONTEND_PORT:-1420}:1420" + + tool-server: + build: + context: .. + dockerfile: docker/backend/Dockerfile + restart: unless-stopped + depends_on: + postgres: + condition: service_healthy + env_file: + - .stack.env.local + environment: + DATABASE_URL: ${DATABASE_URL} + entrypoint: ["/bin/sh", "-c"] + command: + - >- + exec uvicorn ii_tool.integrations.app.main:app + --host 0.0.0.0 + --port 1236 + ports: + - "${TOOL_SERVER_PORT:-1236}:1236" + volumes: + - ii-agent-filestore-local:/.ii_agent + healthcheck: + test: ["CMD-SHELL", "curl -fsS http://localhost:1236/health || exit 1"] + interval: 15s + timeout: 5s + retries: 5 + + sandbox-server: + build: + context: .. + dockerfile: docker/backend/Dockerfile + restart: unless-stopped + extra_hosts: + - "host.docker.internal:host-gateway" + depends_on: + postgres: + condition: service_healthy + redis: + condition: service_healthy + env_file: + - .stack.env.local + environment: + SANDBOX_DATABASE_URL: ${SANDBOX_DATABASE_URL} + SERVER_HOST: 0.0.0.0 + SERVER_PORT: ${SANDBOX_SERVER_PORT:-8100} + REDIS_URL: redis://redis:6379/0 + MCP_PORT: ${MCP_PORT:-6060} + # Use Docker provider instead of E2B + PROVIDER: docker + PROVIDER_TYPE: docker + SANDBOX_DOCKER_IMAGE: ${SANDBOX_DOCKER_IMAGE:-ii-agent-sandbox:latest} + # Network for sandbox containers - must match the compose project network + DOCKER_NETWORK: ${COMPOSE_PROJECT_NAME:-ii-agent-local}_default + # Enable local mode features (orphan cleanup, etc.) + LOCAL_MODE: "true" + ORPHAN_CLEANUP_ENABLED: "true" + ORPHAN_CLEANUP_INTERVAL_SECONDS: "300" + # Backend URL for session verification during orphan cleanup + BACKEND_URL: "http://backend:8000" + entrypoint: ["/bin/bash", "/app/start_sandbox_server.sh"] + ports: + - "${SANDBOX_SERVER_PORT:-8100}:8100" + # Mount Docker socket so sandbox-server can create containers + volumes: + - /var/run/docker.sock:/var/run/docker.sock + - sandbox-workspaces:/tmp/ii-agent-sandboxes + healthcheck: + test: ["CMD-SHELL", "curl -fsS http://localhost:8100/health || exit 1"] + interval: 15s + timeout: 5s + retries: 5 + + backend: + build: + context: .. + dockerfile: docker/backend/Dockerfile + init: true + restart: unless-stopped + extra_hosts: + - "host.docker.internal:host-gateway" + depends_on: + postgres: + condition: service_healthy + redis: + condition: service_healthy + sandbox-server: + condition: service_started + tool-server: + condition: service_started + env_file: + - .stack.env.local + environment: + DATABASE_URL: ${DATABASE_URL} + SANDBOX_SERVER_URL: http://sandbox-server:${SANDBOX_SERVER_PORT:-8100} + # Tool server URL for backend-to-tool-server (Docker network) + TOOL_SERVER_URL: http://tool-server:1236 + # Tool server URL for sandbox-to-tool-server (via host) + SANDBOX_TOOL_SERVER_URL: ${SANDBOX_TOOL_SERVER_URL:-http://host.docker.internal:1236} + REDIS_SESSION_URL: redis://redis:6379/1 + # Use local filesystem storage instead of GCS + STORAGE_PROVIDER: local + LOCAL_STORAGE_PATH: /.ii_agent/storage + # Enable dev authentication (bypasses OAuth) + DEV_AUTH_ENABLED: "true" + ports: + - "${BACKEND_PORT:-8000}:8000" + volumes: + - ii-agent-filestore-local:/.ii_agent + healthcheck: + test: ["CMD-SHELL", "curl -fsS http://localhost:8000/health || exit 1"] + interval: 15s + timeout: 5s + retries: 5 + +volumes: + postgres-data-local: + redis-data-local: + ii-agent-filestore-local: + sandbox-workspaces: diff --git a/docker/docker-compose.local.yaml b/docker/docker-compose.local.yaml new file mode 100644 index 000000000..0c144d412 --- /dev/null +++ b/docker/docker-compose.local.yaml @@ -0,0 +1,10 @@ +# Override file to disable ngrok for local-only development +# Usage: docker compose -f docker-compose.stack.yaml -f docker-compose.local.yaml up -d + +services: + ngrok: + # Disable ngrok by setting an invalid entrypoint that exits immediately + entrypoint: ["/bin/sh", "-c", "echo 'ngrok disabled for local development' && exit 0"] + restart: "no" + profiles: + - disabled diff --git a/docker/docker-compose.stack.yaml b/docker/docker-compose.stack.yaml index 9e641bb23..7829b9dd4 100644 --- a/docker/docker-compose.stack.yaml +++ b/docker/docker-compose.stack.yaml @@ -106,6 +106,9 @@ services: SERVER_PORT: ${SANDBOX_SERVER_PORT:-8100} REDIS_URL: redis://redis:6379/0 MCP_PORT: ${MCP_PORT:-6060} + DOCKER_NETWORK: docker_default + volumes: + - /var/run/docker.sock:/var/run/docker.sock entrypoint: ["/bin/bash", "/app/start_sandbox_server.sh"] ports: - "${SANDBOX_SERVER_PORT:-8100}:8100" @@ -136,7 +139,8 @@ services: GOOGLE_APPLICATION_CREDENTIALS: /app/google-application-credentials.json DATABASE_URL: ${DATABASE_URL} SANDBOX_SERVER_URL: http://sandbox-server:${SANDBOX_SERVER_PORT:-8100} - TOOL_SERVER_URL: ${PUBLIC_TOOL_SERVER_URL} + # Internal URL for sandbox containers to reach tool-server (container-to-container) + TOOL_SERVER_URL: http://tool-server:${TOOL_SERVER_PORT:-1236} REDIS_SESSION_URL: redis://redis:6379/1 ports: - "${BACKEND_PORT:-8000}:8000" diff --git a/docker/sandbox/start-services.sh b/docker/sandbox/start-services.sh index 75002cbb7..a31ece863 100644 --- a/docker/sandbox/start-services.sh +++ b/docker/sandbox/start-services.sh @@ -1,8 +1,10 @@ #!/bin/bash -# If running as root, use gosu to re-execute as pn user +# If running as root, fix workspace permissions and switch to pn user if [ "$(id -u)" = "0" ]; then - echo "Running as root, switching to pn user with gosu..." + echo "Running as root, fixing workspace permissions and switching to pn user..." + # Ensure /workspace is owned by pn user before switching + chown -R pn:pn /workspace 2>/dev/null || true exec gosu pn bash "$0" "$@" fi @@ -15,9 +17,23 @@ export PATH="/home/pn/.bun/bin:/app/ii_agent/.venv/bin:$PATH" mkdir -p /workspace cd /workspace +# Start Xvfb with a known display number so x11vnc can connect to it +echo "Starting Xvfb virtual display..." +Xvfb :99 -screen 0 1280x720x24 & +sleep 1 +export DISPLAY=:99 + # Start the sandbox server in the background echo "Starting sandbox server..." -tmux new-session -d -s sandbox-server-system-never-kill -c /workspace 'WORKSPACE_DIR=/workspace xvfb-run python -m ii_tool.mcp.server' +tmux new-session -d -s sandbox-server-system-never-kill -c /workspace "DISPLAY=:99 WORKSPACE_DIR=/workspace python -m ii_tool.mcp.server" + +# Start x11vnc (VNC server connected to the virtual display) +echo "Starting x11vnc on port 5900..." +tmux new-session -d -s vnc-server-system-never-kill 'x11vnc -display :99 -forever -nopw -listen 0.0.0.0 -rfbport 5900 -shared' + +# Start noVNC web proxy (allows browser-based VNC access) +echo "Starting noVNC on port 6080..." +tmux new-session -d -s novnc-server-system-never-kill 'websockify --web /usr/share/novnc 6080 localhost:5900' # Start code-server in the background echo "Starting code-server on port 9000..." @@ -48,9 +64,23 @@ else echo "✗ Code-server failed to start" fi +if pgrep -f "x11vnc" >/dev/null; then + echo "✓ x11vnc is running on port 5900" +else + echo "✗ x11vnc failed to start" +fi + +if pgrep -f "websockify" >/dev/null; then + echo "✓ noVNC is running on port 6080" +else + echo "✗ noVNC failed to start" +fi + echo "Services started. Container ready." echo "Sandbox server available" echo "Code-server available on port 9000" +echo "noVNC viewer available on port 6080" -# Keep the container running by waiting for all background processes -wait +# Keep the container running by tailing the tmux sessions +# This prevents the container from exiting while services run in tmux +exec tail -f /dev/null diff --git a/docs/docs/architecture-local-to-cloud.md b/docs/docs/architecture-local-to-cloud.md new file mode 100644 index 000000000..178dd3ae0 --- /dev/null +++ b/docs/docs/architecture-local-to-cloud.md @@ -0,0 +1,532 @@ +# Architecture: Local to Cloud Deployment Path + +This document outlines the architectural evolution of ii-agent from a local development setup to a production-ready cloud deployment, with emphasis on security considerations for sensitive/NDA-protected data. + +## Overview + +ii-agent supports multiple deployment models through a pluggable sandbox provider architecture: + +| Stage | Sandbox Provider | Network Exposure | Data Location | Multi-tenant | +|-------|------------------|------------------|---------------|--------------| +| **Local Dev** | Docker | localhost only | Your machine | No | +| **Team/On-prem** | Docker + Auth | Internal network | Your infrastructure | Limited | +| **Cloud Production** | Kubernetes/gVisor | Internet-facing | Cloud VPC | Yes | + +--- + +## Stage 1: Local Development (Current) + +### Architecture + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ Single Developer Machine │ +├─────────────────────────────────────────────────────────────────┤ +│ │ +│ Browser ──▶ Frontend (:1420) │ +│ │ │ +│ ▼ Socket.IO (WebSocket) │ +│ Backend (:8000) ◀──▶ Redis (session mgr) │ +│ │ │ +│ ┌────────┴────────┐ │ +│ ▼ ▼ │ +│ Sandbox-Server Tool-Server │ +│ (:8100) (:1236) │ +│ │ │ +│ │ Docker API + PortPoolManager │ +│ ▼ (host ports 30000-30999) │ +│ ┌─────────────────────────────────────────┐ │ +│ │ Ephemeral Sandbox Containers │ │ +│ │ ┌─────────────────────────────────┐ │ │ +│ │ │ Sandbox │ │ │ +│ │ │ Xvfb (:99) + x11vnc (:5900) │ │ │ +│ │ │ noVNC (:6080) │ │ │ +│ │ │ MCP Server (:6060) │ │ │ +│ │ │ code-server (:9000) │ │ │ +│ │ └─────────────────────────────────┘ │ │ +│ │ ┌─────────┐ ┌─────────┐ │ │ +│ │ │Sandbox 2│ │ ... │ │ │ +│ │ └─────────┘ └─────────┘ │ │ +│ └─────────────────────────────────────────┘ │ +│ │ +│ ┌──────────┐ ┌───────┐ │ +│ │ Postgres │ │ Redis │ │ +│ │ (:5433) │ │(:6379)│ │ +│ └──────────┘ └───────┘ │ +│ │ +└─────────────────────────────────────────────────────────────────┘ +``` + +### Security Model + +| Aspect | Implementation | Risk Level | +|--------|----------------|------------| +| Network exposure | localhost only | ✅ Low | +| Authentication | JWT (optional demo mode) | ⚠️ Acceptable for dev | +| Sandbox isolation | Docker containers | ⚠️ Process-level | +| Data at rest | Local filesystem | ✅ Your control | +| Secrets | Environment variables | ⚠️ Acceptable for dev | + +### What Works Now + +- ✅ Full agent functionality without E2B/ngrok +- ✅ Local MCP server connectivity +- ✅ File operations with path traversal protection +- ✅ Command execution in isolated containers +- ✅ Resource limits (memory, CPU, PIDs) +- ✅ Basic capability dropping +- ✅ **Orphan cleanup** — Automatic removal of sandboxes with no active session (5-minute grace period, runs every 300s) +- ✅ **Local storage** — Files stored locally instead of cloud storage (GCS) +- ✅ **Port pool management** — Dynamic host-port allocation (default 30000–30999, configurable via `SANDBOX_PORT_RANGE_START`/`SANDBOX_PORT_RANGE_END`). Thread-safe with startup scanning to reclaim ports from existing containers. +- ✅ **noVNC browser handoff** — User interaction for CAPTCHAs/login via browser-based VNC viewer (noVNC :6080 → x11vnc :5900 → Xvfb :99 inside sandbox) +- ✅ **Socket.IO real-time transport** — Backend ↔ Browser communication over WebSocket with Redis-backed session manager (`AsyncRedisManager`) for horizontal scaling. Configured with `ping_timeout=300s`, `ping_interval=30s`, 10 MB max buffer. +- ✅ **Conversation state resilience** — Defense-in-depth sanitization of LLM thinking blocks on restore, runtime, save, and API call boundaries to prevent stuck sessions from corrupted state. + +### Known Limitations + +- Docker socket mount gives sandbox-server root-equivalent host access +- No network policy between sandbox containers +- No audit logging +- Single-user only + +### Quick Start + +```bash +# Configure +cp docker/.stack.env.local.example docker/.stack.env.local +# Edit: add JWT_SECRET_KEY and LLM API key + +# Build sandbox image + start all services +scripts/stack_control.sh --local build +scripts/stack_control.sh --local start + +# Or equivalently, rebuild a single service: +scripts/stack_control.sh --local rebuild backend +``` + +> `scripts/stack_control.sh` is the preferred interface. It wraps `docker compose` with the correct env-file, compose files, and build context. Run it without arguments to see the full command reference. + +--- + +## Stage 2: Team/On-Premises Deployment + +### Architecture Changes + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ Internal Network / VPN │ +├─────────────────────────────────────────────────────────────────┤ +│ │ +│ ┌──────────────────────────────────────┐ │ +│ │ Reverse Proxy (nginx) │ │ +│ │ - TLS termination │ │ +│ │ - Rate limiting │ │ +│ │ - IP allowlisting │ │ +│ └─────────────────┬────────────────────┘ │ +│ │ │ +│ ┌───────────┴───────────┐ │ +│ ▼ ▼ │ +│ ┌──────────┐ ┌──────────┐ │ +│ │ Frontend │ │ Backend │ │ +│ └──────────┘ └────┬─────┘ │ +│ │ │ +│ ┌──────────┴──────────┐ │ +│ ▼ ▼ │ +│ Sandbox-Server Tool-Server │ +│ (+ mTLS auth) (+ mTLS auth) │ +│ │ │ +│ ▼ │ +│ ┌─────────────────────────────────────────┐ │ +│ │ Sandboxes (isolated Docker network) │ │ +│ │ - No inter-container communication │ │ +│ │ - Egress restricted to MCP only │ │ +│ └─────────────────────────────────────────┘ │ +│ │ +│ ┌──────────┐ ┌───────┐ ┌────────────────┐ │ +│ │ Postgres │ │ Redis │ │ MCP Server │ │ +│ │ (TLS) │ │ (TLS) │ │ (internal only)│ │ +│ └──────────┘ └───────┘ └────────────────┘ │ +│ │ +└─────────────────────────────────────────────────────────────────┘ +``` + +### Required Changes + +#### 1. Add Service-to-Service Authentication + +```yaml +# docker-compose.team.yaml additions +services: + sandbox-server: + environment: + # Require mTLS or JWT for API calls + REQUIRE_AUTH: "true" + AUTH_JWT_SECRET: ${SANDBOX_AUTH_SECRET} +``` + +#### 2. Create Isolated Docker Network + +```yaml +networks: + sandbox-net: + driver: bridge + internal: true # No external access + driver_opts: + com.docker.network.bridge.enable_icc: "false" # No inter-container +``` + +#### 3. Add Reverse Proxy with TLS + +```nginx +# nginx.conf +upstream backend { + server backend:8000; +} + +server { + listen 443 ssl; + ssl_certificate /etc/ssl/certs/ii-agent.crt; + ssl_certificate_key /etc/ssl/private/ii-agent.key; + + # Rate limiting + limit_req_zone $binary_remote_addr zone=api:10m rate=10r/s; + + location /api/ { + limit_req zone=api burst=20; + proxy_pass http://backend; + } +} +``` + +#### 4. Implement Audit Logging + +```python +# Add to sandbox-server +import structlog + +logger = structlog.get_logger() + +async def create_sandbox(..., user_id: str): + logger.info( + "sandbox_created", + user_id=user_id, + sandbox_id=sandbox_id, + action="create" + ) +``` + +### Security Improvements + +| Aspect | Change | Risk Reduction | +|--------|--------|----------------| +| Network | TLS everywhere, mTLS for services | High | +| Authentication | OIDC/SAML integration | High | +| Network isolation | Isolated Docker network | Medium | +| Audit | Structured logging to SIEM | Medium | +| Rate limiting | Nginx/HAProxy rate limits | Medium | + +--- + +## Stage 3: Cloud Production (AWS/GCP/Azure) + +### Target Architecture + +``` +┌─────────────────────────────────────────────────────────────────────────┐ +│ AWS VPC │ +├─────────────────────────────────────────────────────────────────────────┤ +│ │ +│ ┌─────────────────────────────────────────────────────────────────┐ │ +│ │ Public Subnet │ │ +│ │ ┌─────────────┐ │ │ +│ │ │ ALB │◀── WAF + Shield │ │ +│ │ │ (HTTPS) │ │ │ +│ │ └──────┬──────┘ │ │ +│ └──────────┼──────────────────────────────────────────────────────┘ │ +│ │ │ +│ ┌──────────┼──────────────────────────────────────────────────────┐ │ +│ │ │ Private Subnet (EKS) │ │ +│ │ ▼ │ │ +│ │ ┌─────────────────────────────────────────────────────────┐ │ │ +│ │ │ EKS Cluster │ │ │ +│ │ │ │ │ │ +│ │ │ ┌──────────┐ ┌──────────────┐ ┌──────────────┐ │ │ │ +│ │ │ │ Frontend │ │ Backend │ │ Tool-Server │ │ │ │ +│ │ │ │ (Pod) │ │ (Pod) │ │ (Pod) │ │ │ │ +│ │ │ └──────────┘ └──────┬───────┘ └──────────────┘ │ │ │ +│ │ │ │ │ │ │ +│ │ │ ▼ │ │ │ +│ │ │ ┌─────────────────┐ │ │ │ +│ │ │ │ Sandbox-Server │ │ │ │ +│ │ │ │ (Pod + IAM Role)│ │ │ │ +│ │ │ └────────┬────────┘ │ │ │ +│ │ │ │ │ │ │ +│ │ │ ┌───────────────────┴───────────────────┐ │ │ │ +│ │ │ │ Sandbox Namespace │ │ │ │ +│ │ │ │ ┌─────────┐ ┌─────────┐ │ │ │ │ +│ │ │ │ │Sandbox 1│ │Sandbox 2│ ... │◀─┐ │ │ │ +│ │ │ │ │ (gVisor)│ │ (gVisor)│ │ │ │ │ │ +│ │ │ │ └─────────┘ └─────────┘ │ │ │ │ │ +│ │ │ │ │ │ │ │ │ +│ │ │ │ NetworkPolicy: deny-all + allow-mcp │ │ │ │ │ +│ │ │ └────────────────────────────────────────┘ │ │ │ │ +│ │ │ │ │ │ │ +│ │ └───────────────────────────────────────────────┼─────────┘ │ │ +│ │ │ │ │ +│ │ ┌────────────────┐ ┌────────────────┐ │ │ │ +│ │ │ RDS Postgres │ │ ElastiCache │ │ │ │ +│ │ │ (encrypted) │ │ (Redis) │ │ │ │ +│ │ └────────────────┘ └────────────────┘ │ │ │ +│ │ │ │ │ +│ └───────────────────────────────────────────────────┼─────────────┘ │ +│ │ │ +│ ┌───────────────────────────────────────────────────┼─────────────┐ │ +│ │ Private Subnet (Data) │ │ │ +│ │ ▼ │ │ +│ │ ┌────────────────────────────────────────────────────────┐ │ │ +│ │ │ Your MCP Server (Fargate) │ │ │ +│ │ │ - IAM Role for data access │ │ │ +│ │ │ - VPC endpoint for S3/Secrets Manager │ │ │ +│ │ │ - No internet access │ │ │ +│ │ └────────────────────────────────────────────────────────┘ │ │ +│ └─────────────────────────────────────────────────────────────────┘ │ +│ │ +└─────────────────────────────────────────────────────────────────────────┘ + +External Services (via VPC Endpoints): +├── AWS Secrets Manager (API keys) +├── CloudWatch (logs, metrics) +├── S3 (artifacts, optional) +└── ECR (container images) +``` + +### Implementation Requirements + +#### 1. Kubernetes Sandbox Provider + +Replace Docker provider with Kubernetes-native sandbox management: + +```python +# src/ii_sandbox_server/sandboxes/kubernetes.py (new file) +class KubernetesSandbox(BaseSandbox): + """ + Kubernetes-native sandbox provider. + + Creates pods with gVisor runtime for VM-level isolation + without the overhead of actual VMs. + """ + + async def create(self, ...): + pod_manifest = { + "apiVersion": "v1", + "kind": "Pod", + "metadata": { + "name": f"sandbox-{sandbox_id}", + "namespace": "ii-agent-sandboxes", + "labels": {"ii-agent.sandbox": "true"} + }, + "spec": { + "runtimeClassName": "gvisor", # VM-level isolation + "securityContext": { + "runAsNonRoot": True, + "seccompProfile": {"type": "RuntimeDefault"} + }, + "containers": [{ + "name": "sandbox", + "image": self.config.sandbox_image, + "resources": { + "limits": {"memory": "2Gi", "cpu": "2"}, + "requests": {"memory": "512Mi", "cpu": "0.5"} + }, + "securityContext": { + "allowPrivilegeEscalation": False, + "capabilities": {"drop": ["ALL"]} + } + }] + } + } +``` + +#### 2. Network Policies + +```yaml +# k8s/network-policy.yaml +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: sandbox-isolation + namespace: ii-agent-sandboxes +spec: + podSelector: + matchLabels: + ii-agent.sandbox: "true" + policyTypes: + - Ingress + - Egress + ingress: + - from: + - namespaceSelector: + matchLabels: + name: ii-agent-system + podSelector: + matchLabels: + app: sandbox-server + egress: + # Allow DNS + - to: + - namespaceSelector: {} + podSelector: + matchLabels: + k8s-app: kube-dns + ports: + - protocol: UDP + port: 53 + # Allow MCP server only + - to: + - namespaceSelector: + matchLabels: + name: ii-agent-data + podSelector: + matchLabels: + app: mcp-server + ports: + - protocol: TCP + port: 6060 +``` + +#### 3. Pod Security Standards + +```yaml +# k8s/namespace.yaml +apiVersion: v1 +kind: Namespace +metadata: + name: ii-agent-sandboxes + labels: + pod-security.kubernetes.io/enforce: restricted + pod-security.kubernetes.io/enforce-version: latest +``` + +#### 4. IAM Roles for Service Accounts (IRSA) + +```yaml +# k8s/service-account.yaml +apiVersion: v1 +kind: ServiceAccount +metadata: + name: sandbox-server + namespace: ii-agent-system + annotations: + eks.amazonaws.com/role-arn: arn:aws:iam::ACCOUNT:role/ii-agent-sandbox-server +--- +# IAM Policy (Terraform) +resource "aws_iam_role_policy" "sandbox_server" { + role = aws_iam_role.sandbox_server.id + policy = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + Effect = "Allow" + Action = [ + "secretsmanager:GetSecretValue" + ] + Resource = [ + "arn:aws:secretsmanager:*:*:secret:ii-agent/*" + ] + } + ] + }) +} +``` + +#### 5. Secrets Management + +```python +# src/ii_sandbox_server/config.py additions +import boto3 + +def get_secret(secret_name: str) -> str: + """Retrieve secret from AWS Secrets Manager.""" + client = boto3.client('secretsmanager') + response = client.get_secret_value(SecretId=secret_name) + return response['SecretString'] + +# Usage +config = SandboxConfig( + jwt_secret=get_secret("ii-agent/jwt-secret"), + # Never in environment variables +) +``` + +### Security Comparison + +| Aspect | Local Docker | Cloud K8s | +|--------|--------------|-----------| +| Container isolation | Process namespace | gVisor (VM-level) | +| Network isolation | Bridge network | NetworkPolicy (deny-all) | +| Host access | Docker socket (root) | No host access | +| Secrets | Env vars | Secrets Manager + IRSA | +| Multi-tenant | ❌ No | ✅ Yes (namespace isolation) | +| Audit logging | Optional | CloudWatch + CloudTrail | +| Compliance | Manual | SOC2/HIPAA capable | + +--- + +## Migration Checklist + +### Local → Team + +- [ ] Generate TLS certificates (or use Let's Encrypt) +- [ ] Configure reverse proxy with rate limiting +- [ ] Set up OIDC/SAML authentication +- [ ] Create isolated Docker network for sandboxes +- [ ] Implement audit logging +- [ ] Document incident response procedures + +### Team → Cloud + +- [ ] Provision EKS cluster with gVisor runtime +- [ ] Implement KubernetesSandbox provider +- [ ] Configure NetworkPolicies +- [ ] Set up IRSA for service accounts +- [ ] Migrate secrets to Secrets Manager +- [ ] Configure CloudWatch logging +- [ ] Set up ALB with WAF +- [ ] Implement horizontal pod autoscaling +- [ ] Configure pod disruption budgets +- [ ] Set up monitoring (Prometheus/Grafana or CloudWatch) +- [ ] Penetration testing +- [ ] Compliance review (if required) + +--- + +## Cost Considerations + +| Component | Local | Team (On-prem) | Cloud (AWS) | +|-----------|-------|----------------|-------------| +| Compute | Your hardware | Your servers | ~$200-500/mo (EKS + nodes) | +| Database | Docker | Your DB | ~$50-200/mo (RDS) | +| Networking | Free | Your network | ~$20-50/mo (NAT, ALB) | +| Secrets | N/A | HashiCorp Vault | ~$5/mo (Secrets Manager) | +| Monitoring | Local | Prometheus | ~$50-100/mo (CloudWatch) | +| **Total** | **$0** | **Your infra** | **~$325-850/mo** | + +--- + +## Timeline Estimate + +| Phase | Effort | Prerequisites | +|-------|--------|---------------| +| Local (done) | 0 | Docker installed | +| Team deployment | 1-2 weeks | TLS certs, auth provider | +| Cloud MVP | 2-4 weeks | AWS account, K8s experience | +| Production hardening | 2-4 weeks | Security review, compliance | + +--- + +## References + +- [Kubernetes Pod Security Standards](https://kubernetes.io/docs/concepts/security/pod-security-standards/) +- [gVisor Container Sandbox](https://gvisor.dev/) +- [AWS EKS Best Practices](https://aws.github.io/aws-eks-best-practices/) +- [OWASP Container Security](https://cheatsheetseries.owasp.org/cheatsheets/Docker_Security_Cheat_Sheet.html) diff --git a/docs/docs/core-infrastructure.md b/docs/docs/core-infrastructure.md index 9bffb38bc..b172f3aec 100644 --- a/docs/docs/core-infrastructure.md +++ b/docs/docs/core-infrastructure.md @@ -52,6 +52,12 @@ Variables: `BACKEND_PORT`, `FRONTEND_PORT`, `SANDBOX_SERVER_PORT`, `TOOL_SERVER_ - Map each to an open host port. The defaults (8000/3000/9000/etc.) usually work. - When a collision happens, bump the conflicting port and update any URLs or CLIs that pointed to the old value (e.g., `VITE_API_URL`). +## Docker sandbox port pool + +When running in local Docker mode (`SANDBOX_PROVIDER=docker`), the sandbox server dynamically maps container ports to the host from the range **30000-30999**. Each sandbox reserves 6 host ports (MCP, code-server, noVNC, and spares), allowing approximately 166 concurrent sandboxes. + +The frontend automatically rewrites `localhost` URLs to the browser's hostname so sandbox services remain accessible when the UI is accessed from a different machine on the LAN. + ## Validation checklist 1. Run `./scripts/run_stack.sh --build` and ensure Docker does **not** report binding conflicts. diff --git a/docs/docs/feature-branch-analysis.md b/docs/docs/feature-branch-analysis.md new file mode 100644 index 000000000..307b881ff --- /dev/null +++ b/docs/docs/feature-branch-analysis.md @@ -0,0 +1,405 @@ +# Feature Branch Dependency Analysis + +> **Branch:** Feature branch vs `develop` +> **Summary:** 124 files changed, 16,024 insertions(+), 295 deletions(-) +> **Primary Feature:** Local Docker Sandbox - Air-gapped deployment without E2B cloud + +--- + +## Executive Summary + +This feature branch implements a **complete local-only deployment mode** for ii-agent, eliminating the dependency on E2B cloud sandboxes and GCS storage. The changes enable: + +1. **Docker-based sandboxes** running on the local host +2. **Local filesystem storage** replacing Google Cloud Storage +3. **Orphan cleanup system** to manage sandbox lifecycle +4. **Extended token budgets** for large context models + +--- + +## Tier 0: Configuration & Constants (Foundation Layer) + +### Token Budget Constants +**File:** [src/ii_agent/utils/constants.py](../src/ii_agent/utils/constants.py) + +| Constant | Value | Purpose | +|----------|-------|---------| +| `TOKEN_BUDGET_NORMAL` | 200,000 | Standard context window | +| `TOKEN_BUDGET_EXTENDED` | 800,000 | **NEW** - Extended context models (Claude 4.5) | + +### Agent Configuration +**File:** [src/ii_agent/core/config/ii_agent_config.py](../src/ii_agent/core/config/ii_agent_config.py) + +| Setting | Old Default | New Default | Notes | +|---------|-------------|-------------|-------| +| `storage_provider` | `"gcs"` | `"local"` | Enables local-first deployment | + +### Sandbox Server Configuration +**File:** [src/ii_sandbox_server/config.py](../src/ii_sandbox_server/config.py) + +**New Configuration Options:** + +```python +class Config(BaseSettings): + # Sandbox provider selection + provider_type: Literal["e2b", "docker"] = "e2b" # validation_alias="SANDBOX_PROVIDER" + + # Docker-specific settings + docker_image: str = "ii-sandbox:latest" + docker_network: str = "ii-agent-network" + + # Orphan cleanup settings + local_mode: bool = False # Enable orphan cleanup + orphan_cleanup_enabled: bool = True # Can be disabled + orphan_cleanup_interval_seconds: int = 60 + backend_url: str = "http://backend:8000" # For session verification +``` + +### Base Classes (API Contracts) + +**Storage Base** - [src/ii_agent/storage/base.py](../src/ii_agent/storage/base.py) +- No changes to interface - LocalStorage implements existing contract + +**Sandbox Base** - [src/ii_sandbox_server/sandboxes/base.py](../src/ii_sandbox_server/sandboxes/base.py) +- `expose_port(port: int, external: bool = False)` - **NEW parameter** + - `external=False`: Returns container-to-container URL (Docker network) + - `external=True`: Returns browser-accessible URL (host port) + +--- + +## Tier 1: Infrastructure Components (Building Blocks) + +### Port Pool Manager (NEW) +**File:** [src/ii_sandbox_server/sandboxes/port_manager.py](../src/ii_sandbox_server/sandboxes/port_manager.py) (480 lines) + +A singleton service managing port allocation for Docker sandbox containers. + +**Architecture:** +``` +┌─────────────────────────────────────────────────────────────┐ +│ PortPoolManager │ +│ ┌──────────────┐ ┌──────────────┐ ┌──────────────────┐ │ +│ │ Port Pool │ │ Allocations │ │ Orphan Cleanup │ │ +│ │ 30000-30999 │ │ by Sandbox │ │ Background │ │ +│ └──────────────┘ └──────────────┘ └──────────────────┘ │ +└─────────────────────────────────────────────────────────────┘ +``` + +**Key Components:** + +| Class | Purpose | +|-------|---------| +| `PortAllocation` | Single port mapping (host_port, container_port, purpose) | +| `SandboxPortSet` | All ports for one sandbox + creation timestamp | +| `PortPoolManager` | Singleton managing allocation/deallocation | + +**Port Range:** +- **Range:** 30000-30999 (1,000 ports) +- **Per Sandbox:** Up to 5 ports (SSH, web server, debug, etc.) +- **Capacity:** ~200 concurrent sandboxes + +**Key Features:** +1. **Thread-safe allocation** using asyncio Lock +2. **Startup scanning** - Detects existing ii-sandbox containers on restart +3. **Orphan cleanup** - Background task releases ports for dead containers +4. **Graceful initialization** - Handles Docker not running + +### Local Storage Provider (NEW) +**File:** [src/ii_agent/storage/local.py](../src/ii_agent/storage/local.py) (175 lines) + +**Also duplicated for tool server:** +**File:** [src/ii_tool/integrations/storage/local.py](../src/ii_tool/integrations/storage/local.py) (172 lines) + +Replaces GCS for file storage in local deployments. + +**Features:** +| Feature | Implementation | +|---------|----------------| +| Path traversal protection | `os.path.abspath().startswith(base_path)` | +| Content-type storage | `.meta` sidecar files | +| URL download | Browser-like headers to avoid bot detection | +| Public URL generation | `{TOOL_SERVER_URL}/storage/{path}` | + +**Storage Factory Updates:** +**File:** [src/ii_agent/storage/factory.py](../src/ii_agent/storage/factory.py) + +```python +def create_storage_client(config: StorageConfig) -> BaseStorage: + if config.storage_provider == "local": + return LocalStorage(config) # NEW + if config.storage_provider == "gcs": + return GCS(config) + raise ValueError(f"Unknown storage provider: {config.storage_provider}") +``` + +--- + +## Tier 2: Docker Sandbox Implementation (Core Feature) + +### DockerSandbox Provider (NEW) +**File:** [src/ii_sandbox_server/sandboxes/docker.py](../src/ii_sandbox_server/sandboxes/docker.py) (974 lines) + +The core implementation replacing E2B cloud sandboxes. + +**Class Hierarchy:** +``` +BaseSandbox (Abstract) + ├── E2BSandbox (Cloud - existing) + └── DockerSandbox (Local - NEW) +``` + +**Container Lifecycle:** +``` +create() ────► Container Created ────► Running + │ + ▼ + Port Allocated + (via PortPoolManager) + │ + ▼ + Services Started + (SSH, Agent) + │ + ▼ +kill() ────────► Container Removed ────► Ports Released +``` + +**Key Methods:** + +| Method | Purpose | +|--------|---------| +| `create()` | Create container, allocate ports, start services | +| `run_command()` | Execute shell command with timeout and streaming | +| `upload()` / `download()` | File transfer via docker cp | +| `expose_port()` | Dynamic port mapping for web servers | +| `kill()` | Stop container, release ports | + +**Security Features:** +1. **Path validation** - Prevents escaping sandbox directory +2. **Command sanitization** - Protects against shell injection +3. **Resource limits** - CPU/memory constraints via Docker +4. **Network isolation** - Containers on dedicated network + +**Port Mapping Strategy:** +``` +Browser Request Docker Container + │ │ + ▼ ▼ + localhost:30001 ──────────► container:8080 + (host port) expose_port (container port) +``` + +--- + +## Tier 3: Orchestration (Lifecycle Management) + +### Sandbox Controller - Orphan Cleanup (NEW) +**File:** [src/ii_sandbox_server/lifecycle/sandbox_controller.py](../src/ii_sandbox_server/lifecycle/sandbox_controller.py) + +**New Feature:** Background cleanup of orphaned sandboxes (~120 new lines) + +**Problem Solved:** +When a chat session is deleted in the backend, the sandbox continues running. The orphan cleanup system detects and removes these orphans. + +**Flow:** +``` +┌─────────────────────────────────────────────────────────────┐ +│ _orphan_cleanup_loop() │ +│ │ +│ 1. List all active sandboxes │ +│ 2. For each sandbox: │ +│ a. Skip if created < 5 minutes ago (grace period) │ +│ b. Call backend: GET /internal/sandboxes/{id}/has-active│ +│ c. If no active session → kill sandbox │ +│ 3. Sleep for orphan_cleanup_interval_seconds │ +│ 4. Repeat │ +└─────────────────────────────────────────────────────────────┘ +``` + +**Configuration:** +```python +local_mode: bool = False # Must be True to enable +orphan_cleanup_enabled: bool = True # Can disable for debugging +orphan_cleanup_interval_seconds: int = 60 # Check frequency +backend_url: str = "http://backend:8000" # Backend API endpoint +``` + +**Grace Period:** +- New sandboxes are protected for **5 minutes** after creation +- Prevents race condition during session initialization + +--- + +## Tier 4: Integration Layer (API & Infrastructure) + +### Backend API - File Endpoints +**File:** [src/ii_agent/server/api/files.py](../src/ii_agent/server/api/files.py) + +**New Endpoints for Local Storage:** + +| Method | Endpoint | Purpose | +|--------|----------|---------| +| `PUT` | `/files/upload/{path:path}` | Upload file to local storage | +| `GET` | `/files/{path:path}` | Download file with token validation | + +**Token-Based Authentication:** +- Files accessed via signed URLs with `token` query parameter +- Tokens are HMAC signatures with expiration + +### Tool Server - Storage Endpoint +**File:** [src/ii_tool/integrations/app/main.py](../src/ii_tool/integrations/app/main.py) + +**New Endpoint:** + +| Method | Endpoint | Purpose | +|--------|----------|---------| +| `GET` | `/storage/{file_path:path}` | Serve files from LocalStorage | + +Only active when `STORAGE_PROVIDER=local`. Returns 404 for GCS mode. + +### Docker Compose - Local-Only Stack (NEW) +**File:** [docker/docker-compose.local-only.yaml](../docker/docker-compose.local-only.yaml) (194 lines) + +Complete local deployment without any cloud dependencies. + +**Services:** +```yaml +services: + postgres: # Database + redis: # Cache/Queue + frontend: # React UI + backend: # FastAPI server + tool-server: # Tool execution + sandbox-server: # Sandbox management +``` + +**Key Environment Variables:** +```yaml +sandbox-server: + SANDBOX_PROVIDER: docker + LOCAL_MODE: "true" + DOCKER_HOST: unix:///var/run/docker.sock + +backend: + STORAGE_PROVIDER: local + LOCAL_STORAGE_PATH: /app/storage +``` + +**Volume Mounts:** +```yaml +sandbox-server: + volumes: + - /var/run/docker.sock:/var/run/docker.sock # Docker access + - shared-storage:/app/storage # File storage +``` + +--- + +## Dependency Graph + +``` + ┌─────────────────────┐ + │ Configuration │ + │ (constants, config)│ + └─────────┬───────────┘ + │ + ┌───────────────┼───────────────┐ + ▼ ▼ ▼ + ┌─────────────────┐ ┌──────────────┐ ┌──────────────┐ + │ PortPoolManager│ │ LocalStorage │ │ Base Classes │ + │ (Tier 1) │ │ (Tier 1) │ │ (Tier 0) │ + └────────┬────────┘ └──────┬───────┘ └──────┬───────┘ + │ │ │ + ▼ │ │ + ┌─────────────────┐ │ │ + │ DockerSandbox │◄───────┴────────────────┘ + │ (Tier 2) │ + └────────┬────────┘ + │ + ▼ + ┌─────────────────┐ + │SandboxController│ + │ Orphan Cleanup │ + │ (Tier 3) │ + └────────┬────────┘ + │ + ▼ + ┌─────────────────┐ + │ API Routes │ + │ Docker Compose │ + │ (Tier 4) │ + └─────────────────┘ +``` + +--- + +## Migration Guide + +### From E2B Cloud to Local Docker + +1. **Prerequisites:** + - Docker installed and running + - Docker Compose v2+ + - At least 8GB RAM available + +2. **Environment Variables:** + ```bash + # Required changes + SANDBOX_PROVIDER=docker + STORAGE_PROVIDER=local + LOCAL_MODE=true + + # Not required for local mode + # E2B_API_KEY + # GCS_BUCKET_NAME + # GCS_PROJECT_ID + ``` + +3. **Start Local Stack:** + ```bash + docker compose -f docker/docker-compose.local-only.yaml up -d + ``` + +4. **Verify:** + - Check sandbox-server logs for "Using Docker sandbox provider" + - Create a test chat and verify container creation + - Upload a file and verify local storage + +--- + +## Security Considerations + +| Component | Security Measure | +|-----------|-----------------| +| DockerSandbox | Path validation, command sanitization, resource limits | +| LocalStorage | Path traversal protection, base path enforcement | +| Port Manager | Dynamic allocation prevents port conflicts | +| Orphan Cleanup | Grace period prevents premature termination | +| File Endpoints | Token-based signed URLs with expiration | + +--- + +## Performance Notes + +| Metric | E2B Cloud | Local Docker | +|--------|-----------|--------------| +| Sandbox creation | 5-10s | 1-3s | +| File upload | Network dependent | Local disk speed | +| Concurrent sandboxes | Limited by API quota | ~200 (port pool) | +| Network latency | Cloud RTT | Negligible | + +--- + +## Files Changed Summary + +| Category | Files | Lines Changed | +|----------|-------|---------------| +| New Docker Sandbox | 2 | +1,454 | +| New Local Storage | 4 | +400 | +| Orphan Cleanup | 1 | +120 | +| Configuration | 4 | +80 | +| Docker Compose | 2 | +200 | +| API Endpoints | 2 | +100 | +| Tests | ~20 | +3,000 | +| Documentation | 5 | +1,500 | +| **Total** | **124** | **+16,024 / -295** | diff --git a/docs/docs/getting-started.md b/docs/docs/getting-started.md index b31650e8b..2ddd662eb 100644 --- a/docs/docs/getting-started.md +++ b/docs/docs/getting-started.md @@ -32,6 +32,8 @@ Use this runbook whenever you need to spin up the full II-Agent Docker stack (Po - Drop the `--build` flag after the first boot to reuse images. - Stop the stack with `docker compose -f docker/docker-compose.stack.yaml down`. +> **Local-only mode (no cloud services):** If you don't need E2B, ngrok, or GCS you can run entirely with Docker sandboxes. See the [Local Docker Sandbox](./local-docker-sandbox.md) guide and use `docker-compose.local-only.yaml` instead. + ## Required variables overview | Section | Key variables | Why they matter | @@ -43,7 +45,7 @@ Use this runbook whenever you need to spin up the full II-Agent Docker stack (Po | Storage | `SLIDE_ASSETS_PROJECT_ID`, `SLIDE_ASSETS_BUCKET_NAME`, `FILE_UPLOAD_*`, `AVATAR_*`, `CUSTOM_DOMAIN` | Buckets that persist agent-generated assets. | | Backend sandbox | `SANDBOX_TEMPLATE_ID`, `TIME_TIL_CLEAN_UP` | Define how on-demand sandboxes are provisioned and reclaimed. | | Tool server | `STORAGE_CONFIG__GCS_*` | Buckets used by the tool server baseline. | -| Sandbox server | `E2B_API_KEY`, `E2B_TEMPLATE_ID` | Credentials for the hosted sandbox provider. | +| Sandbox server | `E2B_API_KEY`, `E2B_TEMPLATE_ID` | Credentials for the hosted sandbox provider (not needed for local-only Docker mode). | | Core infra | `POSTGRES_*`, `DATABASE_URL`, `SANDBOX_DB_*`, `REDIS_PORT`, `BACKEND_PORT`, `FRONTEND_PORT`, `SANDBOX_SERVER_PORT`, `TOOL_SERVER_PORT`, `NGROK_METRICS_PORT`, `MCP_PORT` | Databases and host port mappings that every service relies on. | The required guide links to the detailed setup pages for each section (frontend env, tunnels, host paths, etc.). Keep it open while editing `.stack.env`. diff --git a/docs/docs/local-docker-sandbox.md b/docs/docs/local-docker-sandbox.md new file mode 100644 index 000000000..8ec5ab1d4 --- /dev/null +++ b/docs/docs/local-docker-sandbox.md @@ -0,0 +1,391 @@ +# Local Docker Sandbox Setup + +This guide explains how to run ii-agent with **local Docker containers** instead of E2B cloud sandboxes. This setup keeps all data on your machine and is suitable for: + +- Privileged or NDA-protected data +- Air-gapped or restricted network environments +- Development and testing without cloud dependencies +- Self-hosted deployments + +## Overview + +ii-agent supports multiple sandbox providers through a pluggable architecture: + +| Provider | Description | Use Case | +|----------|-------------|----------| +| `e2b` (default) | E2B cloud micro-VMs | Production, quick setup | +| `docker` | Local Docker containers | Privacy, air-gapped, self-hosted | + +## Prerequisites + +- Docker Engine 20.10+ with Docker Compose v2 +- At least 4GB RAM available for containers +- An LLM API key (OpenAI, Anthropic, etc.) + +## Quick Start + +### 1. Build the Sandbox Image + +The sandbox image contains the same tools as E2B sandboxes (Python, Node.js, Playwright, code-server): + +```bash +cd /path/to/ii-agent + +# Build the sandbox image +docker build -t ii-agent-sandbox:latest -f e2b.Dockerfile . +``` + +This creates an image with: +- Python 3.10 with common data science packages +- Node.js 24 with npm/yarn/pnpm +- Playwright with Chromium for web automation +- code-server (VS Code in browser) +- noVNC + x11vnc for browser-based VNC access (user handoff for CAPTCHAs/login) +- Bun runtime +- tmux for session management + +### 2. Configure Environment + +```bash +# Copy the example environment file +cp docker/.stack.env.local.example docker/.stack.env.local + +# Edit and configure required values +nano docker/.stack.env.local +``` + +**Required configuration:** +```bash +# Generate a secure JWT secret +JWT_SECRET_KEY=$(openssl rand -hex 32) + +# Add at least one LLM API key +OPENAI_API_KEY=sk-... +# or +ANTHROPIC_API_KEY=sk-ant-... +``` + +### 3. Start the Stack + +```bash +# From the project root +docker compose -f docker/docker-compose.local-only.yaml \ + --env-file docker/.stack.env.local \ + up -d +``` + +### 4. Access the Application + +- **Frontend**: http://localhost:1420 +- **Backend API**: http://localhost:8000 +- **Sandbox Server**: http://localhost:8100 + +## How It Works + +### Architecture + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ Host Machine │ +├─────────────────────────────────────────────────────────────────┤ +│ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌──────────────────┐ │ +│ │Frontend │ │ Backend │ │ Sandbox │ │ Tool Server │ │ +│ │ :1420 │ │ :8000 │ │ Server │ │ :1236 │ │ +│ └────┬────┘ └────┬────┘ │ :8100 │ └──────────────────┘ │ +│ │ │ └────┬────┘ │ +│ │ │ │ │ +│ │ │ │ Docker API │ +│ │ │ ▼ │ +│ │ │ ┌──────────────────────────────────┐ │ +│ │ │ │ Sandbox Containers (ephemeral) │ │ +│ │ │ │ ┌─────────┐ ┌─────────┐ │ │ +│ │ │ │ │Sandbox 1│ │Sandbox 2│ ... │ │ +│ │ │ │ │ Python │ │ Node.js │ │ │ +│ │ │ │ │Playwright│ │code-svr │ │ │ +│ │ │ │ │ noVNC │ │ noVNC │ │ │ +│ │ │ │ └─────────┘ └─────────┘ │ │ +│ │ │ └──────────────────────────────────┘ │ +│ │ │ │ +│ ┌────┴────────────┴────────────────────────────────────────┐ │ +│ │ Docker Network │ │ +│ └──────────────────────────────────────────────────────────┘ │ +│ │ +│ ┌─────────┐ ┌─────────┐ │ +│ │Postgres │ │ Redis │ │ +│ │ :5433 │ │ :6379 │ │ +│ └─────────┘ └─────────┘ │ +└─────────────────────────────────────────────────────────────────┘ +``` + +### Sandbox Lifecycle + +1. **Creation**: When a task requires code execution, `sandbox-server` creates a new Docker container +2. **Execution**: Commands and file operations run inside the isolated container +3. **Persistence**: Workspace files persist in a mounted volume for the session duration +4. **Cleanup**: Containers are stopped/removed when the session ends or times out + +### Key Differences from E2B + +| Feature | E2B Cloud | Docker Local | +|---------|-----------|--------------| +| Startup time | ~150ms (pre-warmed) | ~2-5s (cold start) | +| Isolation | Firecracker micro-VM | Docker container | +| Network | Requires ngrok tunnel | Host-local only | +| Data location | E2B infrastructure | Your machine | +| Scaling | Managed by E2B | Manual (resource limits) | +| Cost | Pay per use | Free (your hardware) | + +## Configuration Reference + +### Environment Variables + +#### Sandbox Configuration + +| Variable | Default | Description | +|----------|---------|-------------| +| `SANDBOX_PROVIDER` | `e2b` | Set to `docker` for local sandboxes | +| `SANDBOX_DOCKER_IMAGE` | `ii-agent-sandbox:latest` | Docker image for sandboxes | +| `SANDBOX_DOCKER_NETWORK` | (none) | Optional network for sandbox containers | +| `SANDBOX_PORT_RANGE_START` | `30000` | Start of host port range for sandbox port mappings | +| `SANDBOX_PORT_RANGE_END` | `30999` | End of host port range for sandbox port mappings | +| `POSTGRES_PORT` | `5432` | PostgreSQL port (use 5433 if 5432 is taken) | + +#### Orphan Cleanup Configuration + +When running in local mode, the sandbox server automatically cleans up containers whose associated chat sessions have been deleted. + +| Variable | Default | Description | +|----------|---------|-------------| +| `LOCAL_MODE` | `false` | Set to `true` to enable orphan cleanup | +| `ORPHAN_CLEANUP_ENABLED` | `true` | Can disable cleanup for debugging | +| `ORPHAN_CLEANUP_INTERVAL_SECONDS` | `60` | How often to check for orphaned sandboxes | +| `BACKEND_URL` | `http://backend:8000` | Backend API URL for session verification | + +**How It Works:** +1. Every 60 seconds (configurable), the sandbox server queries all active sandboxes +2. For each sandbox older than 5 minutes, it calls the backend to verify the session exists +3. If the session was deleted, the sandbox container is automatically removed +4. The 5-minute grace period prevents cleanup during session initialization + +#### Storage Configuration + +Local deployments use local filesystem storage instead of cloud storage (GCS): + +| Variable | Default | Description | +|----------|---------|-------------| +| `STORAGE_PROVIDER` | `local` | Use `local` for filesystem, `gcs` for Google Cloud | +| `LOCAL_STORAGE_PATH` | `/.ii_agent/storage` | Base directory for file storage | +| `PUBLIC_TOOL_SERVER_URL` | (auto) | Public URL for the tool server (for file URLs) | + +When using local storage: +- Files are stored on the local filesystem +- Content-types are preserved in `.meta` sidecar files +- Files are served via the tool server's `/storage/{path}` endpoint +- Path traversal attacks are prevented by path validation + +### Port Management + +Docker sandboxes expose internal ports (MCP server, code-server, noVNC, dev servers) to the host. The sandbox server manages a **port pool** to prevent conflicts: + +- **Default range**: 30000-30999 (1000 ports) +- **Per sandbox**: 6 ports allocated (MCP:6060, code-server:9000, noVNC:6080, plus dev ports 3000, 5173, 8080) +- **Capacity**: ~166 concurrent sandboxes with default settings + +**API Endpoints** (for monitoring): +- `GET /ports/stats` - Pool statistics (allocated, free, sandboxes) +- `GET /ports/allocations` - List all current port allocations +- `POST /ports/cleanup` - Force cleanup of orphaned allocations + +### noVNC Browser Handoff + +Each sandbox container runs a **noVNC** web viewer (port 6080) that provides browser-based access to the sandbox's virtual display. This enables a **human-in-the-loop** workflow: + +1. The agent automates a browser task using Playwright +2. The agent hits a barrier it can't handle (CAPTCHA, login page, 2FA prompt) +3. The agent calls `expose_port(sandbox_id, 6080, external=True)` to get a noVNC URL +4. The agent shares the URL with the user +5. The user opens the URL in their browser and interacts directly with the sandbox's Chromium instance +6. The user tells the agent they're done +7. The agent resumes automation + +**Architecture:** + +``` +Agent (Playwright MCP) → Chromium → Xvfb :99 ← x11vnc :5900 ← websockify :6080 ← User's browser +``` + +The virtual display was always running (for Playwright's headed mode). x11vnc + noVNC simply provide a window into it. Both the agent and user can interact with the browser simultaneously (x11vnc runs with `-shared`). + +**Manual access** (for debugging — find the host-mapped port): + +```bash +# Find the noVNC port for a sandbox +curl -s http://localhost:8100/ports/allocations | jq '.[] | select(.service_name == "novnc")' + +# Or check Docker port mapping directly +docker port ii-sandbox- 6080 +``` + +Then open `http://localhost:/vnc.html` in your browser. + +### Resource Limits + +Edit the Docker Compose file to adjust container resources: + +```yaml +sandbox-server: + deploy: + resources: + limits: + cpus: '2' + memory: 4G +``` + +## Connecting Your Local MCP Server + +If you have a local MCP server with privileged data: + +### MCP Server on Host Machine + +```bash +# In .stack.env.local +MCP_SERVER_URL=http://host.docker.internal:6060 +``` + +### MCP Server in Docker + +If your MCP server runs in a container, put it on the same network: + +```yaml +# In docker-compose.local-only.yaml, add your MCP server: +services: + mcp-server: + image: your-mcp-server:latest + networks: + - default + ports: + - "6060:6060" +``` + +Then configure: +```bash +MCP_SERVER_URL=http://mcp-server:6060 +``` + +## Troubleshooting + +### Container fails to start + +Check Docker logs: +```bash +docker logs ii-agent-sandbox-server-1 +``` + +Verify the sandbox image exists: +```bash +docker images | grep ii-agent-sandbox +``` + +### Permission denied on Docker socket + +The sandbox-server needs access to create containers. Either: + +1. Add your user to the docker group: `sudo usermod -aG docker $USER` +2. Or run with elevated privileges (not recommended for production) + +### PostgreSQL port conflict + +If you have PostgreSQL running locally: +```bash +# In .stack.env.local +POSTGRES_PORT=5433 +``` + +### Sandbox containers not cleaning up + +**Automatic Cleanup (Recommended):** + +If `LOCAL_MODE=true` is set, orphan cleanup runs automatically. Check if it's working: +```bash +# Check sandbox-server logs for cleanup activity +docker logs ii-agent-sandbox-server-1 2>&1 | grep -i orphan +``` + +**Manual cleanup:** +```bash +# List sandbox containers +docker ps -a | grep ii-sandbox + +# Remove all stopped sandbox containers +docker container prune -f --filter "label=ii-agent-sandbox=true" + +# Force cleanup via API +curl -X POST http://localhost:8100/ports/cleanup +``` + +## Security Considerations + +### Network Isolation + +By default, sandbox containers can access the network. For stricter isolation: + +```yaml +# In DockerSandbox configuration +network_mode: none # Complete isolation +# or +network_mode: internal # Container-to-container only +``` + +### Resource Limits + +Prevent runaway containers: + +```python +# These are configured in DockerSandbox +mem_limit="2g" +cpu_quota=100000 # 1 CPU +pids_limit=256 +``` + +### Filesystem Access + +Sandbox containers only have access to: +- Their workspace volume (mounted at `/workspace`) +- Temporary files (mounted at `/tmp`) + +They cannot access host filesystem or other containers' data. + +## Development + +### Running Tests + +```bash +# Test sandbox provider locally +pytest tests/sandbox/test_docker_sandbox.py -v +``` + +### Extending the Sandbox Image + +Create a custom Dockerfile based on `e2b.Dockerfile`: + +```dockerfile +FROM ii-agent-sandbox:latest + +# Add your custom tools +RUN pip install your-private-package +``` + +Build and configure: +```bash +docker build -t ii-agent-sandbox-custom:latest -f Dockerfile.custom . +SANDBOX_DOCKER_IMAGE=ii-agent-sandbox-custom:latest +``` + +## Contributing + +This Docker sandbox provider is designed as an extensible alternative to E2B. Contributions welcome: + +- Performance improvements +- Additional isolation options (gVisor, Kata containers) +- Kubernetes provider for scalable deployments +- Better resource management and pooling diff --git a/docs/docs/required-environment-variables/index.md b/docs/docs/required-environment-variables/index.md index e54e5c565..dbbe47fb3 100644 --- a/docs/docs/required-environment-variables/index.md +++ b/docs/docs/required-environment-variables/index.md @@ -79,8 +79,11 @@ The Docker stack only works when **every** mandatory variable in `docker/.stack. | Variable | Status | Notes | | --- | --- | --- | -| `E2B_API_KEY` | ✅ | API key issued by e2b. | -| `E2B_TEMPLATE_ID` | ✅ | Template ID for sandbox provisioning. | +| `SANDBOX_PROVIDER` | ☑️ | `e2b` (cloud, default) or `docker`/`local` (local Docker containers). | +| `E2B_API_KEY` | ☑️ | API key issued by e2b (not needed for local Docker mode). | +| `E2B_TEMPLATE_ID` | ☑️ | Template ID for e2b sandbox provisioning (not needed for local Docker mode). | +| `SANDBOX_DOCKER_IMAGE` | ☑️ | Docker image for local sandboxes (default `ii-agent-sandbox:latest`). | +| `LOCAL_MODE` | ☑️ | Enable local-mode features such as orphan cleanup. | ## Core infrastructure [`/docs/required-environment-variables/core-infra`](/docs/required-environment-variables/core-infra) diff --git a/docs/docs/required-environment-variables/llm-auth.md b/docs/docs/required-environment-variables/llm-auth.md index fea184bb2..bda80abc0 100644 --- a/docs/docs/required-environment-variables/llm-auth.md +++ b/docs/docs/required-environment-variables/llm-auth.md @@ -25,3 +25,12 @@ The backend relies on these secrets to talk to model providers, orchestrate rese ``` 4. Paste the serialized JSON blob into `LLM_CONFIGS` (wrap the value in single quotes inside `.stack.env` so special characters survive). +### Supported Anthropic models + +The frontend model selector includes: + +- `claude-sonnet-4-5` / `claude-sonnet-4-6` +- `claude-opus-4-5` / `claude-opus-4-6` + +When extended thinking is enabled (`thinking_tokens >= 1024`), the Anthropic provider automatically sets `max_tokens = thinking_tokens + 8192` to leave room for both reasoning and the final response. + diff --git a/docs/docs/required-environment-variables/sandbox-server.md b/docs/docs/required-environment-variables/sandbox-server.md index 549ef05fe..c70e3b5e6 100644 --- a/docs/docs/required-environment-variables/sandbox-server.md +++ b/docs/docs/required-environment-variables/sandbox-server.md @@ -5,24 +5,61 @@ slug: /required-environment-variables/sandbox-server sidebar_position: 17 --- -These variables configure the external sandbox provider (e.g., e2b) that powers interactive coding environments. +These variables configure the sandbox provider that powers interactive coding environments. II-Agent supports two providers: **E2B** (cloud) and **Docker** (local). -## `E2B_API_KEY` +## Choosing a provider + +Set `SANDBOX_PROVIDER` in your `.stack.env` file: + +| Value | Description | +|-------|-------------| +| `e2b` | Cloud sandboxes via [e2b.dev](https://e2b.dev/). Requires `E2B_API_KEY`. | +| `docker` or `local` | Local Docker containers. No cloud account needed. | + +For local-only deployments see the [Local Docker Sandbox](../local-docker-sandbox.md) guide. + +## E2B cloud mode + +### `E2B_API_KEY` 1. Log into the [e2b dashboard](https://e2b.dev/) (or your equivalent provider). 2. Navigate to **API Keys** and create a new key scoped for development use. 3. Copy the key (looks like `e2b_live_...`) and paste it into `docker/.stack.env`. -4. Rotate the key if you suspect compromise—do not commit it to Git. +4. Rotate the key if you suspect compromise -- do not commit it to Git. +### `E2B_TEMPLATE_ID` -The backend provisions isolated sandboxes for executing user code. These variables define the template and lifecycle policies. +1. Open the sandbox provisioning portal or service you use for backend execution (internal tool, provider dashboard, etc.). +2. Locate the template/image you want the stack to spawn (for example "ii-backend-dev"). +3. Copy its unique identifier and place it in `docker/.stack.env` as `E2B_TEMPLATE_ID`. -## `SANDBOX_TEMPLATE_ID` +## Docker local mode -1. Open the sandbox provisioning portal or service you use for backend execution (internal tool, provider dashboard, etc.). -2. Locate the template/image you want the stack to spawn (for example “ii-backend-dev”). -3. Copy its unique identifier and place it in `docker/.stack.env` as `SANDBOX_TEMPLATE_ID`. -4. If you do not know which template to use, ask the infrastructure team for the default dev template. +When `SANDBOX_PROVIDER=docker` (or `local`), the sandbox server creates ephemeral Docker containers on the host. No cloud account or API key is needed. + +### Key variables + +| Variable | Default | Description | +|----------|---------|-------------| +| `SANDBOX_DOCKER_IMAGE` | `ii-agent-sandbox:latest` | Docker image to spawn for each sandbox. | +| `DOCKER_NETWORK` | (compose project network) | Docker network sandboxes attach to. | +| `LOCAL_MODE` | `false` | Enable local-mode features (orphan cleanup). | +| `ORPHAN_CLEANUP_ENABLED` | `true` | Auto-remove sandboxes whose sessions no longer exist. | +| `ORPHAN_CLEANUP_INTERVAL_SECONDS` | `300` | How often (seconds) to check for orphans. | +| `BACKEND_URL` | `http://backend:8000` | Backend URL for session verification during cleanup. | + +### Container services + +Each Docker sandbox container runs: + +| Service | Container port | Description | +|---------|---------------|-------------| +| MCP Server | 6060 | Tool calls from the agent | +| code-server | 9000 | VS Code in the browser | +| noVNC | 6080 | Browser-based VNC for user handoff (CAPTCHAs, login) | +| Xvfb + x11vnc | :99 / 5900 | Virtual display for headed Chromium | + +Ports are dynamically mapped to the host from pool 30000-30999 (6 ports per sandbox, ~166 concurrent sandboxes). ## `TIME_TIL_CLEAN_UP` diff --git a/e2b.Dockerfile b/e2b.Dockerfile index 85f8a9a05..cfaa89dad 100644 --- a/e2b.Dockerfile +++ b/e2b.Dockerfile @@ -58,6 +58,8 @@ RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \ unzip \ libmagic1 \ xvfb \ + x11vnc \ + novnc \ pandoc \ weasyprint \ libpq-dev \ diff --git a/frontend/package.json b/frontend/package.json index ec9ba5950..70848d45f 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -12,6 +12,8 @@ "dev": "vite", "build": "tsc && vite build", "preview": "vite preview", + "test": "vitest run", + "test:watch": "vitest", "tauri": "tauri", "prepare": "husky", "lint": "eslint . --report-unused-disable-directives --max-warnings 0", @@ -121,6 +123,7 @@ "typescript": "^5.8.3", "typescript-eslint": "^8.31.1", "vite": "^6.3.4", - "vite-plugin-svgr": "^4.3.0" + "vite-plugin-svgr": "^4.3.0", + "vitest": "^4.1.0" } } diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml index 42797e07b..8f736a5f0 100644 --- a/frontend/pnpm-lock.yaml +++ b/frontend/pnpm-lock.yaml @@ -306,6 +306,9 @@ importers: vite-plugin-svgr: specifier: ^4.3.0 version: 4.3.0(rollup@4.46.2)(typescript@5.9.2)(vite@6.3.5(@types/node@22.17.2)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(yaml@2.8.1)) + vitest: + specifier: ^4.1.0 + version: 4.1.0(@opentelemetry/api@1.9.0)(@types/node@22.17.2)(vite@6.3.5(@types/node@22.17.2)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(yaml@2.8.1)) packages: @@ -1277,56 +1280,67 @@ packages: resolution: {integrity: sha512-EtP8aquZ0xQg0ETFcxUbU71MZlHaw9MChwrQzatiE8U/bvi5uv/oChExXC4mWhjiqK7azGJBqU0tt5H123SzVA==} cpu: [arm] os: [linux] + libc: [glibc] '@rollup/rollup-linux-arm-musleabihf@4.46.2': resolution: {integrity: sha512-qO7F7U3u1nfxYRPM8HqFtLd+raev2K137dsV08q/LRKRLEc7RsiDWihUnrINdsWQxPR9jqZ8DIIZ1zJJAm5PjQ==} cpu: [arm] os: [linux] + libc: [musl] '@rollup/rollup-linux-arm64-gnu@4.46.2': resolution: {integrity: sha512-3dRaqLfcOXYsfvw5xMrxAk9Lb1f395gkoBYzSFcc/scgRFptRXL9DOaDpMiehf9CO8ZDRJW2z45b6fpU5nwjng==} cpu: [arm64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-arm64-musl@4.46.2': resolution: {integrity: sha512-fhHFTutA7SM+IrR6lIfiHskxmpmPTJUXpWIsBXpeEwNgZzZZSg/q4i6FU4J8qOGyJ0TR+wXBwx/L7Ho9z0+uDg==} cpu: [arm64] os: [linux] + libc: [musl] '@rollup/rollup-linux-loongarch64-gnu@4.46.2': resolution: {integrity: sha512-i7wfGFXu8x4+FRqPymzjD+Hyav8l95UIZ773j7J7zRYc3Xsxy2wIn4x+llpunexXe6laaO72iEjeeGyUFmjKeA==} cpu: [loong64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-ppc64-gnu@4.46.2': resolution: {integrity: sha512-B/l0dFcHVUnqcGZWKcWBSV2PF01YUt0Rvlurci5P+neqY/yMKchGU8ullZvIv5e8Y1C6wOn+U03mrDylP5q9Yw==} cpu: [ppc64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-riscv64-gnu@4.46.2': resolution: {integrity: sha512-32k4ENb5ygtkMwPMucAb8MtV8olkPT03oiTxJbgkJa7lJ7dZMr0GCFJlyvy+K8iq7F/iuOr41ZdUHaOiqyR3iQ==} cpu: [riscv64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-riscv64-musl@4.46.2': resolution: {integrity: sha512-t5B2loThlFEauloaQkZg9gxV05BYeITLvLkWOkRXogP4qHXLkWSbSHKM9S6H1schf/0YGP/qNKtiISlxvfmmZw==} cpu: [riscv64] os: [linux] + libc: [musl] '@rollup/rollup-linux-s390x-gnu@4.46.2': resolution: {integrity: sha512-YKjekwTEKgbB7n17gmODSmJVUIvj8CX7q5442/CK80L8nqOUbMtf8b01QkG3jOqyr1rotrAnW6B/qiHwfcuWQA==} cpu: [s390x] os: [linux] + libc: [glibc] '@rollup/rollup-linux-x64-gnu@4.46.2': resolution: {integrity: sha512-Jj5a9RUoe5ra+MEyERkDKLwTXVu6s3aACP51nkfnK9wJTraCC8IMe3snOfALkrjTYd2G1ViE1hICj0fZ7ALBPA==} cpu: [x64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-x64-musl@4.46.2': resolution: {integrity: sha512-7kX69DIrBeD7yNp4A5b81izs8BqoZkCIaxQaOpumcJ1S/kmqNFjPhDu1LHeVXv0SexfHQv5cqHsxLOjETuqDuA==} cpu: [x64] os: [linux] + libc: [musl] '@rollup/rollup-win32-arm64-msvc@4.46.2': resolution: {integrity: sha512-wiJWMIpeaak/jsbaq2HMh/rzZxHVW1rU6coyeNNpMwk5isiPjSTx0a4YLSlYDwBH/WBvLz+EtsNqQScZTLJy3g==} @@ -1464,6 +1478,9 @@ packages: '@standard-schema/spec@1.0.0': resolution: {integrity: sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==} + '@standard-schema/spec@1.1.0': + resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} + '@standard-schema/utils@0.3.0': resolution: {integrity: sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==} @@ -1577,24 +1594,28 @@ packages: engines: {node: '>= 10'} cpu: [arm64] os: [linux] + libc: [glibc] '@tailwindcss/oxide-linux-arm64-musl@4.1.12': resolution: {integrity: sha512-V8pAM3s8gsrXcCv6kCHSuwyb/gPsd863iT+v1PGXC4fSL/OJqsKhfK//v8P+w9ThKIoqNbEnsZqNy+WDnwQqCA==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] + libc: [musl] '@tailwindcss/oxide-linux-x64-gnu@4.1.12': resolution: {integrity: sha512-xYfqYLjvm2UQ3TZggTGrwxjYaLB62b1Wiysw/YE3Yqbh86sOMoTn0feF98PonP7LtjsWOWcXEbGqDL7zv0uW8Q==} engines: {node: '>= 10'} cpu: [x64] os: [linux] + libc: [glibc] '@tailwindcss/oxide-linux-x64-musl@4.1.12': resolution: {integrity: sha512-ha0pHPamN+fWZY7GCzz5rKunlv9L5R8kdh+YNvP5awe3LtuXb5nRi/H27GeL2U+TdhDOptU7T6Is7mdwh5Ar3A==} engines: {node: '>= 10'} cpu: [x64] os: [linux] + libc: [musl] '@tailwindcss/oxide-wasm32-wasi@4.1.12': resolution: {integrity: sha512-4tSyu3dW+ktzdEpuk6g49KdEangu3eCYoqPhWNsZgUhyegEda3M9rG0/j1GV/JjVVsj+lG7jWAyrTlLzd/WEBg==} @@ -1666,30 +1687,35 @@ packages: engines: {node: '>= 10'} cpu: [arm64] os: [linux] + libc: [glibc] '@tauri-apps/cli-linux-arm64-musl@2.7.1': resolution: {integrity: sha512-/HXY0t4FHkpFzjeYS5c16mlA6z0kzn5uKLWptTLTdFSnYpr8FCnOP4Sdkvm2TDQPF2ERxXtNCd+WR/jQugbGnA==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] + libc: [musl] '@tauri-apps/cli-linux-riscv64-gnu@2.7.1': resolution: {integrity: sha512-GeW5lVI2GhhnaYckiDzstG2j2Jwlud5d2XefRGwlOK+C/bVGLT1le8MNPYK8wgRlpeK8fG1WnJJYD6Ke7YQ8bg==} engines: {node: '>= 10'} cpu: [riscv64] os: [linux] + libc: [glibc] '@tauri-apps/cli-linux-x64-gnu@2.7.1': resolution: {integrity: sha512-DprxKQkPxIPYwUgg+cscpv2lcIUhn2nxEPlk0UeaiV9vATxCXyytxr1gLcj3xgjGyNPlM0MlJyYaPy1JmRg1cA==} engines: {node: '>= 10'} cpu: [x64] os: [linux] + libc: [glibc] '@tauri-apps/cli-linux-x64-musl@2.7.1': resolution: {integrity: sha512-KLlq3kOK7OUyDR757c0zQjPULpGZpLhNB0lZmZpHXvoOUcqZoCXJHh4dT/mryWZJp5ilrem5l8o9ngrDo0X1AA==} engines: {node: '>= 10'} cpu: [x64] os: [linux] + libc: [musl] '@tauri-apps/cli-win32-arm64-msvc@2.7.1': resolution: {integrity: sha512-dH7KUjKkSypCeWPiainHyXoES3obS+JIZVoSwSZfKq2gWgs48FY3oT0hQNYrWveE+VR4VoR3b/F3CPGbgFvksA==} @@ -1744,6 +1770,9 @@ packages: '@types/babel__traverse@7.28.0': resolution: {integrity: sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==} + '@types/chai@5.2.3': + resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} + '@types/d3-array@3.2.2': resolution: {integrity: sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==} @@ -1840,6 +1869,9 @@ packages: '@types/debug@4.1.12': resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==} + '@types/deep-eql@4.0.2': + resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} + '@types/estree-jsx@1.0.5': resolution: {integrity: sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==} @@ -1968,6 +2000,35 @@ packages: peerDependencies: vite: ^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 + '@vitest/expect@4.1.0': + resolution: {integrity: sha512-EIxG7k4wlWweuCLG9Y5InKFwpMEOyrMb6ZJ1ihYu02LVj/bzUwn2VMU+13PinsjRW75XnITeFrQBMH5+dLvCDA==} + + '@vitest/mocker@4.1.0': + resolution: {integrity: sha512-evxREh+Hork43+Y4IOhTo+h5lGmVRyjqI739Rz4RlUPqwrkFFDF6EMvOOYjTx4E8Tl6gyCLRL8Mu7Ry12a13Tw==} + peerDependencies: + msw: ^2.4.9 + vite: ^6.0.0 || ^7.0.0 || ^8.0.0-0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true + + '@vitest/pretty-format@4.1.0': + resolution: {integrity: sha512-3RZLZlh88Ib0J7NQTRATfc/3ZPOnSUn2uDBUoGNn5T36+bALixmzphN26OUD3LRXWkJu4H0s5vvUeqBiw+kS0A==} + + '@vitest/runner@4.1.0': + resolution: {integrity: sha512-Duvx2OzQ7d6OjchL+trw+aSrb9idh7pnNfxrklo14p3zmNL4qPCDeIJAK+eBKYjkIwG96Bc6vYuxhqDXQOWpoQ==} + + '@vitest/snapshot@4.1.0': + resolution: {integrity: sha512-0Vy9euT1kgsnj1CHttwi9i9o+4rRLEaPRSOJ5gyv579GJkNpgJK+B4HSv/rAWixx2wdAFci1X4CEPjiu2bXIMg==} + + '@vitest/spy@4.1.0': + resolution: {integrity: sha512-pz77k+PgNpyMDv2FV6qmk5ZVau6c3R8HC8v342T2xlFxQKTrSeYw9waIJG8KgV9fFwAtTu4ceRzMivPTH6wSxw==} + + '@vitest/utils@4.1.0': + resolution: {integrity: sha512-XfPXT6a8TZY3dcGY8EdwsBulFCIw+BeeX0RZn2x/BtiY/75YGh8FeWGG8QISN/WhaqSrE2OrlDgtF8q5uhOTmw==} + '@xmldom/xmldom@0.9.8': resolution: {integrity: sha512-p96FSY54r+WJ50FIOsCOjyj/wavs8921hG5+kVMmZgKcvIKxMXHTrjNJvRgWa/zuX3B6t2lijLNFaOyuxUH+2A==} engines: {node: '>=14.6'} @@ -2067,6 +2128,10 @@ packages: resolution: {integrity: sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==} engines: {node: '>= 0.4'} + assertion-error@2.0.1: + resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} + engines: {node: '>=12'} + async-function@1.0.0: resolution: {integrity: sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==} engines: {node: '>= 0.4'} @@ -2135,6 +2200,10 @@ packages: ccount@2.0.1: resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==} + chai@6.2.2: + resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==} + engines: {node: '>=18'} + chalk@4.1.2: resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} engines: {node: '>=10'} @@ -2571,6 +2640,9 @@ packages: resolution: {integrity: sha512-uDn+FE1yrDzyC0pCo961B2IHbdM8y/ACZsKD4dG6WqrjV53BADjwa7D+1aom2rsNVfLyDgU/eigvlJGJ08OQ4w==} engines: {node: '>= 0.4'} + es-module-lexer@2.0.0: + resolution: {integrity: sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw==} + es-object-atoms@1.1.1: resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} engines: {node: '>= 0.4'} @@ -2664,6 +2736,9 @@ packages: estree-walker@2.0.2: resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} + estree-walker@3.0.3: + resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + esutils@2.0.3: resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} engines: {node: '>=0.10.0'} @@ -2679,6 +2754,10 @@ packages: resolution: {integrity: sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==} engines: {node: '>=16.17'} + expect-type@1.3.0: + resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==} + engines: {node: '>=12.0.0'} + exsolve@1.0.7: resolution: {integrity: sha512-VO5fQUzZtI6C+vx4w/4BWJpg3s/5l+6pRQEHzFRM8WFi4XffSP1Z+4qi7GbjWbvRQEbdIco5mIMq+zX4rPuLrw==} @@ -3231,24 +3310,28 @@ packages: engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] + libc: [glibc] lightningcss-linux-arm64-musl@1.30.1: resolution: {integrity: sha512-jmUQVx4331m6LIX+0wUhBbmMX7TCfjF5FoOH6SD1CttzuYlGNVpA7QnrmLxrsub43ClTINfGSYyHe2HWeLl5CQ==} engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] + libc: [musl] lightningcss-linux-x64-gnu@1.30.1: resolution: {integrity: sha512-piWx3z4wN8J8z3+O5kO74+yr6ze/dKmPnI7vLqfSqI8bccaTGY5xiSGVIJBDd5K5BHlvVLpUB3S2YCfelyJ1bw==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] + libc: [glibc] lightningcss-linux-x64-musl@1.30.1: resolution: {integrity: sha512-rRomAK7eIkL+tHY0YPxbc5Dra2gXlI63HL+v1Pdi1a3sC+tJTcFrHX+E86sulgAXeI7rSzDYhPSeHHjqFhqfeQ==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] + libc: [musl] lightningcss-win32-arm64-msvc@1.30.1: resolution: {integrity: sha512-mSL4rqPi4iXq5YVqzSsJgMVFENoa4nGTT/GjO2c0Yl9OuQfPsIfncvLrEW6RbbB24WtZ3xP/2CCmI3tNkNV4oA==} @@ -3344,6 +3427,9 @@ packages: magic-string@0.30.17: resolution: {integrity: sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==} + magic-string@0.30.21: + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + magic-string@0.30.8: resolution: {integrity: sha512-ISQTe55T2ao7XtlAStud6qwYPZjE4GK1S/BeVPus4jrq6JuOnQ00YKQC581RWhR122W7msZV263KzVeLoqidyQ==} engines: {node: '>=12'} @@ -3693,6 +3779,9 @@ packages: resolution: {integrity: sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==} engines: {node: '>= 0.4'} + obug@2.1.1: + resolution: {integrity: sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==} + onetime@6.0.0: resolution: {integrity: sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==} engines: {node: '>=12'} @@ -4133,6 +4222,9 @@ packages: resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==} engines: {node: '>= 0.4'} + siginfo@2.0.0: + resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + signal-exit@4.1.0: resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} engines: {node: '>=14'} @@ -4180,9 +4272,15 @@ packages: resolution: {integrity: sha512-S6ji+flMEga+1QU79NDbwZ8Ivf0S/MpupQQiIC0rTpU/ZTKgcajijJJb1OcByBQDjrXCN1/DJtGz4ZJeBMPGJw==} hasBin: true + stackback@0.0.2: + resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + state-local@1.0.7: resolution: {integrity: sha512-HTEHMNieakEnoe33shBYcZ7NX83ACUjCu8c40iOGEZsngj9zRnkqS9j1pqQPXwobB0ZcVTk27REb7COQ0UR59w==} + std-env@4.0.0: + resolution: {integrity: sha512-zUMPtQ/HBY3/50VbpkupYHbRroTRZJPRLvreamgErJVys0ceuzMkD44J/QjqhHjOzK42GQ3QZIeFG1OYfOtKqQ==} + stop-iteration-iterator@1.1.0: resolution: {integrity: sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==} engines: {node: '>= 0.4'} @@ -4278,13 +4376,28 @@ packages: engines: {node: '>=10'} hasBin: true + tinybench@2.9.0: + resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} + tinyexec@1.0.1: resolution: {integrity: sha512-5uC6DDlmeqiOwCPmK9jMSdOuZTh8bU39Ys6yidB+UTt5hfZUPGAypSgFRiEp+jbi9qH40BLDvy85jIU88wKSqw==} + tinyexec@1.0.4: + resolution: {integrity: sha512-u9r3uZC0bdpGOXtlxUIdwf9pkmvhqJdrVCH9fapQtgy/OeTTMZ1nqH7agtvEfmGui6e1XxjcdrlxvxJvc3sMqw==} + engines: {node: '>=18'} + tinyglobby@0.2.14: resolution: {integrity: sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==} engines: {node: '>=12.0.0'} + tinyglobby@0.2.15: + resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} + engines: {node: '>=12.0.0'} + + tinyrainbow@3.1.0: + resolution: {integrity: sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==} + engines: {node: '>=14.0.0'} + to-regex-range@5.0.1: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} engines: {node: '>=8.0'} @@ -4483,6 +4596,41 @@ packages: yaml: optional: true + vitest@4.1.0: + resolution: {integrity: sha512-YbDrMF9jM2Lqc++2530UourxZHmkKLxrs4+mYhEwqWS97WJ7wOYEkcr+QfRgJ3PW9wz3odRijLZjHEaRLTNbqw==} + engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0} + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@opentelemetry/api': ^1.9.0 + '@types/node': ^20.0.0 || ^22.0.0 || >=24.0.0 + '@vitest/browser-playwright': 4.1.0 + '@vitest/browser-preview': 4.1.0 + '@vitest/browser-webdriverio': 4.1.0 + '@vitest/ui': 4.1.0 + happy-dom: '*' + jsdom: '*' + vite: ^6.0.0 || ^7.0.0 || ^8.0.0-0 + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@opentelemetry/api': + optional: true + '@types/node': + optional: true + '@vitest/browser-playwright': + optional: true + '@vitest/browser-preview': + optional: true + '@vitest/browser-webdriverio': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + vscode-jsonrpc@8.2.0: resolution: {integrity: sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA==} engines: {node: '>=14.0.0'} @@ -4540,6 +4688,11 @@ packages: engines: {node: '>= 8'} hasBin: true + why-is-node-running@2.3.0: + resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} + engines: {node: '>=8'} + hasBin: true + wicked-good-xpath@1.3.0: resolution: {integrity: sha512-Gd9+TUn5nXdwj/hFsPVx5cuHHiF5Bwuc30jZ4+ronF1qHK5O7HD0sgmXWSEgwKquT3ClLoKPVbO6qGwVwLzvAw==} @@ -5722,6 +5875,8 @@ snapshots: '@standard-schema/spec@1.0.0': {} + '@standard-schema/spec@1.1.0': {} + '@standard-schema/utils@0.3.0': {} '@stripe/stripe-js@7.9.0': {} @@ -5968,6 +6123,11 @@ snapshots: dependencies: '@babel/types': 7.28.2 + '@types/chai@5.2.3': + dependencies: + '@types/deep-eql': 4.0.2 + assertion-error: 2.0.1 + '@types/d3-array@3.2.2': {} '@types/d3-axis@3.0.6': @@ -6089,6 +6249,8 @@ snapshots: dependencies: '@types/ms': 2.1.0 + '@types/deep-eql@4.0.2': {} + '@types/estree-jsx@1.0.5': dependencies: '@types/estree': 1.0.8 @@ -6249,6 +6411,47 @@ snapshots: transitivePeerDependencies: - supports-color + '@vitest/expect@4.1.0': + dependencies: + '@standard-schema/spec': 1.1.0 + '@types/chai': 5.2.3 + '@vitest/spy': 4.1.0 + '@vitest/utils': 4.1.0 + chai: 6.2.2 + tinyrainbow: 3.1.0 + + '@vitest/mocker@4.1.0(vite@6.3.5(@types/node@22.17.2)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(yaml@2.8.1))': + dependencies: + '@vitest/spy': 4.1.0 + estree-walker: 3.0.3 + magic-string: 0.30.21 + optionalDependencies: + vite: 6.3.5(@types/node@22.17.2)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(yaml@2.8.1) + + '@vitest/pretty-format@4.1.0': + dependencies: + tinyrainbow: 3.1.0 + + '@vitest/runner@4.1.0': + dependencies: + '@vitest/utils': 4.1.0 + pathe: 2.0.3 + + '@vitest/snapshot@4.1.0': + dependencies: + '@vitest/pretty-format': 4.1.0 + '@vitest/utils': 4.1.0 + magic-string: 0.30.21 + pathe: 2.0.3 + + '@vitest/spy@4.1.0': {} + + '@vitest/utils@4.1.0': + dependencies: + '@vitest/pretty-format': 4.1.0 + convert-source-map: 2.0.0 + tinyrainbow: 3.1.0 + '@xmldom/xmldom@0.9.8': {} '@xterm/addon-fit@0.10.0(@xterm/xterm@5.5.0)': @@ -6387,6 +6590,8 @@ snapshots: get-intrinsic: 1.3.0 is-array-buffer: 3.0.5 + assertion-error@2.0.1: {} + async-function@1.0.0: {} asynckit@0.4.0: {} @@ -6456,6 +6661,8 @@ snapshots: ccount@2.0.1: {} + chai@6.2.2: {} + chalk@4.1.2: dependencies: ansi-styles: 4.3.0 @@ -6979,6 +7186,8 @@ snapshots: iterator.prototype: 1.1.5 safe-array-concat: 1.1.3 + es-module-lexer@2.0.0: {} + es-object-atoms@1.1.1: dependencies: es-errors: 1.3.0 @@ -7134,6 +7343,10 @@ snapshots: estree-walker@2.0.2: {} + estree-walker@3.0.3: + dependencies: + '@types/estree': 1.0.8 + esutils@2.0.3: {} eventemitter3@5.0.1: {} @@ -7152,6 +7365,8 @@ snapshots: signal-exit: 4.1.0 strip-final-newline: 3.0.0 + expect-type@1.3.0: {} + exsolve@1.0.7: {} extend@3.0.2: {} @@ -7850,6 +8065,10 @@ snapshots: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 + magic-string@0.30.21: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + magic-string@0.30.8: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 @@ -8428,6 +8647,8 @@ snapshots: define-properties: 1.2.1 es-object-atoms: 1.1.1 + obug@2.1.1: {} + onetime@6.0.0: dependencies: mimic-fn: 4.0.0 @@ -8966,6 +9187,8 @@ snapshots: side-channel-map: 1.0.1 side-channel-weakmap: 1.0.2 + siginfo@2.0.0: {} + signal-exit@4.1.0: {} slice-ansi@5.0.0: @@ -9023,8 +9246,12 @@ snapshots: commander: 13.1.0 wicked-good-xpath: 1.3.0 + stackback@0.0.2: {} + state-local@1.0.7: {} + std-env@4.0.0: {} + stop-iteration-iterator@1.1.0: dependencies: es-errors: 1.3.0 @@ -9165,13 +9392,24 @@ snapshots: commander: 2.20.3 source-map-support: 0.5.21 + tinybench@2.9.0: {} + tinyexec@1.0.1: {} + tinyexec@1.0.4: {} + tinyglobby@0.2.14: dependencies: fdir: 6.5.0(picomatch@4.0.3) picomatch: 4.0.3 + tinyglobby@0.2.15: + dependencies: + fdir: 6.5.0(picomatch@4.0.3) + picomatch: 4.0.3 + + tinyrainbow@3.1.0: {} + to-regex-range@5.0.1: dependencies: is-number: 7.0.0 @@ -9387,6 +9625,34 @@ snapshots: terser: 5.43.1 yaml: 2.8.1 + vitest@4.1.0(@opentelemetry/api@1.9.0)(@types/node@22.17.2)(vite@6.3.5(@types/node@22.17.2)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(yaml@2.8.1)): + dependencies: + '@vitest/expect': 4.1.0 + '@vitest/mocker': 4.1.0(vite@6.3.5(@types/node@22.17.2)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(yaml@2.8.1)) + '@vitest/pretty-format': 4.1.0 + '@vitest/runner': 4.1.0 + '@vitest/snapshot': 4.1.0 + '@vitest/spy': 4.1.0 + '@vitest/utils': 4.1.0 + es-module-lexer: 2.0.0 + expect-type: 1.3.0 + magic-string: 0.30.21 + obug: 2.1.1 + pathe: 2.0.3 + picomatch: 4.0.3 + std-env: 4.0.0 + tinybench: 2.9.0 + tinyexec: 1.0.4 + tinyglobby: 0.2.15 + tinyrainbow: 3.1.0 + vite: 6.3.5(@types/node@22.17.2)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(yaml@2.8.1) + why-is-node-running: 2.3.0 + optionalDependencies: + '@opentelemetry/api': 1.9.0 + '@types/node': 22.17.2 + transitivePeerDependencies: + - msw + vscode-jsonrpc@8.2.0: {} vscode-languageserver-protocol@3.17.5: @@ -9462,6 +9728,11 @@ snapshots: dependencies: isexe: 2.0.0 + why-is-node-running@2.3.0: + dependencies: + siginfo: 2.0.0 + stackback: 0.0.2 + wicked-good-xpath@1.3.0: {} word-wrap@1.2.5: {} diff --git a/frontend/src/app/routes/agent.tsx b/frontend/src/app/routes/agent.tsx index e6eaaff94..8c2fb97ef 100644 --- a/frontend/src/app/routes/agent.tsx +++ b/frontend/src/app/routes/agent.tsx @@ -21,6 +21,7 @@ import { selectIsSandboxIframeAwake, selectIsMobileChatVisible, selectResultUrl, + selectSandboxStatus, setSelectedFeature, setIsMobileChatVisible, useAppDispatch, @@ -32,7 +33,6 @@ import AgentResult from '@/components/agent/agent-result' import AgentPopoverDone from '@/components/agent/agent-popover-done' import { useSocketIOContext } from '@/contexts/websocket-context' import AwakeMeUpScreen from '@/components/agent/awake-me-up-screen' -import { isE2bLink } from '@/lib/utils' import { SidebarProvider } from '@/components/ui/sidebar' import Sidebar from '@/components/sidebar' import AgentTabMobile, { @@ -51,6 +51,7 @@ function AgentPageContent() { const vscodeUrl = useAppSelector(selectVscodeUrl) const selectedBuildStep = useAppSelector(selectSelectedBuildStep) const isSandboxIframeAwake = useAppSelector(selectIsSandboxIframeAwake) + const sandboxStatus = useAppSelector(selectSandboxStatus) const [sessionData, setSessionData] = useState() const [sessionError, setSessionError] = useState(null) const [iframeKey, setIframeKey] = useState(0) @@ -126,6 +127,11 @@ function AgentPageContent() { fetchSession() }, 5000) } else { + // Redirect chat sessions to the chat page + if (data.agent_type === 'chat') { + navigate(`/chat?id=${sessionId}`, { replace: true }) + return + } dispatch(setSelectedFeature(data.agent_type)) setSessionData(data) setSessionError(null) // Clear any previous errors @@ -285,8 +291,7 @@ function AgentPageContent() {
- {vscodeUrl && - isE2bLink(vscodeUrl) && + {sandboxStatus === 'paused' && !isSandboxIframeAwake && !isRunning && !isShareMode ? ( diff --git a/frontend/src/app/routes/login.tsx b/frontend/src/app/routes/login.tsx index 65e566055..6bafaa89b 100644 --- a/frontend/src/app/routes/login.tsx +++ b/frontend/src/app/routes/login.tsx @@ -1,5 +1,5 @@ import { useGoogleLogin } from '@react-oauth/google' -import { useCallback, useEffect, useMemo, useRef } from 'react' +import React, { useCallback, useEffect, useMemo, useRef } from 'react' import { Link, useNavigate } from 'react-router' import { useForm } from 'react-hook-form' import { z } from 'zod' @@ -12,8 +12,10 @@ import { Form, FormControl, FormField, FormItem } from '@/components/ui/form' import { Input } from '@/components/ui/input' import { ACCESS_TOKEN } from '@/constants/auth' import { authService } from '@/services/auth.service' +import { settingsService } from '@/services/settings.service' import { useAppDispatch } from '@/state/store' import { setUser } from '@/state/slice/user' +import { setAvailableModels, setSelectedModel } from '@/state' import { fetchWishlist } from '@/state/slice/favorites' import { toast } from 'sonner' @@ -103,6 +105,18 @@ export function LoginPage() { const userRes = await authService.getCurrentUser() dispatch(setUser(userRes)) + + // Fetch available LLM models after login + try { + const modelsData = await settingsService.getAvailableModels() + dispatch(setAvailableModels(modelsData?.models || [])) + if (modelsData?.models?.length) { + dispatch(setSelectedModel(modelsData.models[0].id)) + } + } catch (modelError) { + console.error('Failed to fetch LLM models:', modelError) + } + dispatch(fetchWishlist()) navigate('/') @@ -322,9 +336,64 @@ export function LoginPage() { /> Continue with II Account +
) } +/** + * Dev login button - only shows if DEV_AUTH_ENABLED is set on backend + */ +function DevLoginButton({ + apiBaseUrl, + onSuccess +}: { + apiBaseUrl: string + onSuccess: (payload: IiAuthPayload | null | undefined) => Promise +}) { + const [isAvailable, setIsAvailable] = React.useState(null) + + React.useEffect(() => { + // Check if dev login is available + fetch(`${apiBaseUrl}/auth/dev/login`) + .then((res) => { + // 403 means endpoint exists but not enabled + // 200 means it's available + setIsAvailable(res.ok) + }) + .catch(() => setIsAvailable(false)) + }, [apiBaseUrl]) + + const handleDevLogin = async () => { + try { + const res = await fetch(`${apiBaseUrl}/auth/dev/login`) + if (!res.ok) { + throw new Error('Dev login failed') + } + const data = await res.json() + await onSuccess(data) + } catch (error) { + console.error('Dev login failed:', error) + } + } + + if (isAvailable !== true) { + return null + } + + return ( + + ) +} + export const Component = LoginPage diff --git a/frontend/src/components/agent/agent-build.tsx b/frontend/src/components/agent/agent-build.tsx index b91dd9139..530cf3a89 100644 --- a/frontend/src/components/agent/agent-build.tsx +++ b/frontend/src/components/agent/agent-build.tsx @@ -764,7 +764,7 @@ const AgentBuild = ({ className }: AgentBuildProps) => {

- Once finished, your app screen will placed here + Once finished, your app screen will be placed here

{/*
diff --git a/frontend/src/components/agent/agent-result.tsx b/frontend/src/components/agent/agent-result.tsx index a2f2d7b34..0a16639cc 100644 --- a/frontend/src/components/agent/agent-result.tsx +++ b/frontend/src/components/agent/agent-result.tsx @@ -8,6 +8,7 @@ import { selectIsSandboxIframeAwake, selectMessages, selectResultUrl, + selectSandboxStatus, useAppSelector } from '@/state' import { TAB, TOOL } from '@/typings/agent' @@ -15,7 +16,6 @@ import SlidesResult from './slides-result' import { Icon } from '../ui/icon' import AwakeMeUpScreen from './awake-me-up-screen' import { useLocation, useParams } from 'react-router' -import { isE2bLink } from '@/lib/utils' interface AgentResultProps { className?: string @@ -32,6 +32,7 @@ const AgentResult = ({ className }: AgentResultProps) => { const resultUrl = useAppSelector(selectResultUrl) const activeTab = useAppSelector(selectActiveTab) const isSandboxIframeAwake = useAppSelector(selectIsSandboxIframeAwake) + const sandboxStatus = useAppSelector(selectSandboxStatus) const messages = useAppSelector(selectMessages) const isRunning = useAppSelector(selectIsLoading) const isShareMode = useMemo( @@ -111,12 +112,12 @@ const AgentResult = ({ className }: AgentResultProps) => { const shouldShowAwakeScreen = useMemo(() => { return ( - isE2bLink(resultUrl) && + sandboxStatus === 'paused' && !isSandboxIframeAwake && !isRunning && !isShareMode ) - }, [resultUrl, isSandboxIframeAwake, isRunning, isShareMode]) + }, [sandboxStatus, isSandboxIframeAwake, isRunning, isShareMode]) // Extract slide data from SlideWrite and SlideEdit messages const slideContent = useMemo(() => { @@ -183,8 +184,6 @@ const AgentResult = ({ className }: AgentResultProps) => { ) } - if (!resultUrl) return null - if (shouldShowAwakeScreen) return ( { /> ) + if (!resultUrl) return null + return (
{ const messages = useAppSelector(selectMessages) + const isStopped = useAppSelector(selectIsStopped) const dispatch = useAppDispatch() const [plans, setPlans] = useState([]) @@ -26,6 +27,9 @@ const AgentTasks = ({ className }: AgentTasksProps) => { }, [messages]) useEffect(() => { + // Don't auto-promote tasks if the agent is stopped + if (isStopped) return + // Check if there are no in_progress tasks const hasInProgress = plans.some( (plan) => plan.status === 'in_progress' @@ -46,11 +50,11 @@ const AgentTasks = ({ className }: AgentTasksProps) => { setPlans(updatedPlans) } } - }, [plans, dispatch]) + }, [plans, dispatch, isStopped]) const inProgressPlans = useMemo( - () => countBy(plans, 'status').in_progress || 0, - [plans] + () => isStopped ? 0 : (countBy(plans, 'status').in_progress || 0), + [plans, isStopped] ) const completedPlans = useMemo( @@ -65,7 +69,7 @@ const AgentTasks = ({ className }: AgentTasksProps) => { className={`flex flex-col items-center justify-center w-full ${className}`} >

- In progress + {isStopped ? 'Stopped' : 'In progress'}

diff --git a/frontend/src/components/agent/subagent-container.tsx b/frontend/src/components/agent/subagent-container.tsx index 7b2bc06cf..f89bc3b04 100644 --- a/frontend/src/components/agent/subagent-container.tsx +++ b/frontend/src/components/agent/subagent-container.tsx @@ -7,11 +7,13 @@ import { CheckCircle2, XCircle, Loader2, - Clock + Clock, + StopCircle } from 'lucide-react' import { useState, useMemo } from 'react' import { AgentContext, Message } from '@/typings/agent' import { formatDuration } from '@/lib/utils' +import { useAppSelector, selectIsStopped, selectIsLoading } from '@/state' interface SubagentContainerProps { agentContext: AgentContext @@ -22,7 +24,8 @@ interface SubagentContainerProps { enum SubAgentStatus { RUNNING = 'running', COMPLETED = 'completed', - FAILED = 'failed' + FAILED = 'failed', + STOPPED = 'stopped' } const SubagentContainer = ({ @@ -31,6 +34,8 @@ const SubagentContainer = ({ children }: SubagentContainerProps) => { const [isExpanded, setIsExpanded] = useState(true) + const isStopped = useAppSelector(selectIsStopped) + const isLoading = useAppSelector(selectIsLoading) // Calculate execution time const executionTime = useMemo(() => { @@ -49,17 +54,29 @@ const SubagentContainer = ({ }, [messages]) // Determine actual status - use completed if endTime exists, even if status is not set properly + // Also check global isStopped/isLoading state to determine subagent status const actualStatus = useMemo(() => { if (agentContext.endTime) { return SubAgentStatus.COMPLETED } - const finalStatus = agentContext.status || SubAgentStatus.RUNNING - return finalStatus + const contextStatus = agentContext.status || SubAgentStatus.RUNNING + // If global agent is stopped and this subagent was still running, show as stopped + if (isStopped && contextStatus === SubAgentStatus.RUNNING) { + return SubAgentStatus.STOPPED + } + // If main agent is done (not loading, not stopped) and subagent is still "running", + // it means the subagent completed but wasn't marked - show as completed + if (!isLoading && !isStopped && contextStatus === SubAgentStatus.RUNNING) { + return SubAgentStatus.COMPLETED + } + return contextStatus }, [ agentContext.status, agentContext.endTime, agentContext.agentId, - agentContext.agentName + agentContext.agentName, + isStopped, + isLoading ]) // Get status icon @@ -69,6 +86,8 @@ const SubagentContainer = ({ return case SubAgentStatus.FAILED: return + case SubAgentStatus.STOPPED: + return case SubAgentStatus.RUNNING: return default: @@ -139,6 +158,7 @@ const SubagentContainer = ({ ${actualStatus === SubAgentStatus.COMPLETED ? 'bg-green-500/20 text-green-400' : ''} ${actualStatus === SubAgentStatus.RUNNING ? 'bg-blue-500/20 text-blue-400' : ''} ${actualStatus === SubAgentStatus.FAILED ? 'bg-red-500/20 text-red-400' : ''} + ${actualStatus === SubAgentStatus.STOPPED ? 'bg-yellow-500/20 text-yellow-400' : ''} `} > {actualStatus} diff --git a/frontend/src/components/share-agent-content.tsx b/frontend/src/components/share-agent-content.tsx index f43a16e1a..5d98211d7 100644 --- a/frontend/src/components/share-agent-content.tsx +++ b/frontend/src/components/share-agent-content.tsx @@ -28,7 +28,7 @@ import { import { BUILD_STEP, ISession, TAB } from '@/typings/agent' import AgentResult from '@/components/agent/agent-result' import AgentPopoverDone from '@/components/agent/agent-popover-done' -import { isE2bLink } from '@/lib/utils' +import { isSandboxLink } from '@/lib/utils' import { SidebarProvider } from '@/components/ui/sidebar' import AgentTabMobile, { type ChatOption as MobileChatOption @@ -75,7 +75,10 @@ export function ShareAgentContent() { fetchSession() }, 5000) } else { - dispatch(setSelectedFeature(data.agent_type)) + // Normalize chat sessions to 'general' to prevent invalid agent_type + // (ShareAgentContent should not receive chat sessions, but be defensive) + const agentType = data.agent_type === 'chat' ? 'general' : data.agent_type + dispatch(setSelectedFeature(agentType)) setSessionData(data) setSessionError(null) // Clear any previous errors } @@ -231,7 +234,7 @@ export function ShareAgentContent() {
- {vscodeUrl && isE2bLink(vscodeUrl) && ( + {vscodeUrl && isSandboxLink(vscodeUrl) && (