#!/usr/bin/env bash
#===============================================================================
# Loki Mode - Docker Sandbox Manager
# Provides isolated container execution for enhanced security
#
# Usage:
#   ./autonomy/sandbox.sh start [OPTIONS] [PRD_PATH]
#   ./autonomy/sandbox.sh stop
#   ./autonomy/sandbox.sh status
#   ./autonomy/sandbox.sh shell
#
# Environment Variables:
#   LOKI_SANDBOX_IMAGE    - Docker image to use (default: loki-mode:sandbox)
#   LOKI_SANDBOX_NETWORK  - Network mode: bridge, none, host (default: bridge)
#   LOKI_SANDBOX_ALLOW_HOSTS - Opt-in egress allowlist (comma-separated hostnames);
#                              blocks link-local and private ranges (default: unset)
#   LOKI_SANDBOX_CPUS     - CPU limit (default: 2)
#   LOKI_SANDBOX_MEMORY   - Memory limit (default: 4g)
#   LOKI_SANDBOX_READONLY - Mount project as read-only (default: false)
#
# Security Features:
#   - Seccomp profile restricts dangerous syscalls
#   - No new privileges flag prevents privilege escalation
#   - Dropped capabilities reduce attack surface
#   - Resource limits prevent DoS
#   - Optional read-only filesystem
#   - API keys mounted read-only
#===============================================================================

set -euo pipefail

# Colors
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
CYAN='\033[0;36m'
NC='\033[0m'
BOLD='\033[1m'

# Configuration
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
SKILL_DIR="$(dirname "$SCRIPT_DIR")"
PROJECT_DIR="${LOKI_PROJECT_DIR:-$(pwd)}"
# Normalize PROJECT_DIR (remove trailing slash)
PROJECT_DIR="${PROJECT_DIR%/}"

# Container name includes path hash to avoid collisions between similarly-named projects
# macOS uses md5 instead of md5sum
PROJECT_HASH=$(echo "$PROJECT_DIR" | (md5sum 2>/dev/null || md5 -r 2>/dev/null || echo "$$") | cut -c1-8)
CONTAINER_NAME="loki-sandbox-$(basename "$PROJECT_DIR" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9-]/-/g')-${PROJECT_HASH}"

# Sandbox settings
SANDBOX_IMAGE="${LOKI_SANDBOX_IMAGE:-loki-mode:sandbox}"
SANDBOX_NETWORK="${LOKI_SANDBOX_NETWORK:-bridge}"
SANDBOX_CPUS="${LOKI_SANDBOX_CPUS:-2}"
SANDBOX_MEMORY="${LOKI_SANDBOX_MEMORY:-4g}"
SANDBOX_READONLY="${LOKI_SANDBOX_READONLY:-false}"

# API ports
API_PORT="${LOKI_DASHBOARD_PORT:-57374}"
DASHBOARD_PORT="${LOKI_DASHBOARD_PORT:-57374}"

# Security: Prompt injection disabled by default for enterprise security
PROMPT_INJECTION_ENABLED="${LOKI_PROMPT_INJECTION:-false}"

#===============================================================================
# Docker Credential Mount Presets
#===============================================================================

# Preset definitions: "host_path:container_path:mode"
# Container runs as user 'loki' (UID 1000), so mount to /home/loki/
# Uses functions instead of declare -A for bash 3.2 compatibility (macOS default)
_get_mount_preset() {
    case "$1" in
        gh)        echo "$HOME/.config/gh:/home/loki/.config/gh:ro" ;;
        git)       echo "$HOME/.gitconfig:/home/loki/.gitconfig:ro" ;;
        ssh)       echo "$HOME/.ssh/known_hosts:/home/loki/.ssh/known_hosts:ro" ;;
        aws)       echo "$HOME/.aws:/home/loki/.aws:ro" ;;
        azure)     echo "$HOME/.azure:/home/loki/.azure:ro" ;;
        kube)      echo "$HOME/.kube:/home/loki/.kube:ro" ;;
        terraform) echo "$HOME/.terraform.d:/home/loki/.terraform.d:ro" ;;
        gcloud)    echo "$HOME/.config/gcloud:/home/loki/.config/gcloud:ro" ;;
        npm)       echo "$HOME/.npmrc:/home/loki/.npmrc:ro" ;;
        *)         echo "" ;;
    esac
}

# Environment variables auto-passed per preset (comma-separated)
_get_env_preset() {
    case "$1" in
        aws)       echo "AWS_REGION,AWS_PROFILE,AWS_DEFAULT_REGION" ;;
        azure)     echo "AZURE_SUBSCRIPTION_ID,AZURE_TENANT_ID" ;;
        gcloud)    echo "GOOGLE_PROJECT,GOOGLE_REGION,GCLOUD_PROJECT" ;;
        terraform) echo "TF_VAR_*" ;;
        *)         echo "" ;;
    esac
}

#===============================================================================
# Utility Functions
#===============================================================================

log_info() {
    echo -e "${BLUE}[SANDBOX]${NC} $1"
}

log_success() {
    echo -e "${GREEN}[SANDBOX]${NC} $1"
}

log_warn() {
    echo -e "${YELLOW}[SANDBOX]${NC} $1"
}

log_error() {
    echo -e "${RED}[SANDBOX]${NC} $1" >&2
}

#===============================================================================
# A5: ALLOWED_PATHS + BLOCKED_COMMANDS enforcement (sandbox-local)
#
# sandbox.sh runs as its own process (set -euo pipefail) and does NOT source
# run.sh, so the run.sh helpers are out of scope. These are self-contained
# mirrors that read the same env vars (LOKI_ALLOWED_PATHS / LOKI_BLOCKED_COMMANDS)
# so the sandbox -- the real security boundary -- enforces them at the point
# where run.sh/sandbox.sh actually controls the surface: which host paths get
# bind-mounted, and the operator-supplied `loki sandbox run <cmd>` argv.
#
# HONEST SCOPE: this does NOT restrict provider-driven agent writes inside the
# container (those are contained by cap-drop/seccomp/read-only mounts, not a
# path allowlist). It is defense-in-depth on the surfaces run.sh owns.
#===============================================================================

# Return 0 if target is within LOKI_ALLOWED_PATHS (or the allowlist is unset/
# empty -> no restriction). realpath -m collapses ../ traversal before the
# prefix check, so allowed/../../etc cannot slip past.
_sandbox_path_within_allowed() {
    local target="$1"
    local allow="${LOKI_ALLOWED_PATHS:-}"
    [ -z "$allow" ] && return 0
    [ -z "$target" ] && return 1

    local norm_target
    norm_target="$(realpath -m -- "$target" 2>/dev/null || true)"
    if [ -z "$norm_target" ]; then
        norm_target="$(LOKI_AP_T="$target" python3 -c 'import os; print(os.path.realpath(os.environ["LOKI_AP_T"]))' 2>/dev/null || true)"
    fi
    [ -z "$norm_target" ] && return 1

    local entry norm_entry
    IFS=',' read -ra _AP_ARRAY <<< "$allow"
    for entry in "${_AP_ARRAY[@]}"; do
        entry="${entry#"${entry%%[![:space:]]*}"}"
        entry="${entry%"${entry##*[![:space:]]}"}"
        [ -z "$entry" ] && continue
        # Tilde-expand allowlist entries so "~/proj" matches a resolved $HOME path.
        # SC2088 disabled intentionally: we are pattern-MATCHING a literal "~/" at
        # the start of a config-supplied string (no expansion wanted here), then
        # expanding it ourselves via $HOME. The directive sits in front of the whole
        # if-block (it cannot precede an elif branch -- SC1123).
        # shellcheck disable=SC2088
        if [[ "$entry" == "~/"* ]]; then
            entry="$HOME/${entry#\~/}"
        elif [[ "$entry" == "~" ]]; then
            entry="$HOME"
        fi
        norm_entry="$(realpath -m -- "$entry" 2>/dev/null || true)"
        if [ -z "$norm_entry" ]; then
            norm_entry="$(LOKI_AP_E="$entry" python3 -c 'import os; print(os.path.realpath(os.environ["LOKI_AP_E"]))' 2>/dev/null || true)"
        fi
        [ -z "$norm_entry" ] && continue
        if [ "$norm_target" = "$norm_entry" ] || [ "${norm_target#"${norm_entry}/"}" != "$norm_target" ]; then
            return 0
        fi
    done
    return 1
}

# Return 1 (block) if the command string contains a blocked pattern. Mirrors
# run.sh check_command_allowed; default list matches run.sh's BLOCKED_COMMANDS.
_sandbox_command_allowed() {
    local command="$1"
    # NOTE: the default list is assigned to a separate var first. It cannot be an
    # inline ${VAR:-default} default because the default value contains a literal
    # "}" (the fork-bomb ":(){ :|:& };:"), which would prematurely terminate the
    # ${...} parameter expansion and corrupt the list.
    local _default_blocked='rm -rf /,dd if=,mkfs,:(){ :|:& };:'
    local blocked_list="${LOKI_BLOCKED_COMMANDS:-$_default_blocked}"
    local blocked
    IFS=',' read -ra _BC_ARRAY <<< "$blocked_list"
    for blocked in "${_BC_ARRAY[@]}"; do
        [ -z "$blocked" ] && continue
        if [[ "$command" == *"$blocked"* ]]; then
            log_error "SECURITY: blocked dangerous command (matched '$blocked'): $command"
            return 1
        fi
    done
    return 0
}

# Check if a port is available
check_port_available() {
    local port="$1"
    if command -v lsof &>/dev/null; then
        ! lsof -i ":$port" &>/dev/null
    elif command -v nc &>/dev/null; then
        ! nc -z localhost "$port" 2>/dev/null
    else
        # Assume available if we can't check
        return 0
    fi
}

# Validate project directory
validate_project_dir() {
    if [[ ! -d "$PROJECT_DIR" ]]; then
        log_error "Project directory does not exist: $PROJECT_DIR"
        return 1
    fi
    if [[ ! -r "$PROJECT_DIR" ]]; then
        log_error "Project directory is not readable: $PROJECT_DIR"
        return 1
    fi
    if [[ "$SANDBOX_READONLY" != "true" ]] && [[ ! -w "$PROJECT_DIR" ]]; then
        log_warn "Project directory is not writable: $PROJECT_DIR"
        log_info "Consider using LOKI_SANDBOX_READONLY=true"
    fi
    return 0
}

# Warn about API keys based on provider
warn_missing_api_keys() {
    local provider="$1"
    case "$provider" in
        claude)
            if [[ -z "${ANTHROPIC_API_KEY:-}" ]]; then
                log_warn "ANTHROPIC_API_KEY not set - Claude commands will fail inside container"
            fi
            ;;
        codex)
            if [[ -z "${OPENAI_API_KEY:-}" ]]; then
                log_warn "OPENAI_API_KEY not set - Codex commands will fail inside container"
            fi
            ;;
        gemini)
            if [[ -z "${GOOGLE_API_KEY:-}" ]]; then
                log_warn "GOOGLE_API_KEY not set - Gemini commands will fail inside container"
            fi
            ;;
        cline)
            log_info "Cline manages its own API keys via 'cline auth' - ensure authentication is configured"
            ;;
        aider)
            if [[ -z "${OPENAI_API_KEY:-}" ]] && [[ -z "${ANTHROPIC_API_KEY:-}" ]]; then
                log_warn "No API key set for Aider - set OPENAI_API_KEY or ANTHROPIC_API_KEY"
            fi
            ;;
    esac
}

# Cleanup handler for signals
cleanup_container() {
    log_warn "Interrupted - cleaning up container..."
    docker rm -f "$CONTAINER_NAME" 2>/dev/null || true
    _sandbox_egress_down
    exit 130
}

check_docker() {
    if ! command -v docker &> /dev/null; then
        log_error "Docker not found. Install Docker to use sandbox mode."
        log_error "  macOS: brew install --cask docker"
        log_error "  Linux: curl -fsSL https://get.docker.com | sh"
        exit 1
    fi

    if ! docker info &> /dev/null; then
        log_error "Docker daemon not running. Start Docker Desktop or dockerd."
        exit 1
    fi
}

build_sandbox_image() {
    local dockerfile="$SKILL_DIR/docker/Dockerfile.sandbox"

    if [[ ! -f "$dockerfile" ]]; then
        log_error "Sandbox Dockerfile not found at $dockerfile"
        exit 1
    fi

    log_info "Building sandbox image..."
    docker build -t "$SANDBOX_IMAGE" -f "$dockerfile" "$SKILL_DIR"
    log_success "Sandbox image built: $SANDBOX_IMAGE"
}

ensure_image() {
    if ! docker image inspect "$SANDBOX_IMAGE" &> /dev/null; then
        log_warn "Sandbox image not found. Building..."
        build_sandbox_image
    fi
}

#===============================================================================
# Git Worktree Sandbox (Fallback for non-Docker environments)
#===============================================================================

# Worktree sandbox settings
WORKTREE_PREFIX="loki-sandbox"
WORKTREE_BASE="${LOKI_WORKTREE_BASE:-${TMPDIR:-/tmp}}"
WORKTREE_STATE_FILE="${PROJECT_DIR}/.loki/sandbox/worktree-state.json"

# Check if Docker Desktop Sandbox is available (microVM-based isolation)
is_docker_desktop_sandbox_available() {
    command -v docker &>/dev/null && docker sandbox version &>/dev/null 2>&1
}

# Check if Docker is available (non-fatal version)
is_docker_available() {
    command -v docker &>/dev/null && docker info &>/dev/null 2>&1
}

# Check if git worktree is available
is_git_available() {
    command -v git &>/dev/null && git rev-parse --git-dir &>/dev/null 2>&1
}

# Detect which sandbox mode to use
# Priority: docker-desktop > docker > worktree
detect_sandbox_mode() {
    local requested="${1:-auto}"

    case "$requested" in
        docker-desktop)
            if is_docker_desktop_sandbox_available; then
                echo "docker-desktop"
            else
                log_error "Docker Desktop Sandbox not available (requires Docker Desktop 4.58+)"
                return 1
            fi
            ;;
        docker)
            if is_docker_available; then
                echo "docker"
            else
                log_error "Docker requested but not available"
                return 1
            fi
            ;;
        worktree)
            if is_git_available; then
                echo "worktree"
            else
                log_error "Git not available for worktree sandbox"
                return 1
            fi
            ;;
        auto|*)
            if is_docker_desktop_sandbox_available; then
                echo "docker-desktop"
            elif is_docker_available; then
                echo "docker"
            elif is_git_available; then
                log_warn "Docker not available - using worktree sandbox (soft isolation)"
                echo "worktree"
            else
                log_error "Neither Docker nor Git available for sandbox mode"
                return 1
            fi
            ;;
    esac
}

# Create a worktree sandbox
create_worktree_sandbox() {
    local prd_path="${1:-}"
    local provider="${LOKI_PROVIDER:-claude}"

    local timestamp
    timestamp=$(date +%Y%m%d-%H%M%S)
    local sandbox_name="${WORKTREE_PREFIX}-${timestamp}"
    local sandbox_branch="${WORKTREE_PREFIX}-${timestamp}"
    local sandbox_path="${WORKTREE_BASE}/${sandbox_name}"

    log_warn ""
    log_warn "=========================================="
    log_warn "  WORKTREE SANDBOX - SOFT ISOLATION ONLY"
    log_warn "=========================================="
    log_warn ""
    log_warn "This provides workspace isolation but NOT:"
    log_warn "  - Filesystem isolation (can access any file)"
    log_warn "  - Network isolation (full network access)"
    log_warn "  - Process isolation (no resource limits)"
    log_warn ""
    log_warn "For full isolation, install Docker."
    log_warn ""

    log_info "Creating worktree sandbox..."
    log_info "  Location: $sandbox_path"
    log_info "  Branch:   $sandbox_branch"

    # Check for existing sandbox
    if [[ -f "$WORKTREE_STATE_FILE" ]]; then
        local existing_path
        existing_path=$(jq -r '.sandbox_path // empty' "$WORKTREE_STATE_FILE" 2>/dev/null)
        if [[ -n "$existing_path" ]] && [[ -d "$existing_path" ]]; then
            log_warn "Existing sandbox found: $existing_path"
            log_info "Use 'loki sandbox stop' to stop it first."
            return 1
        fi
    fi

    # Create branch for sandbox
    if ! git branch "$sandbox_branch" HEAD 2>/dev/null; then
        log_error "Failed to create sandbox branch. Are you in a git repository?"
        return 1
    fi

    # Create worktree
    if ! git worktree add "$sandbox_path" "$sandbox_branch" 2>/dev/null; then
        git branch -D "$sandbox_branch" 2>/dev/null
        log_error "Failed to create worktree at $sandbox_path"
        return 1
    fi

    # Set up sandbox environment
    mkdir -p "$sandbox_path/.loki/"{state,logs,signals,queue,memory}

    # Copy essential files
    for file in ".loki/CONTINUITY.md" ".loki/config.yaml" "SKILL.md" "CLAUDE.md"; do
        if [[ -f "$PROJECT_DIR/$file" ]]; then
            mkdir -p "$(dirname "$sandbox_path/$file")"
            cp "$PROJECT_DIR/$file" "$sandbox_path/$file" 2>/dev/null || true
        fi
    done

    # Create sandbox marker
    cat > "$sandbox_path/.loki/SANDBOX_MODE" << EOF
ISOLATION_TYPE=worktree
CREATED_AT=$(date -Iseconds)
PARENT_DIR=$PROJECT_DIR
EOF

    # Save state using python3 for safe JSON encoding (avoids shell injection
    # from paths containing quotes, backslashes, or other special characters)
    mkdir -p "$(dirname "$WORKTREE_STATE_FILE")"
    python3 -c "
import json, sys, datetime
data = {
    'sandbox_path': sys.argv[1],
    'sandbox_branch': sys.argv[2],
    'created_at': datetime.datetime.now().astimezone().isoformat(),
    'provider': sys.argv[3],
    'prd_path': sys.argv[4],
    'status': 'created',
    'isolation_type': 'worktree'
}
with open(sys.argv[5], 'w') as f:
    json.dump(data, f, indent=4)
    f.write('\n')
" "$sandbox_path" "$sandbox_branch" "$provider" "$prd_path" "$WORKTREE_STATE_FILE"

    log_success "Worktree sandbox created: $sandbox_path"
    return 0
}

# Start loki in worktree sandbox
start_worktree_sandbox() {
    local prd_path="${1:-}"
    local provider="${LOKI_PROVIDER:-claude}"

    # Create sandbox if needed
    if ! [[ -f "$WORKTREE_STATE_FILE" ]]; then
        create_worktree_sandbox "$prd_path" || return 1
    fi

    local sandbox_path
    sandbox_path=$(jq -r '.sandbox_path' "$WORKTREE_STATE_FILE")

    if [[ ! -d "$sandbox_path" ]]; then
        log_error "Sandbox path does not exist: $sandbox_path"
        rm -f "$WORKTREE_STATE_FILE"
        return 1
    fi

    log_info "Starting Loki in worktree sandbox..."
    log_info "  Path:     $sandbox_path"
    log_info "  Provider: $provider"

    # Build loki command
    local loki_cmd="$SKILL_DIR/autonomy/run.sh"
    if [[ -n "$prd_path" ]]; then
        if [[ -f "$sandbox_path/$prd_path" ]]; then
            loki_cmd="$loki_cmd $prd_path"
        elif [[ -f "$prd_path" ]]; then
            cp "$prd_path" "$sandbox_path/" 2>/dev/null || true
            loki_cmd="$loki_cmd $(basename "$prd_path")"
        fi
    fi
    loki_cmd="$loki_cmd --provider $provider"

    # Set environment
    export LOKI_SANDBOX_MODE=true
    export LOKI_SANDBOX_TYPE=worktree
    export LOKI_NOTIFICATIONS=false

    log_info ""
    log_info "Commands (in another terminal):"
    log_info "  loki sandbox status      - Check status"
    log_info "  loki sandbox prompt 'msg' - Send prompt"
    log_info "  loki sandbox stop        - Stop sandbox"
    log_info ""

    # Run loki in sandbox directory
    cd "$sandbox_path" && $loki_cmd
}

# Stop worktree sandbox
stop_worktree_sandbox() {
    if [[ ! -f "$WORKTREE_STATE_FILE" ]]; then
        log_warn "No active worktree sandbox found"
        return 0
    fi

    local sandbox_path
    sandbox_path=$(jq -r '.sandbox_path' "$WORKTREE_STATE_FILE")
    local sandbox_branch
    sandbox_branch=$(jq -r '.sandbox_branch' "$WORKTREE_STATE_FILE")

    log_info "Stopping worktree sandbox..."

    # Send stop signal
    if [[ -d "$sandbox_path" ]]; then
        touch "$sandbox_path/.loki/STOP" 2>/dev/null || true
    fi

    # Cleanup
    if [[ "${LOKI_SANDBOX_CLEANUP:-true}" == "true" ]]; then
        log_info "Cleaning up worktree..."

        if [[ -n "$sandbox_path" ]] && [[ -d "$sandbox_path" ]]; then
            git worktree remove "$sandbox_path" --force 2>/dev/null || rm -rf "$sandbox_path" 2>/dev/null
        fi

        if [[ -n "$sandbox_branch" ]]; then
            git branch -D "$sandbox_branch" 2>/dev/null || true
        fi

        git worktree prune 2>/dev/null || true
        rm -f "$WORKTREE_STATE_FILE"

        log_success "Worktree sandbox cleaned up"
    else
        log_info "Sandbox preserved at: $sandbox_path"
        log_info "Run 'loki sandbox cleanup' to remove."
    fi
}

# Worktree sandbox status
worktree_sandbox_status() {
    if [[ ! -f "$WORKTREE_STATE_FILE" ]]; then
        log_info "No active worktree sandbox"
        return 0
    fi

    local sandbox_path
    sandbox_path=$(jq -r '.sandbox_path' "$WORKTREE_STATE_FILE")
    local sandbox_branch
    sandbox_branch=$(jq -r '.sandbox_branch' "$WORKTREE_STATE_FILE")
    local created_at
    created_at=$(jq -r '.created_at' "$WORKTREE_STATE_FILE")

    echo ""
    echo -e "${BOLD}Worktree Sandbox Status${NC}"
    echo "========================"
    echo ""
    echo -e "  Path:    ${CYAN}$sandbox_path${NC}"
    echo -e "  Branch:  $sandbox_branch"
    echo -e "  Created: $created_at"

    if [[ -d "$sandbox_path" ]]; then
        local disk_usage
        disk_usage=$(du -sh "$sandbox_path" 2>/dev/null | cut -f1)
        echo -e "  Disk:    $disk_usage"

        if [[ -f "$sandbox_path/.loki/STOP" ]]; then
            echo -e "  Status:  ${YELLOW}Stopping${NC}"
        else
            echo -e "  Status:  ${GREEN}Active${NC}"
        fi
    else
        echo -e "  Status:  ${RED}Missing${NC}"
    fi

    echo ""
    echo -e "${YELLOW}[SOFT ISOLATION]${NC} - No filesystem/network/process isolation"
    echo ""
}

# Send prompt to worktree sandbox
worktree_sandbox_prompt() {
    local prompt="$*"

    # Security check: prompt injection disabled by default
    if [[ "$PROMPT_INJECTION_ENABLED" != "true" ]]; then
        log_error "Prompt injection is disabled for security"
        log_info ""
        log_info "To enable, set LOKI_PROMPT_INJECTION=true"
        log_info "  Example: LOKI_PROMPT_INJECTION=true loki sandbox prompt 'your message'"
        log_info ""
        log_warn "WARNING: Only enable in trusted environments"
        return 1
    fi

    if [[ -z "$prompt" ]]; then
        log_error "Usage: loki sandbox prompt <your message>"
        return 1
    fi

    if [[ ! -f "$WORKTREE_STATE_FILE" ]]; then
        log_error "No active worktree sandbox"
        return 1
    fi

    local sandbox_path
    sandbox_path=$(jq -r '.sandbox_path' "$WORKTREE_STATE_FILE")

    if [[ ! -d "$sandbox_path" ]]; then
        log_error "Sandbox path does not exist"
        return 1
    fi

    # Use printf to safely write prompt without interpretation
    printf '%s\n' "$prompt" > "$sandbox_path/.loki/HUMAN_INPUT.md"
    log_success "Prompt sent to worktree sandbox"
    log_info "Check $sandbox_path/.loki/logs/ for response"
}

# Cleanup orphaned worktrees
cleanup_worktrees() {
    log_info "Scanning for orphaned sandbox worktrees..."

    local found=0
    while IFS= read -r line; do
        if [[ "$line" == *"$WORKTREE_PREFIX"* ]]; then
            log_info "  Found: $line"
            found=$((found+1))
        fi
    done < <(git worktree list 2>/dev/null)

    if [[ $found -eq 0 ]]; then
        log_info "No sandbox worktrees found"
        return 0
    fi

    log_info "Pruning worktrees..."
    git worktree prune 2>/dev/null || true

    # Clean up orphaned branches
    local branches
    branches=$(git branch --list "${WORKTREE_PREFIX}*" 2>/dev/null)
    while IFS= read -r branch; do
        branch=$(echo "$branch" | tr -d '* ')
        if [[ -n "$branch" ]]; then
            log_info "  Removing branch: $branch"
            git branch -D "$branch" 2>/dev/null || true
        fi
    done <<< "$branches"

    rm -f "$WORKTREE_STATE_FILE" 2>/dev/null
    log_success "Cleanup complete"
}

#===============================================================================
# Docker Desktop Sandbox (microVM-based isolation via Docker Desktop 4.58+)
#===============================================================================
#
# v8.1 DEPRECATION SPLIT (binary-hosting is legacy; isolation is NOT):
# This path creates a microVM from Docker Desktop's "claude" sandbox TEMPLATE,
# which bakes the `claude` CLI binary into the VM so the agent can run inside it.
# With v8's SDK route, the judge/text sites (and, under LOKI_SDK_MODE, the whole
# engine) run pure-HTTPS with only ANTHROPIC_API_KEY -- NO in-VM `claude` binary
# is needed. So the *binary-hosting* purpose of this template is obsolete.
#
# What is NOT obsolete, and matters MORE with an in-process SDK: isolating
# untrusted CODE EXECUTION (npm install / build / test / the generated app).
# In-process SDK judging runs at HOST privilege, so wrapping the code-exec in a
# container/VM is still the right posture. The forward path is the existing
# `start_sandbox` container/worktree isolation (the automatic fallback at the
# `docker sandbox create` failure below); a future release ships a lightweight
# `bun + SDK` sandbox image with no bundled agent binary.
#
# This release: annotation + a one-shot deprecation notice on selection only. No
# behavior change, no removal. See tests/test-sandbox-deprecation.sh.

# Desktop sandbox name follows same pattern as container name
DESKTOP_SANDBOX_NAME="$CONTAINER_NAME"

# Emit a one-shot deprecation notice for the microVM binary-hosting path. Names
# the binary-hosting only and states explicitly that ISOLATION is retained (so it
# is never misread as "the sandbox is going away"). Best-effort + non-blocking:
# telemetry must never abort a sandbox start. Mirrors the cli_command_deprecated
# pattern (event type/source/action + a human log line). Opt out with
# LOKI_SANDBOX_DEPRECATION_QUIET=1.
_desktop_deprecation_notice() {
    [ "${LOKI_SANDBOX_DEPRECATION_QUIET:-0}" = "1" ] && return 0
    log_warn "DEPRECATED: the Docker Desktop microVM 'claude' template hosts the"
    log_warn "  claude BINARY in-VM, which the v8 SDK route no longer needs."
    log_warn "  Code-execution ISOLATION is RETAINED via the container/worktree"
    log_warn "  sandbox (unchanged); only the binary-hosting template is legacy."
    # Best-effort telemetry (never abort the start on a telemetry miss).
    if [ -f "$SKILL_DIR/events/emit.sh" ]; then
        bash "$SKILL_DIR/events/emit.sh" sandbox cli sandbox_microvm_binary_deprecated \
            isolation_retained=true path=docker-desktop >/dev/null 2>&1 || true
    fi
    return 0
}

# Install provider CLI inside sandbox if not pre-installed
_desktop_install_provider_cli() {
    local sandbox_name="$1" provider="$2"
    case "$provider" in
        codex)
            if ! docker sandbox exec "$sandbox_name" which codex &>/dev/null 2>&1; then
                log_info "Installing Codex CLI in sandbox (one-time)..."
                docker sandbox exec "$sandbox_name" npm install -g @openai/codex 2>&1 | tail -1
            fi
            ;;
        gemini)
            if ! docker sandbox exec "$sandbox_name" which gemini &>/dev/null 2>&1; then
                log_info "Installing Gemini CLI in sandbox (one-time)..."
                docker sandbox exec "$sandbox_name" npm install -g @google/gemini-cli 2>&1 | tail -1
            fi
            ;;
        cline)
            if ! docker sandbox exec "$sandbox_name" which cline &>/dev/null 2>&1; then
                log_info "Installing Cline CLI in sandbox (one-time)..."
                docker sandbox exec "$sandbox_name" npm install -g cline 2>&1 | tail -1
            fi
            ;;
        aider)
            if ! docker sandbox exec "$sandbox_name" which aider &>/dev/null 2>&1; then
                log_info "Installing Aider in sandbox (one-time)..."
                docker sandbox exec "$sandbox_name" pip install aider-chat 2>&1 | tail -1
            fi
            ;;
        # claude is pre-installed in the sandbox template
    esac
}

# Build environment variable args for docker sandbox exec
# Uses printf %q to escape values and prevent shell expansion corruption
# (API keys can contain $, !, and other special characters)
_desktop_build_env_args() {
    DESKTOP_ENV_ARGS=()
    local _escaped
    if [[ -n "${ANTHROPIC_API_KEY:-}" ]]; then
        printf -v _escaped '%s' "$ANTHROPIC_API_KEY"
        DESKTOP_ENV_ARGS+=("-e" "ANTHROPIC_API_KEY=$_escaped")
    fi
    if [[ -n "${OPENAI_API_KEY:-}" ]]; then
        printf -v _escaped '%s' "$OPENAI_API_KEY"
        DESKTOP_ENV_ARGS+=("-e" "OPENAI_API_KEY=$_escaped")
    fi
    if [[ -n "${GOOGLE_API_KEY:-}" ]]; then
        printf -v _escaped '%s' "$GOOGLE_API_KEY"
        DESKTOP_ENV_ARGS+=("-e" "GOOGLE_API_KEY=$_escaped")
    fi
    if [[ -n "${GITHUB_TOKEN:-}" ]]; then
        printf -v _escaped '%s' "$GITHUB_TOKEN"
        DESKTOP_ENV_ARGS+=("-e" "GITHUB_TOKEN=$_escaped")
    fi
    if [[ -n "${GH_TOKEN:-}" ]]; then
        printf -v _escaped '%s' "$GH_TOKEN"
        DESKTOP_ENV_ARGS+=("-e" "GH_TOKEN=$_escaped")
    fi
    # Forward all LOKI_* env vars via --env-file to avoid shell expansion issues.
    # Security: LOKI_* values may include secrets, so the env-file is created with
    # mktemp (unpredictable name, no symlink race) and restricted to mode 600 before
    # anything is written into it. The desktop path consumes this file via a
    # foreground/blocking "docker sandbox exec" (see start_docker_desktop_sandbox),
    # so the file is no longer needed once the script exits -- an EXIT trap is the
    # correct cleanup point here. The trap value is baked in (double quotes) so the
    # local path is captured now rather than re-evaluated at exit when it is out of
    # scope. We append our removal so we do not clobber any existing INT/TERM trap.
    local _env_file
    _env_file="$(mktemp "${TMPDIR:-/tmp}/loki-sandbox-env.XXXXXX")"
    chmod 600 "$_env_file"
    local _has_loki_vars=false
    local var
    while IFS= read -r var; do
        if [[ -n "$var" ]]; then
            printf '%s=%s\n' "$var" "${!var}" >> "$_env_file"
            _has_loki_vars=true
        fi
    done < <(compgen -v LOKI_ 2>/dev/null || true)
    if [[ "$_has_loki_vars" == "true" ]] && [[ -f "$_env_file" ]]; then
        DESKTOP_ENV_ARGS+=("--env-file" "$_env_file")
        # shellcheck disable=SC2064
        trap "rm -f -- '$_env_file'" EXIT
    else
        # No LOKI_ vars were forwarded; the file is unused, remove it immediately.
        rm -f -- "$_env_file"
    fi
}

start_docker_desktop_sandbox() {
    local prd_path="${1:-}"
    local provider="${LOKI_PROVIDER:-claude}"

    validate_project_dir || return 1
    warn_missing_api_keys "$provider"

    # v8.1: one-shot deprecation notice for the microVM binary-hosting path
    # (isolation is retained; only the bundled-binary template is legacy).
    _desktop_deprecation_notice

    # Check if sandbox already exists
    local sandbox_exists=false
    if docker sandbox ls 2>/dev/null | grep -q "$DESKTOP_SANDBOX_NAME"; then
        sandbox_exists=true
    fi

    if [[ "$sandbox_exists" == "false" ]]; then
        log_info "Creating Docker Desktop Sandbox (microVM)..."
        log_info "  Workspace: $PROJECT_DIR"
        log_info "  Agent:     claude (provider: $provider)"
        docker sandbox create --name "$DESKTOP_SANDBOX_NAME" claude "$PROJECT_DIR" || {
            log_error "Failed to create Docker Desktop Sandbox"
            log_info "Falling back to Docker container sandbox..."
            start_sandbox "$@"
            return $?
        }
        log_success "Sandbox created: $DESKTOP_SANDBOX_NAME"
    else
        log_info "Using existing sandbox: $DESKTOP_SANDBOX_NAME"
    fi

    # Install provider CLI if needed (codex/gemini not pre-installed)
    _desktop_install_provider_cli "$DESKTOP_SANDBOX_NAME" "$provider"

    # Build environment variable args
    _desktop_build_env_args

    # Build loki command - use SKILL_DIR (the loki-mode install directory) rather than
    # PROJECT_DIR, which is the user's project and may not contain autonomy/run.sh
    local loki_cmd="bash ${SKILL_DIR}/autonomy/run.sh"
    if [[ -n "$prd_path" ]]; then
        local abs_prd
        abs_prd=$(cd "$(dirname "$prd_path")" && pwd)/$(basename "$prd_path")
        loki_cmd="$loki_cmd $abs_prd"
    fi
    loki_cmd="LOKI_PROVIDER=$provider LOKI_SANDBOX_MODE=true LOKI_DASHBOARD=false $loki_cmd"

    log_info ""
    log_info "Starting Loki Mode in Docker Desktop Sandbox..."
    log_info "  Isolation: microVM (hypervisor-based)"
    log_info "  Provider:  $provider"
    log_info "  Sandbox:   $DESKTOP_SANDBOX_NAME"
    log_info "  Workspace: $PROJECT_DIR (synced at same path)"
    log_info ""
    log_info "  Private Docker daemon available inside sandbox"
    log_info "  Dashboard: run 'loki dashboard' on host (reads synced .loki/ files)"
    log_info ""
    log_info "Commands:"
    log_info "  loki sandbox status     - Check sandbox status"
    log_info "  loki sandbox shell      - Open shell in sandbox"
    log_info "  loki sandbox stop       - Stop sandbox"
    log_info ""

    # Execute loki inside sandbox (use -it only when stdin is a terminal)
    local tty_args="-i"
    if [ -t 0 ]; then
        tty_args="-it"
    fi
    docker sandbox exec $tty_args \
        -w "$PROJECT_DIR" \
        ${DESKTOP_ENV_ARGS[@]+"${DESKTOP_ENV_ARGS[@]}"} \
        "$DESKTOP_SANDBOX_NAME" \
        bash -c "$loki_cmd"
}

# shellcheck disable=SC2120
stop_docker_desktop_sandbox() {
    local remove="${1:-false}"

    if ! docker sandbox ls 2>/dev/null | grep -q "$DESKTOP_SANDBOX_NAME"; then
        log_error "Sandbox not found: $DESKTOP_SANDBOX_NAME"
        return 1
    fi

    # Signal loki to stop gracefully
    docker sandbox exec "$DESKTOP_SANDBOX_NAME" \
        bash -c "touch ${PROJECT_DIR}/.loki/STOP 2>/dev/null" 2>/dev/null || true

    log_info "Stopping sandbox: $DESKTOP_SANDBOX_NAME"
    docker sandbox stop "$DESKTOP_SANDBOX_NAME" 2>/dev/null || true

    if [[ "$remove" == "true" ]]; then
        log_info "Removing sandbox: $DESKTOP_SANDBOX_NAME"
        docker sandbox rm "$DESKTOP_SANDBOX_NAME" 2>/dev/null || true
    fi

    log_success "Sandbox stopped"
}

docker_desktop_sandbox_status() {
    local found=false
    while IFS= read -r line; do
        if echo "$line" | grep -q "$DESKTOP_SANDBOX_NAME"; then
            found=true
            echo "$line"
        fi
    done < <(docker sandbox ls 2>/dev/null)

    if [[ "$found" == "false" ]]; then
        log_info "No Docker Desktop Sandbox found for this project"
        log_info "Expected name: $DESKTOP_SANDBOX_NAME"
        return 1
    fi

    # If sandbox is running, get loki status
    if docker sandbox ls 2>/dev/null | grep "$DESKTOP_SANDBOX_NAME" | grep -q "running"; then
        echo ""
        docker sandbox exec -w "$PROJECT_DIR" "$DESKTOP_SANDBOX_NAME" \
            bash -c "bash ${PROJECT_DIR}/autonomy/loki status" 2>/dev/null || true
    fi
}

docker_desktop_sandbox_shell() {
    if ! docker sandbox ls 2>/dev/null | grep "$DESKTOP_SANDBOX_NAME" | grep -q "running"; then
        log_error "Sandbox is not running: $DESKTOP_SANDBOX_NAME"
        log_info "Start it with: loki sandbox start"
        return 1
    fi

    log_info "Opening shell in Docker Desktop Sandbox..."
    docker sandbox exec -it -w "$PROJECT_DIR" "$DESKTOP_SANDBOX_NAME" bash
}

docker_desktop_sandbox_logs() {
    local lines="${1:-100}"
    if ! docker sandbox ls 2>/dev/null | grep -q "$DESKTOP_SANDBOX_NAME"; then
        log_error "Sandbox not found: $DESKTOP_SANDBOX_NAME"
        return 1
    fi

    docker sandbox exec -w "$PROJECT_DIR" "$DESKTOP_SANDBOX_NAME" \
        bash -c "cat .loki/logs/*.log 2>/dev/null | tail -${lines}" || \
        log_warn "No logs found in .loki/logs/"
}

docker_desktop_sandbox_prompt() {
    local message="${1:-}"
    if [[ -z "$message" ]]; then
        log_error "Usage: loki sandbox prompt <message>"
        return 1
    fi

    if ! docker sandbox ls 2>/dev/null | grep "$DESKTOP_SANDBOX_NAME" | grep -q "running"; then
        log_error "Sandbox is not running"
        return 1
    fi

    _desktop_build_env_args
    docker sandbox exec -w "$PROJECT_DIR" \
        ${DESKTOP_ENV_ARGS[@]+"${DESKTOP_ENV_ARGS[@]}"} \
        "$DESKTOP_SANDBOX_NAME" \
        bash -c "printf '%s\n' \"\$1\" > ${PROJECT_DIR}/.loki/HUMAN_INPUT.md" -- "$message"

    log_success "Prompt sent to sandbox"
}

docker_desktop_sandbox_run() {
    local cmd="${*}"
    if [[ -z "$cmd" ]]; then
        log_error "Usage: loki sandbox run <command>"
        return 1
    fi

    if ! docker sandbox ls 2>/dev/null | grep "$DESKTOP_SANDBOX_NAME" | grep -q "running"; then
        log_error "Sandbox is not running"
        return 1
    fi

    _desktop_build_env_args
    docker sandbox exec -w "$PROJECT_DIR" \
        ${DESKTOP_ENV_ARGS[@]+"${DESKTOP_ENV_ARGS[@]}"} \
        "$DESKTOP_SANDBOX_NAME" \
        bash -c "$cmd"
}

#===============================================================================
# Docker Container Sandbox (standard Docker - fallback from Docker Desktop)
#===============================================================================

# Resolve Docker credential mount presets
# Reads from: .loki/config/settings.json dockerMounts, LOKI_DOCKER_MOUNTS env var
# Returns: string of -v and -e flags for docker run
resolve_docker_mounts() {
    RESOLVED_MOUNTS=()

    # Read configured presets (JSON array of strings)
    local presets_json=""
    if [[ -f "${PROJECT_DIR}/.loki/config/settings.json" ]]; then
        presets_json=$(python3 -c "
import json, sys
try:
    with open(sys.argv[1] + '/.loki/config/settings.json') as f:
        print(json.dumps(json.load(f).get('dockerMounts', [])))
except: print('[]')
" "${PROJECT_DIR}" 2>/dev/null || echo "[]")
    fi

    # Override with env var if set
    if [[ -n "${LOKI_DOCKER_MOUNTS:-}" ]]; then
        presets_json="$LOKI_DOCKER_MOUNTS"
    fi

    # Parse and resolve
    if [[ "$presets_json" != "[]" ]] && [[ "$presets_json" != "" ]]; then
        local preset_names
        preset_names=$(python3 -c "
import json, sys
try:
    data = json.loads(sys.argv[1])
    if isinstance(data, list):
        for item in data:
            print(item)
except: pass
" "$presets_json" 2>/dev/null || echo "")

        local name
        while IFS= read -r name; do
            [[ -z "$name" ]] && continue

            local preset_value
            preset_value="$(_get_mount_preset "$name")"
            if [[ -z "$preset_value" ]]; then
                log_warn "Unknown Docker mount preset: $name"
                continue
            fi

            IFS=':' read -ra parts <<< "$preset_value"
            local host_path="${parts[0]}"
            local container_path="${parts[1]}"
            local mode="${parts[2]:-ro}"

            # Safe tilde expansion (no eval) - SC2088 disabled: tilde is intentionally
            # treated as a literal string here for safe expansion without eval
            # shellcheck disable=SC2088
            if [[ "$host_path" == "~/"* ]]; then
                host_path="$HOME/${host_path#\~/}"
            elif [[ "$host_path" == "~" ]]; then
                host_path="$HOME"
            fi

            # Only mount if host path exists
            if [[ -e "$host_path" ]]; then
                RESOLVED_MOUNTS+=("-v" "${host_path}:${container_path}:${mode}")
                log_info "  Mount preset [$name]: $host_path -> $container_path ($mode)"
            fi

            # Add associated env vars
            local env_list
            env_list="$(_get_env_preset "$name")"
            if [[ -n "$env_list" ]]; then
                IFS=',' read -ra env_names <<< "$env_list"
                local env_name
                for env_name in "${env_names[@]}"; do
                    if [[ "$env_name" == *"*" ]]; then
                        # Wildcard: pass all matching env vars
                        local prefix="${env_name%\*}"
                        while IFS='=' read -r key val; do
                            [[ "$key" == "$prefix"* ]] && [[ -n "$val" ]] && {
                                RESOLVED_MOUNTS+=("-e" "$key")
                                log_info "    Env wildcard [$name]: passing $key"
                            }
                        done < <(env)
                    elif [[ -n "${!env_name:-}" ]]; then
                        RESOLVED_MOUNTS+=("-e" "$env_name")
                    fi
                done
            fi
        done <<< "$preset_names"
    fi
}

#-------------------------------------------------------------------------------
# Opt-in egress allowlist (LOKI_SANDBOX_ALLOW_HOSTS, comma-separated hostnames).
# Unset: nothing below runs and the docker run argv is unchanged. Set: the sandbox
# joins an --internal network (no route out) and reaches the world only through a
# filtering proxy sidecar that permits the listed hosts and refuses any target
# resolving to link-local, loopback or private ranges.
#-------------------------------------------------------------------------------
SANDBOX_ALLOW_HOSTS="${LOKI_SANDBOX_ALLOW_HOSTS:-}"
EGRESS_PROXY_PORT=3128
EGRESS_BLOCKED_CIDRS="169.254.0.0/16 fe80::/10 fec0::/10 100.64.0.0/10 10.0.0.0/8 172.16.0.0/12 192.168.0.0/16 127.0.0.0/8 ::1/128 fc00::/7"

# Print the validated allowlist one host per line. Rejects IP literals and
# anything that is not a plain hostname, so a literal metadata IP can never be
# allowlisted. Returns 1 on an invalid entry or an empty list.
_sandbox_egress_hosts() {
    local raw="${1:-$SANDBOX_ALLOW_HOSTS}" h count=0
    local -a parts
    IFS=',' read -ra parts <<< "$raw"
    for h in "${parts[@]}"; do
        h="${h//[[:space:]]/}"
        [[ -z "$h" ]] && continue
        if [[ ! "$h" =~ ^[A-Za-z0-9]([A-Za-z0-9-]*[A-Za-z0-9])?(\.[A-Za-z0-9]([A-Za-z0-9-]*[A-Za-z0-9])?)+$ ]] \
            || [[ "$h" =~ ^[0-9.]+$ ]]; then
            log_error "LOKI_SANDBOX_ALLOW_HOSTS: invalid host (hostnames only, no IPs): $h"
            return 1
        fi
        printf '%s\n' "$h" | tr '[:upper:]' '[:lower:]'
        count=$((count+1))
    done
    [[ $count -gt 0 ]]
}

# Filtering proxy (CONNECT and plain HTTP). Decision function is check_target so
# tests can exercise it with an injected resolver; no network needed.
_sandbox_egress_proxy_source() {
    cat <<'PYEOF'
import ipaddress, os, socket, sys, threading

ALLOW = {h.strip().lower() for h in os.environ.get("ALLOW_HOSTS", "").split(",") if h.strip()}
BLOCKED = [ipaddress.ip_network(c) for c in os.environ.get("BLOCKED_CIDRS", "").split()]


def ip_blocked(ip):
    a = ipaddress.ip_address(ip)
    if getattr(a, "ipv4_mapped", None):
        a = a.ipv4_mapped
    return (not a.is_global or getattr(a, "is_site_local", False) or a.is_private or a.is_link_local or a.is_loopback or a.is_unspecified
            or a.is_multicast or a.is_reserved or any(a in n for n in BLOCKED))


def check_target(host, resolve=None, port=443):
    """Return (ok, reason). Allowed only if host is listed, port is 80 or 443 and every resolved IP is global."""
    if port not in (80, 443):
        return False, "port not allowed"
    host = host.strip().lower().rstrip(".")
    if host not in ALLOW:
        return False, "host not in allowlist"
    resolve = resolve or (lambda h: [i[4][0] for i in socket.getaddrinfo(h, None)])
    try:
        ips = resolve(host)
    except Exception:
        return False, "resolution failed"
    if not ips:
        return False, "no address"
    for ip in ips:
        if ip_blocked(ip):
            return False, "resolves to blocked range " + ip
    return True, ips[0]


def pipe(a, b):
    try:
        while True:
            d = a.recv(65536)
            if not d:
                break
            b.sendall(d)
    except OSError:
        pass
    finally:
        for s in (a, b):
            try:
                s.shutdown(socket.SHUT_RDWR)
            except OSError:
                pass


def handle(c):
    try:
        buf = b""
        while b"\r\n\r\n" not in buf and len(buf) < 65536:
            d = c.recv(4096)
            if not d:
                return
            buf += d
        head, _, rest = buf.partition(b"\r\n\r\n")
        line = head.split(b"\r\n")[0].decode("latin1").split()
        if len(line) < 3:
            return
        method, target = line[0], line[1]
        if method.upper() == "CONNECT":
            host, _, port = target.rpartition(":")
            port = int(port or 443)
            rest_out = b""
        else:
            u = target.split("://", 1)[-1].split("/", 1)[0]
            host, _, port = u.partition(":")
            port = int(port or 80)
            rest_out = buf
        ok, info = check_target(host, port=port)
        if not ok:
            c.sendall(b"HTTP/1.1 403 Forbidden\r\nContent-Length: 0\r\n\r\n")
            return
        up = socket.create_connection((info, port), timeout=20)
        if method.upper() == "CONNECT":
            c.sendall(b"HTTP/1.1 200 Connection established\r\n\r\n")
        else:
            up.sendall(rest_out)
        t = threading.Thread(target=pipe, args=(up, c), daemon=True)
        t.start()
        pipe(c, up)
    except Exception:
        pass
    finally:
        c.close()


def main():
    s = socket.socket()
    s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    s.bind(("0.0.0.0", int(os.environ.get("PROXY_PORT", "3128"))))
    s.listen(128)
    while True:
        conn, _ = s.accept()
        threading.Thread(target=handle, args=(conn,), daemon=True).start()


if __name__ == "__main__":
    main()
PYEOF
}

# Echo the docker run argv fragments the sandbox container needs when the
# allowlist is on: one token per line (network override first, then proxy env).
_sandbox_egress_container_args() {
    local net="$1" proxy="http://${CONTAINER_NAME}-egress:${EGRESS_PROXY_PORT}"
    printf '%s\n' "--network=$net"
    local v
    for v in HTTP_PROXY HTTPS_PROXY http_proxy https_proxy; do
        printf '%s\n' "--env" "$v=$proxy"
    done
    printf '%s\n' "--env" "NO_PROXY=localhost,127.0.0.1"
}

# Echo the docker run argv for the proxy sidecar (test-visible, no side effects).
_sandbox_egress_proxy_args() {
    local script="$1" hosts="$2"
    printf '%s\n' run --detach --name "${CONTAINER_NAME}-egress" \
        --network=bridge --cap-drop=ALL --security-opt=no-new-privileges:true \
        --read-only --pids-limit=128 --memory=256m \
        --env "ALLOW_HOSTS=$hosts" --env "BLOCKED_CIDRS=$EGRESS_BLOCKED_CIDRS" \
        --env "PROXY_PORT=$EGRESS_PROXY_PORT" \
        --volume "$script:/egress-proxy.py:ro" \
        --entrypoint python3 "$SANDBOX_IMAGE" /egress-proxy.py
}

# Create the internal network and proxy sidecar. Echoes the internal network name.
_sandbox_egress_up() {
    local hosts="$1" net="${CONTAINER_NAME}-internal" script
    local dir="${LOKI_RUN_TMP:-}" rec troot
    if [[ -z "$dir" ]]; then
        # Resolve the temp root first (macOS TMPDIR ends in a slash), so the dir is T/loki-egress.X and matches at teardown.
        # mktemp -d picks an unpredictable name with mode 700, so a planted symlink in a shared temp dir is never followed.
        troot="$(cd "${TMPDIR:-/tmp}" 2>/dev/null && pwd -P)" || return 1
        dir="$(mktemp -d "${troot}/loki-egress.XXXXXXXX")" || return 1
        rec="$(_sandbox_egress_record)"
        if ! { mkdir -p "$(dirname "$rec")" && chmod 700 "$(dirname "$rec")" && printf '%s\n' "$dir" > "$rec"; }; then rm -rf -- "$dir"; return 1; fi
    fi
    chmod 700 "$dir" || return 1
    script="$dir/egress-proxy.py"
    _sandbox_egress_proxy_source > "$script"
    docker rm -f "${CONTAINER_NAME}-egress" >/dev/null 2>&1 || true
    docker network rm "$net" >/dev/null 2>&1 || true
    docker network create --internal "$net" >/dev/null || return 1
    local -a pargs=()
    while IFS= read -r line; do pargs+=("$line"); done < <(_sandbox_egress_proxy_args "$script" "$(echo "$hosts" | paste -sd, -)")
    docker "${pargs[@]}" >/dev/null || return 1
    docker network connect --alias "${CONTAINER_NAME}-egress" "$net" "${CONTAINER_NAME}-egress" >/dev/null || return 1
    printf '%s\n' "$net"
}

# Host-side state file naming the fallback egress dir for this container. It lives outside PROJECT_DIR on purpose:
# the project is mounted rw at /workspace, so a record there could be forged by the agent.
_sandbox_egress_record() { printf '%s\n' "${LOKI_SANDBOX_STATE_DIR:-${HOME}/.loki/sandbox-state}/egress-${CONTAINER_NAME}.dir"; }

_sandbox_egress_down() {
    local rec fb root
    rec="$(_sandbox_egress_record)"
    if [[ -f "$rec" && ! -L "$rec" ]]; then
        IFS= read -r fb < "$rec" || fb=""
        root="$(cd "${TMPDIR:-/tmp}" 2>/dev/null && pwd -P)" || root=""
        # Remove only a real, own, unlinked loki-egress.XXXXXXXX directory that sits directly in the resolved temp root.
        if [[ -n "$root" && "$(dirname -- "$fb")" == "$root" ]] \
            && [[ "$(basename -- "$fb")" =~ ^loki-egress\.[A-Za-z0-9]{8}$ ]] \
            && [[ -d "$fb" && ! -L "$fb" && -O "$fb" && "$(cd "$fb" 2>/dev/null && pwd -P)" == "$fb" ]]; then
            rm -rf -- "$fb"
        fi
        rm -f -- "$rec"
    fi
    docker rm -f "${CONTAINER_NAME}-egress" >/dev/null 2>&1 || true
    docker network rm "${CONTAINER_NAME}-internal" >/dev/null 2>&1 || true
}

start_sandbox() {
    # Parse arguments: extract flags before positional args
    local prd_path=""
    local provider="${LOKI_PROVIDER:-claude}"
    local no_mounts=false
    local -a custom_mounts=()

    while [[ $# -gt 0 ]]; do
        case "$1" in
            --mount)
                if [[ -n "${2:-}" ]]; then
                    custom_mounts+=("$2")
                    shift 2
                else
                    log_error "--mount requires HOST:CONTAINER:MODE argument"
                    return 1
                fi
                ;;
            --no-mounts)
                no_mounts=true
                shift
                ;;
            --provider)
                if [[ -n "${2:-}" ]]; then
                    provider="$2"
                    shift 2
                else
                    shift
                fi
                ;;
            *)
                # First non-flag argument is the PRD path
                if [[ -z "$prd_path" ]]; then
                    prd_path="$1"
                fi
                shift
                ;;
        esac
    done

    # Set up signal handler to cleanup on Ctrl+C
    trap cleanup_container INT TERM

    check_docker
    validate_project_dir || return 1
    ensure_image

    # Check port availability (unified dashboard/API port)
    if ! check_port_available "$DASHBOARD_PORT"; then
        log_error "Port $DASHBOARD_PORT is already in use"
        log_info "Set LOKI_DASHBOARD_PORT to use a different port"
        return 1
    fi

    # Warn about missing API keys
    warn_missing_api_keys "$provider"

    # Warn about network=none implications
    if [[ "$SANDBOX_NETWORK" == "none" ]]; then
        log_warn "Network disabled (--network=none)"
        log_warn "  - Git remote operations will fail"
        log_warn "  - Package installations will fail"
        log_warn "  - API calls to AI providers will fail"
    fi

    # Check if already running
    if docker ps --format '{{.Names}}' | grep -q "^${CONTAINER_NAME}$"; then
        log_warn "Sandbox already running: $CONTAINER_NAME"
        log_info "Use 'loki sandbox status' to check or 'loki sandbox stop' to stop"
        return 0
    fi

    # Clean up any stopped container with same name
    docker rm -f "$CONTAINER_NAME" 2>/dev/null || true

    local network_arg="--network=$SANDBOX_NETWORK" egress_args=() egress_ok=1
    if [[ -n "$SANDBOX_ALLOW_HOSTS" ]]; then
        local egress_hosts egress_net
        egress_hosts=$(_sandbox_egress_hosts) || return 1
        if [[ "$SANDBOX_NETWORK" != "bridge" ]]; then
            log_error "LOKI_SANDBOX_ALLOW_HOSTS requires LOKI_SANDBOX_NETWORK=bridge (got $SANDBOX_NETWORK)"
            return 1
        fi
        egress_net=$(_sandbox_egress_up "$egress_hosts") || { _sandbox_egress_down; log_error "Failed to start egress proxy"; return 1; }
        # Any return after this point (refusal, failed docker run) must tear the proxy and network down.
        egress_ok=0
        trap '[[ "${egress_ok:-1}" == 1 ]] || _sandbox_egress_down; trap - RETURN' RETURN
        while IFS= read -r line; do egress_args+=("$line"); done < <(_sandbox_egress_container_args "$egress_net")
        network_arg="${egress_args[0]}"
    fi

    log_info "Starting sandbox container..."
    log_info "  Image:    $SANDBOX_IMAGE"
    log_info "  Project:  $PROJECT_DIR"
    log_info "  Provider: $provider"
    log_info "  Network:  $SANDBOX_NETWORK"
    log_info "  CPUs:     $SANDBOX_CPUS"
    log_info "  Memory:   $SANDBOX_MEMORY"

    # Build docker run command
    local docker_args=(
        "run"
        "--name" "$CONTAINER_NAME"
        "--detach"
        "--interactive"
        "--tty"

        # Resource limits
        "--cpus=$SANDBOX_CPUS"
        "--memory=$SANDBOX_MEMORY"
        "--memory-swap=$SANDBOX_MEMORY"  # Disable swap
        "--pids-limit=256"

        # Security hardening
        "--security-opt=no-new-privileges:true"
        "--cap-drop=ALL"
        "--cap-add=CHOWN"
        # SETUID/SETGID intentionally omitted: container runs as non-root (UID 1000)
        # and should not be able to change UID/GID, which would undermine isolation

        # Network
        "$network_arg"
    )
    if [[ ${#egress_args[@]} -gt 1 ]]; then
        docker_args+=("${egress_args[@]:1}")
        log_info "  Egress:   allowlist only ($(echo "$egress_hosts" | paste -sd, -)); private and link-local ranges blocked"
    fi

    # Add seccomp profile if available
    local seccomp_profile="$SKILL_DIR/autonomy/seccomp-sandbox.json"
    if [[ -f "$seccomp_profile" ]]; then
        docker_args+=("--security-opt" "seccomp=$seccomp_profile")
        log_info "  Seccomp:  enabled"
    fi

    # A5+: workspace-mount allowlist enforcement (opt-in via LOKI_ALLOWED_PATHS).
    #
    # The workspace bind-mount below is where provider-driven agent file writes
    # actually land (the agent writes inside /workspace, which is $PROJECT_DIR on
    # the host). Unlike the custom --mount surface, this mount was previously
    # always made writable regardless of LOKI_ALLOWED_PATHS, so an allowlist that
    # did not include the project dir was silently ignored for the main write
    # surface.
    #
    # When LOKI_ALLOWED_PATHS is set and the workspace itself is OUTSIDE the
    # allowlist, fail closed: refuse to start rather than bind-mount it writable.
    # This is genuinely enforceable because docker mount mode (rw vs ro) and which
    # host paths get bound are decided HERE, before the container exists -- the
    # kernel enforces the resulting mount, not a wrapper check. We refuse rather
    # than auto-downgrade to :ro because a read-only workspace cannot be written
    # by the agent at all and would silently produce a no-op build; refusing makes
    # the misconfiguration visible. (Allowlisted extra paths are still mounted via
    # the custom --mount surface above, which also enforces the allowlist.)
    #
    # When LOKI_ALLOWED_PATHS is empty (default), _sandbox_path_within_allowed
    # returns 0 unconditionally, so this is byte-identical to prior behavior.
    if ! _sandbox_path_within_allowed "$PROJECT_DIR"; then
        log_error "Workspace is outside LOKI_ALLOWED_PATHS, refusing to mount it writable: $PROJECT_DIR"
        log_error "  Add the project directory to LOKI_ALLOWED_PATHS, or unset LOKI_ALLOWED_PATHS to disable path enforcement."
        return 1
    fi

    # Mount project directory
    if [[ "$SANDBOX_READONLY" == "true" ]]; then
        docker_args+=("--volume" "$PROJECT_DIR:/workspace:ro")
        # Need a writable .loki directory - copy existing state to a temp dir so we
        # do not start with an empty volume (which would lose config/state).
        # Security: created with mktemp -d (unpredictable name, no symlink/predictable
        # race) and restricted to mode 700 since it holds a copy of the project's .loki
        # state. Lifecycle: this directory is bind-mounted into a DETACHED container
        # (see "--detach" in docker_args) that outlives this function, so an EXIT trap
        # would delete it out from under the running container. Instead, its path is
        # recorded on the container via a docker label and removed by stop_sandbox()
        # right before the container is removed. The label survives across separate
        # script invocations, which a $$-derived name would not.
        local _loki_state_tmp
        _loki_state_tmp="$(mktemp -d "${TMPDIR:-/tmp}/loki-sandbox-state.XXXXXX")"
        chmod 700 "$_loki_state_tmp"
        if [[ -d "$PROJECT_DIR/.loki" ]]; then
            cp -a "$PROJECT_DIR/.loki/." "$_loki_state_tmp/" 2>/dev/null || true
        fi
        docker_args+=("--volume" "$_loki_state_tmp:/workspace/.loki:rw")
        docker_args+=("--label" "loki.state_dir=$_loki_state_tmp")
    else
        docker_args+=("--volume" "$PROJECT_DIR:/workspace:rw")
    fi

    # Config self-protection: mount critical .loki/ paths as read-only
    # Prevents agents from corrupting council state, config, or audit trail
    if [[ -d "$PROJECT_DIR/.loki/council" ]]; then
        docker_args+=("--volume" "$PROJECT_DIR/.loki/council:/workspace/.loki/council:ro")
    fi
    if [[ -f "$PROJECT_DIR/.loki/config.yaml" ]]; then
        docker_args+=("--volume" "$PROJECT_DIR/.loki/config.yaml:/workspace/.loki/config.yaml:ro")
    fi

    # Mount git config (read-only) - mount to /home/loki since container runs as user loki
    if [[ -f "$HOME/.gitconfig" ]]; then
        docker_args+=("--volume" "$HOME/.gitconfig:/home/loki/.gitconfig:ro")
    fi

    # SSH agent forwarding (more secure than mounting .ssh directory)
    # Only forward if SSH agent is available
    if [[ -n "${SSH_AUTH_SOCK:-}" ]]; then
        docker_args+=("--volume" "$SSH_AUTH_SOCK:/ssh-agent:ro")
        docker_args+=("--env" "SSH_AUTH_SOCK=/ssh-agent")
    elif [[ -d "$HOME/.ssh" ]]; then
        # Fallback: mount only known_hosts and public keys (NOT private keys)
        if [[ -f "$HOME/.ssh/known_hosts" ]]; then
            docker_args+=("--volume" "$HOME/.ssh/known_hosts:/home/loki/.ssh/known_hosts:ro")
        fi
        log_warn "SSH agent not available. Git operations may require manual auth."
    fi

    # Mount GitHub CLI config (read-only) - mount to /home/loki since container runs as user loki
    if [[ -d "$HOME/.config/gh" ]]; then
        docker_args+=("--volume" "$HOME/.config/gh:/home/loki/.config/gh:ro")
    fi

    # Apply Docker credential mount presets (additive on top of defaults above)
    if [[ "$no_mounts" != "true" ]] && [[ "${LOKI_NO_DOCKER_MOUNTS:-}" != "true" ]]; then
        resolve_docker_mounts
        if [[ ${#RESOLVED_MOUNTS[@]} -gt 0 ]]; then
            docker_args+=("${RESOLVED_MOUNTS[@]}")
        fi
    fi

    # Apply custom --mount arguments
    local custom_mount
    for custom_mount in "${custom_mounts[@]+"${custom_mounts[@]}"}"; do
        if [[ -n "$custom_mount" ]]; then
            IFS=':' read -ra mount_parts <<< "$custom_mount"
            local c_host="${mount_parts[0]:-}"
            local c_container="${mount_parts[1]:-}"
            local c_mode="${mount_parts[2]:-ro}"
            if [[ -n "$c_host" ]] && [[ -n "$c_container" ]]; then
                # Safe tilde expansion (no eval) - SC2088 disabled: intentional literal match
                # shellcheck disable=SC2088
                if [[ "$c_host" == "~/"* ]]; then
                    c_host="$HOME/${c_host#\~/}"
                elif [[ "$c_host" == "~" ]]; then
                    c_host="$HOME"
                fi
                if [[ -e "$c_host" ]]; then
                    # A5: when LOKI_ALLOWED_PATHS is set, reject a custom mount
                    # whose host path is outside the allowlist. This is a write
                    # surface run.sh/sandbox.sh genuinely controls (we decide what
                    # to bind into the container), so the allowlist is enforced
                    # here rather than only documented.
                    if ! _sandbox_path_within_allowed "$c_host"; then
                        log_error "Custom mount host path is outside LOKI_ALLOWED_PATHS, refusing to mount: $c_host"
                        return 1
                    fi
                    docker_args+=("--volume" "${c_host}:${c_container}:${c_mode}")
                    log_info "  Custom mount: $c_host -> $c_container ($c_mode)"
                else
                    log_warn "Custom mount source does not exist: $c_host"
                fi
            else
                log_warn "Invalid mount format: $custom_mount (expected HOST:CONTAINER[:MODE])"
            fi
        fi
    done

    # Pass API keys as environment variables (more secure than mounting files)
    if [[ -n "${ANTHROPIC_API_KEY:-}" ]]; then
        docker_args+=("--env" "ANTHROPIC_API_KEY")
    fi
    if [[ -n "${OPENAI_API_KEY:-}" ]]; then
        docker_args+=("--env" "OPENAI_API_KEY")
    fi
    if [[ -n "${GOOGLE_API_KEY:-}" ]]; then
        docker_args+=("--env" "GOOGLE_API_KEY")
    fi
    if [[ -n "${GITHUB_TOKEN:-}" ]]; then
        docker_args+=("--env" "GITHUB_TOKEN")
    fi
    if [[ -n "${GH_TOKEN:-}" ]]; then
        docker_args+=("--env" "GH_TOKEN")
    fi

    # Loki configuration
    docker_args+=(
        "--env" "LOKI_PROVIDER=$provider"
        "--env" "LOKI_SANDBOX_MODE=true"
        "--env" "LOKI_NOTIFICATIONS=false"
        "--env" "LOKI_DASHBOARD=true"
    )

    # Expose ports
    docker_args+=(
        "--publish" "$API_PORT:57374"
    )

    # Expose additional ports for testing (e.g., LOKI_EXTRA_PORTS="3000:3000,8080:8080")
    if [[ -n "${LOKI_EXTRA_PORTS:-}" ]]; then
        IFS=',' read -ra EXTRA_PORTS <<< "$LOKI_EXTRA_PORTS"
        for port_mapping in "${EXTRA_PORTS[@]}"; do
            docker_args+=("--publish" "$port_mapping")
        done
    fi

    # Working directory
    docker_args+=("--workdir" "/workspace")

    # Override entrypoint to use bash (Dockerfile has loki as entrypoint)
    docker_args+=("--entrypoint" "/bin/bash")

    # Image and command
    docker_args+=("$SANDBOX_IMAGE")

    # Build loki command
    local loki_cmd="loki start"
    if [[ -n "$prd_path" ]]; then
        # Convert to container path (handle paths with spaces)
        # macOS realpath does not support --relative-to, so use Python fallback
        local relative_prd
        relative_prd=$(realpath --relative-to="$PROJECT_DIR" "$prd_path" 2>/dev/null \
            || python3 -c "import os.path; print(os.path.relpath('$prd_path', '$PROJECT_DIR'))" 2>/dev/null \
            || basename "$prd_path")
        local container_prd="/workspace/${relative_prd}"
        # Quote path to handle spaces
        loki_cmd="$loki_cmd \"$container_prd\""
    fi
    loki_cmd="$loki_cmd --provider $provider"

    docker_args+=("-c" "$loki_cmd")

    # Run container
    local container_id
    container_id=$(docker "${docker_args[@]}") || { log_error "docker run failed"; return 1; }
    egress_ok=1

    log_success "Sandbox started: ${container_id:0:12}"
    log_info ""
    log_info "Access:"
    log_info "  Dashboard: http://localhost:$DASHBOARD_PORT"
    log_info "  API:       http://localhost:$API_PORT"
    if [[ -n "${LOKI_EXTRA_PORTS:-}" ]]; then
        log_info "  Extra:     $LOKI_EXTRA_PORTS"
    fi
    log_info ""
    log_info "Commands:"
    log_info "  loki sandbox logs       - View logs"
    log_info "  loki sandbox shell      - Open shell in container"
    log_info "  loki sandbox stop       - Stop sandbox"
    log_info ""
    log_info "Testing (when ready):"
    log_info "  loki sandbox phase      - Check SDLC phase & testing tips"
    log_info "  loki sandbox test       - Run tests"
    log_info "  loki sandbox serve      - Start dev server"
    log_info "  loki sandbox prompt 'msg' - Send real-time prompt"
}

stop_sandbox() {
    check_docker

    if docker ps --format '{{.Names}}' | grep -q "^${CONTAINER_NAME}$"; then
        log_info "Stopping sandbox: $CONTAINER_NAME"

        # Read the read-only-mode temp state dir path recorded as a container label
        # (see start_sandbox H5 handling). It is removed once the container is gone.
        local _state_dir=""
        _state_dir=$(docker inspect --format '{{ index .Config.Labels "loki.state_dir" }}' "$CONTAINER_NAME" 2>/dev/null || true)

        # Try graceful stop first (touch STOP file)
        docker exec "$CONTAINER_NAME" touch /workspace/.loki/STOP 2>/dev/null || true

        # Wait for graceful shutdown (check every second for up to 10 seconds)
        local waited=0
        while [ $waited -lt 10 ]; do
            if ! docker ps --format '{{.Names}}' | grep -q "^${CONTAINER_NAME}$"; then
                log_success "Sandbox stopped gracefully"
                _sandbox_egress_down
                # Remove the temp state dir now that the container has stopped.
                if [[ -n "$_state_dir" ]] && [[ -d "$_state_dir" ]]; then
                    rm -rf -- "$_state_dir" 2>/dev/null || true
                fi
                return 0
            fi
            sleep 1
            waited=$((waited+1))
        done

        # Force stop if still running
        log_info "Force stopping container..."
        docker stop --time 5 "$CONTAINER_NAME" 2>/dev/null || true
        docker rm -f "$CONTAINER_NAME" 2>/dev/null || true

        # Remove the temp state dir now that the container is gone.
        if [[ -n "$_state_dir" ]] && [[ -d "$_state_dir" ]]; then
            rm -rf -- "$_state_dir" 2>/dev/null || true
        fi

        _sandbox_egress_down
        log_success "Sandbox stopped"
    else
        _sandbox_egress_down
        log_warn "No running sandbox found"
    fi
}

# Send a prompt/directive to the running sandbox
sandbox_prompt() {
    local prompt="$*"

    # Security check: prompt injection disabled by default
    if [[ "$PROMPT_INJECTION_ENABLED" != "true" ]]; then
        log_error "Prompt injection is disabled for security"
        log_info ""
        log_info "To enable, set LOKI_PROMPT_INJECTION=true"
        log_info "  Example: LOKI_PROMPT_INJECTION=true loki sandbox prompt 'your message'"
        log_info ""
        log_warn "WARNING: Only enable in trusted environments"
        return 1
    fi

    if [[ -z "$prompt" ]]; then
        log_error "Usage: loki sandbox prompt <your message>"
        log_info "Example: loki sandbox prompt 'start the dev server and show me the URL'"
        return 1
    fi

    check_docker

    if ! docker ps --format '{{.Names}}' | grep -q "^${CONTAINER_NAME}$"; then
        log_error "Sandbox is not running. Start it first with: loki sandbox start"
        return 1
    fi

    log_info "Sending prompt to sandbox..."

    # Write to HUMAN_INPUT.md inside the container
    # Use heredoc to avoid command injection via single quotes in prompt
    docker exec "$CONTAINER_NAME" bash -c 'cat > /workspace/.loki/HUMAN_INPUT.md' <<< "$prompt"

    log_success "Prompt sent. Loki will process it in the next iteration."
    log_info ""
    log_info "Watch the response with: loki sandbox logs"
    log_info "Or check status with: loki sandbox status"
}

# Run a command inside the sandbox and show output
sandbox_run() {
    local cmd="$*"

    if [[ -z "$cmd" ]]; then
        log_error "Usage: loki sandbox run <command>"
        log_info "Example: loki sandbox run 'npm run dev'"
        return 1
    fi

    # A5: defense-in-depth on the operator-supplied command. The container is the
    # real boundary, but an obviously-destructive command is blocked here before
    # it is dispatched into the sandbox (honors LOKI_BLOCKED_COMMANDS).
    if ! _sandbox_command_allowed "$cmd"; then
        return 1
    fi

    check_docker

    if ! docker ps --format '{{.Names}}' | grep -q "^${CONTAINER_NAME}$"; then
        log_error "Sandbox is not running"
        return 1
    fi

    log_info "Running command in sandbox: $cmd"
    # Trust contract: $cmd is the operator's CLI argv (`loki sandbox run <cmd>`),
    # and the operator is the same person who is running the sandbox container
    # on their own machine. Shell metacharacters are intentionally allowed so
    # the operator can run pipelines, redirects, etc. inside the sandbox -- but
    # the sandbox is the security boundary, not this shell-out. We:
    #   1. set the working dir via `docker exec -w` instead of `cd && $cmd`,
    #      so the user command cannot break out of the cd via metacharacters
    #      that confuse the wrapper (e.g. unmatched quotes affecting cd);
    #   2. pass `--` to bash so the user string cannot be re-interpreted as a
    #      bash flag if it begins with `-`;
    #   3. pass the user string as a separate argv element to bash -lc so it
    #      is treated as the script body, not concatenated into a wrapper.
    docker exec -it -w /workspace "$CONTAINER_NAME" bash -lc -- "$cmd"
}

# Start a dev server inside the sandbox
sandbox_serve() {
    local port="${1:-3000}"

    check_docker

    if ! docker ps --format '{{.Names}}' | grep -q "^${CONTAINER_NAME}$"; then
        log_error "Sandbox is not running"
        return 1
    fi

    log_info "Looking for dev server configuration..."

    # Detect project type and start appropriate server
    local serve_cmd=""

    if docker exec "$CONTAINER_NAME" test -f /workspace/package.json; then
        # Check for common dev server scripts
        local has_dev
        has_dev=$(docker exec "$CONTAINER_NAME" jq -r '.scripts.dev // empty' /workspace/package.json 2>/dev/null)
        local has_start
        has_start=$(docker exec "$CONTAINER_NAME" jq -r '.scripts.start // empty' /workspace/package.json 2>/dev/null)

        if [[ -n "$has_dev" ]]; then
            serve_cmd="npm run dev"
        elif [[ -n "$has_start" ]]; then
            serve_cmd="npm start"
        fi
    elif docker exec "$CONTAINER_NAME" test -f /workspace/requirements.txt; then
        # Python project
        if docker exec "$CONTAINER_NAME" test -f /workspace/manage.py; then
            serve_cmd="python manage.py runserver 0.0.0.0:$port"
        elif docker exec "$CONTAINER_NAME" test -f /workspace/app.py; then
            serve_cmd="python app.py"
        fi
    fi

    if [[ -z "$serve_cmd" ]]; then
        log_warn "Could not auto-detect dev server"
        log_info "Try: loki sandbox run 'your-dev-command'"
        return 1
    fi

    log_success "Starting dev server: $serve_cmd"
    log_info ""

    # Warn if the dev server port was not published when the container started
    local port_published
    port_published=$(docker port "$CONTAINER_NAME" "$port" 2>/dev/null || true)
    if [[ -z "$port_published" ]]; then
        log_warn "Port $port is NOT published to the host."
        log_warn "The dev server will run inside the container but will not be accessible from localhost."
        log_warn "To fix, restart the sandbox with: LOKI_EXTRA_PORTS='$port:$port' loki sandbox start"
        log_info ""
    fi

    log_info "Access the app at:"
    log_info "  http://localhost:$port"
    log_info ""
    log_info "Press Ctrl+C to stop the server (sandbox continues running)"

    docker exec -it "$CONTAINER_NAME" bash -c "cd /workspace && $serve_cmd"
}

# Run tests inside the sandbox
sandbox_test() {
    local test_type="${1:-all}"

    check_docker

    if ! docker ps --format '{{.Names}}' | grep -q "^${CONTAINER_NAME}$"; then
        log_error "Sandbox is not running"
        return 1
    fi

    log_info "Running tests in sandbox..."

    # Detect project type and test command
    local test_cmd=""

    if docker exec "$CONTAINER_NAME" test -f /workspace/package.json; then
        case "$test_type" in
            unit)
                test_cmd=$(docker exec "$CONTAINER_NAME" jq -r '.scripts["test:unit"] // .scripts.test // "npm test"' /workspace/package.json 2>/dev/null)
                ;;
            integration)
                test_cmd=$(docker exec "$CONTAINER_NAME" jq -r '.scripts["test:integration"] // empty' /workspace/package.json 2>/dev/null)
                [[ -z "$test_cmd" ]] && test_cmd="npm run test:integration"
                ;;
            e2e)
                if docker exec "$CONTAINER_NAME" test -d /workspace/node_modules/.bin/playwright; then
                    test_cmd="npx playwright test"
                elif docker exec "$CONTAINER_NAME" test -d /workspace/node_modules/.bin/cypress; then
                    test_cmd="npx cypress run"
                else
                    test_cmd=$(docker exec "$CONTAINER_NAME" jq -r '.scripts["test:e2e"] // empty' /workspace/package.json 2>/dev/null)
                fi
                ;;
            all|*)
                test_cmd=$(docker exec "$CONTAINER_NAME" jq -r '.scripts.test // "npm test"' /workspace/package.json 2>/dev/null)
                ;;
        esac
    elif docker exec "$CONTAINER_NAME" test -f /workspace/requirements.txt; then
        case "$test_type" in
            unit)
                test_cmd="pytest tests/unit/ -v"
                ;;
            integration)
                test_cmd="pytest tests/integration/ -v"
                ;;
            e2e)
                test_cmd="pytest tests/e2e/ -v"
                ;;
            all|*)
                test_cmd="pytest -v"
                ;;
        esac
    elif docker exec "$CONTAINER_NAME" test -f /workspace/Cargo.toml; then
        test_cmd="cargo test"
    elif docker exec "$CONTAINER_NAME" test -f /workspace/go.mod; then
        test_cmd="go test ./..."
    fi

    if [[ -z "$test_cmd" ]] || [[ "$test_cmd" == "null" ]]; then
        log_warn "Could not auto-detect test command"
        log_info "Supported test types: unit, integration, e2e, all"
        log_info "Or run: loki sandbox run 'your-test-command'"
        return 1
    fi

    log_success "Running: $test_cmd"
    docker exec -it "$CONTAINER_NAME" bash -c "cd /workspace && $test_cmd"
}

# Check SDLC phase and suggest testing
sandbox_phase() {
    check_docker

    if ! docker ps --format '{{.Names}}' | grep -q "^${CONTAINER_NAME}$"; then
        log_error "Sandbox is not running"
        return 1
    fi

    # Get current phase from orchestrator state
    local phase
    phase=$(docker exec "$CONTAINER_NAME" bash -c \
        "python3 -c \"import json; print(json.load(open('/workspace/.loki/state/orchestrator.json')).get('currentPhase', 'UNKNOWN'))\" 2>/dev/null" \
        || echo "UNKNOWN")

    echo ""
    echo -e "${BOLD}Current SDLC Phase: ${CYAN}$phase${NC}"
    echo ""

    case "$phase" in
        BOOTSTRAP|DISCOVERY|ARCHITECTURE)
            log_info "Phase $phase - No testing required yet"
            log_info "Testing begins in DEVELOPMENT phase"
            ;;
        INFRASTRUCTURE)
            log_info "Phase $phase - Infrastructure testing"
            echo "  Recommended:"
            echo "    loki sandbox run 'docker-compose up -d'"
            echo "    loki sandbox run 'terraform plan'"
            ;;
        DEVELOPMENT)
            log_info "Phase $phase - Development testing recommended"
            echo ""
            echo "  Run dev server:"
            echo "    loki sandbox serve"
            echo ""
            echo "  Run unit tests:"
            echo "    loki sandbox test unit"
            echo ""
            echo "  Manual testing:"
            echo "    loki sandbox shell"
            ;;
        QA)
            log_info "Phase $phase - Full testing required"
            echo ""
            echo "  Run all tests:"
            echo "    loki sandbox test all"
            echo ""
            echo "  Run specific test suites:"
            echo "    loki sandbox test unit"
            echo "    loki sandbox test integration"
            echo "    loki sandbox test e2e"
            echo ""
            echo "  Interactive testing:"
            echo "    loki sandbox serve"
            echo "    loki sandbox shell"
            echo ""
            echo "  To report issues during testing:"
            echo "    loki sandbox prompt 'Found bug: describe the bug here'"
            ;;
        DEPLOYMENT)
            log_info "Phase $phase - Smoke testing"
            echo ""
            echo "  Run smoke tests:"
            echo "    loki sandbox run 'npm run test:smoke'"
            echo ""
            echo "  Check deployment status:"
            echo "    loki sandbox run 'curl -s http://localhost:3000/health'"
            ;;
        GROWTH|*)
            log_info "Phase $phase - Continuous testing"
            echo ""
            echo "  Run regression tests:"
            echo "    loki sandbox test all"
            echo ""
            echo "  Performance testing:"
            echo "    loki sandbox run 'npx k6 run tests/load.js'"
            ;;
    esac

    echo ""
}

# Expose additional ports (for development testing)
sandbox_expose() {
    local port="${1:-}"

    if [[ -z "$port" ]]; then
        log_error "Usage: loki sandbox expose <port>"
        log_info "Example: loki sandbox expose 8080"
        return 1
    fi

    check_docker

    if ! docker ps --format '{{.Names}}' | grep -q "^${CONTAINER_NAME}$"; then
        log_error "Sandbox is not running"
        log_info "Note: Ports must be exposed when starting the sandbox"
        log_info "Set LOKI_EXTRA_PORTS='$port:$port' before running 'loki sandbox start'"
        return 1
    fi

    log_warn "Cannot expose ports on running container"
    log_info ""
    log_info "To expose additional ports, restart sandbox with:"
    echo ""
    echo "  LOKI_EXTRA_PORTS='$port:$port' loki sandbox start"
    echo ""
    log_info "Or access via sandbox shell:"
    echo "  loki sandbox shell"
    echo "  # Then run your server inside the container"
}

sandbox_status() {
    check_docker

    if docker ps --format '{{.Names}}' | grep -q "^${CONTAINER_NAME}$"; then
        log_success "Sandbox is running: $CONTAINER_NAME"
        echo ""
        docker ps --filter "name=$CONTAINER_NAME" --format "table {{.ID}}\t{{.Status}}\t{{.Ports}}"
        echo ""

        # Try to get loki status
        log_info "Loki Status:"
        docker exec "$CONTAINER_NAME" loki status 2>/dev/null || log_warn "Could not get loki status"
    else
        log_warn "Sandbox is not running"

        # Check for stopped container
        if docker ps -a --format '{{.Names}}' | grep -q "^${CONTAINER_NAME}$"; then
            log_info "Stopped container exists. Use 'loki sandbox start' to restart."
        fi
    fi
}

sandbox_logs() {
    local lines="${1:-100}"
    check_docker

    if docker ps --format '{{.Names}}' | grep -q "^${CONTAINER_NAME}$"; then
        docker logs --tail "$lines" -f "$CONTAINER_NAME"
    else
        log_error "Sandbox is not running"
        exit 1
    fi
}

sandbox_shell() {
    check_docker

    if docker ps --format '{{.Names}}' | grep -q "^${CONTAINER_NAME}$"; then
        log_info "Opening shell in sandbox..."
        docker exec -it "$CONTAINER_NAME" bash
    else
        log_error "Sandbox is not running"
        exit 1
    fi
}

sandbox_build() {
    check_docker
    build_sandbox_image
}

show_help() {
    echo -e "${BOLD}Loki Mode Sandbox${NC}"
    echo ""
    echo "Usage: loki sandbox <command> [options]"
    echo ""
    echo "Commands:"
    echo "  start [PRD]      Start sandbox with optional PRD"
    echo "  stop             Stop running sandbox"
    echo "  status           Check sandbox status"
    echo "  logs [N]         View last N log lines (default: 100)"
    echo "  shell            Open bash shell in sandbox"
    echo "  build            Build/rebuild sandbox image"
    echo "  cleanup          Remove orphaned sandbox worktrees/branches"
    echo ""
    echo "Interactive Commands (while sandbox is running):"
    echo "  prompt <msg>     Send a prompt/directive to Loki in real-time"
    echo "  run <cmd>        Run a command inside the sandbox"
    echo "  serve [port]     Auto-detect and start dev server (default port: 3000)"
    echo ""
    echo "Testing Commands:"
    echo "  test [type]      Run tests (type: unit, integration, e2e, all)"
    echo "  phase            Check SDLC phase and get testing suggestions"
    echo "  expose <port>    Show how to expose additional ports"
    echo ""
    echo "Mode Options:"
    echo "  --docker-desktop Force Docker Desktop Sandbox (microVM isolation)"
    echo "  --docker         Force Docker container sandbox (seccomp isolation)"
    echo "  --worktree       Force git worktree sandbox (soft isolation)"
    echo "  --auto           Auto-detect best mode (default)"
    echo ""
    echo "Sandbox Modes (priority order):"
    echo "  Docker Desktop   - microVM isolation via 'docker sandbox' (Docker Desktop 4.58+)"
    echo "    (default)        Hypervisor-based VM, private Docker daemon, pre-installed CLIs"
    echo "  Docker Container - Container isolation with seccomp, dropped capabilities,"
    echo "    (fallback)       resource limits, network control (Dockerfile.sandbox)"
    echo "  Worktree         - Git worktree isolation (last resort if no Docker)"
    echo "    (last resort)    Warning: No filesystem/network/process isolation"
    echo ""
    echo "Environment Variables:"
    echo "  LOKI_SANDBOX_IMAGE    Docker image (default: loki-mode:sandbox)"
    echo "  LOKI_SANDBOX_NETWORK  Network mode: bridge, none, host (default: bridge)"
    echo "  LOKI_SANDBOX_ALLOW_HOSTS  Egress allowlist, comma-separated hostnames (default: unset)"
    echo "  LOKI_SANDBOX_CPUS     CPU limit (default: 2)"
    echo "  LOKI_SANDBOX_MEMORY   Memory limit (default: 4g)"
    echo "  LOKI_SANDBOX_READONLY Mount project read-only (default: false)"
    echo "  LOKI_SANDBOX_CLEANUP  Auto-cleanup worktree on stop (default: true)"
    echo "  LOKI_EXTRA_PORTS      Expose extra ports (e.g., '3000:3000,8080:8080')"
    echo "  LOKI_PROMPT_INJECTION Enable real-time prompts (default: false)"
    echo ""
    echo "Security Features (Docker mode):"
    echo "  - Seccomp profile restricts syscalls"
    echo "  - No new privileges flag"
    echo "  - Dropped capabilities"
    echo "  - Resource limits (CPU, memory, PIDs)"
    echo "  - API keys passed as env vars (not mounted)"
    echo "  - Prompt injection DISABLED by default (enterprise security)"
    echo ""
    echo "Examples:"
    echo "  loki sandbox start                              # Start (auto-detect: Desktop > Docker > Worktree)"
    echo "  loki sandbox start --docker-desktop ./prd.md   # Force Docker Desktop microVM"
    echo "  loki sandbox start --docker ./prd.md           # Force Docker container mode"
    echo "  loki sandbox start --worktree                   # Force worktree mode"
    echo "  loki sandbox prompt 'start the app and show URL'  # Send prompt"
    echo "  loki sandbox serve                              # Start dev server"
    echo "  loki sandbox test                               # Run all tests"
    echo "  loki sandbox test unit                          # Run unit tests only"
    echo "  loki sandbox phase                              # Check phase, get testing tips"
    echo "  loki sandbox run 'npm test'                     # Run custom command"
    echo "  loki sandbox cleanup                            # Remove old worktrees"
}

#===============================================================================
# Main
#===============================================================================

main() {
    local command="${1:-help}"
    shift || true

    # Parse mode option
    local mode="auto"
    local args=()
    while [[ $# -gt 0 ]]; do
        case "$1" in
            --docker-desktop) mode="docker-desktop"; shift ;;
            --docker) mode="docker"; shift ;;
            --worktree) mode="worktree"; shift ;;
            --auto) mode="auto"; shift ;;
            *) args+=("$1"); shift ;;
        esac
    done

    # Detect sandbox mode for commands that need it
    # For stop/status/etc, read persisted mode first so we stop the correct sandbox type
    local sandbox_mode=""
    local _mode_file="${PROJECT_DIR}/.loki/sandbox/mode"
    case "$command" in
        start)
            sandbox_mode=$(detect_sandbox_mode "$mode") || exit 1
            # Persist the detected mode so stop/status use the same mode
            mkdir -p "$(dirname "$_mode_file")"
            echo "$sandbox_mode" > "$_mode_file"
            ;;
        stop|status|prompt|shell|logs|run|serve|test|phase)
            # Read persisted mode if no explicit mode override was given
            if [[ "$mode" == "auto" ]] && [[ -f "$_mode_file" ]]; then
                sandbox_mode=$(cat "$_mode_file" 2>/dev/null || true)
            fi
            # Fall back to detection if no persisted mode
            if [[ -z "$sandbox_mode" ]]; then
                sandbox_mode=$(detect_sandbox_mode "$mode") || exit 1
            fi
            ;;
    esac

    case "$command" in
        start)
            if [[ "$sandbox_mode" == "docker-desktop" ]]; then
                start_docker_desktop_sandbox ${args[@]+"${args[@]}"}
            elif [[ "$sandbox_mode" == "docker" ]]; then
                start_sandbox ${args[@]+"${args[@]}"}
            else
                start_worktree_sandbox ${args[@]+"${args[@]}"}
            fi
            ;;
        stop)
            if [[ "$sandbox_mode" == "docker-desktop" ]]; then
                stop_docker_desktop_sandbox
            elif [[ "$sandbox_mode" == "docker" ]]; then
                stop_sandbox
            else
                stop_worktree_sandbox
            fi
            ;;
        status)
            if [[ "$sandbox_mode" == "docker-desktop" ]]; then
                docker_desktop_sandbox_status
            elif [[ "$sandbox_mode" == "docker" ]]; then
                sandbox_status
            else
                worktree_sandbox_status
            fi
            ;;
        logs)
            if [[ "$sandbox_mode" == "docker-desktop" ]]; then
                docker_desktop_sandbox_logs ${args[@]+"${args[@]}"}
            else
                sandbox_logs ${args[@]+"${args[@]}"}
            fi
            ;;
        shell)
            if [[ "$sandbox_mode" == "docker-desktop" ]]; then
                docker_desktop_sandbox_shell
            else
                sandbox_shell
            fi
            ;;
        build)
            sandbox_build
            ;;
        prompt)
            if [[ "$sandbox_mode" == "docker-desktop" ]]; then
                docker_desktop_sandbox_prompt ${args[@]+"${args[@]}"}
            elif [[ "$sandbox_mode" == "docker" ]]; then
                sandbox_prompt ${args[@]+"${args[@]}"}
            else
                worktree_sandbox_prompt ${args[@]+"${args[@]}"}
            fi
            ;;
        run)
            if [[ "$sandbox_mode" == "docker-desktop" ]]; then
                docker_desktop_sandbox_run ${args[@]+"${args[@]}"}
            else
                sandbox_run ${args[@]+"${args[@]}"}
            fi
            ;;
        cleanup)
            cleanup_worktrees
            ;;
        serve)
            if [[ "$sandbox_mode" == "docker-desktop" ]]; then
                docker_desktop_sandbox_run "npm start 2>/dev/null || python3 -m http.server ${args[0]:-3000}"
            else
                sandbox_serve ${args[@]+"${args[@]}"}
            fi
            ;;
        test)
            if [[ "$sandbox_mode" == "docker-desktop" ]]; then
                docker_desktop_sandbox_run "npm test 2>/dev/null || python3 -m pytest 2>/dev/null || echo 'No test runner found'"
            else
                sandbox_test ${args[@]+"${args[@]}"}
            fi
            ;;
        phase)
            if [[ "$sandbox_mode" == "docker-desktop" ]]; then
                docker_desktop_sandbox_run "bash ${PROJECT_DIR}/autonomy/loki status"
            else
                sandbox_phase
            fi
            ;;
        expose)
            sandbox_expose ${args[@]+"${args[@]}"}
            ;;
        help|--help|-h)
            show_help
            ;;
        *)
            log_error "Unknown command: $command"
            show_help
            exit 1
            ;;
    esac
}

main "$@"
