From dfa0aee5e48ab481ca9f3d88fa63d8581cc62ed9 Mon Sep 17 00:00:00 2001 From: egg Date: Fri, 13 Mar 2026 02:26:11 +0000 Subject: [PATCH 1/8] Migrate .env to config.yaml, enable MCP by default --- .env.example | 92 ---------------- .gitignore | 2 +- bin/egg-deploy | 148 ++++++++++++++++--------- config/README.md | 36 +++++- config/config.yaml.example | 86 +++++++++++++++ docker-compose.yml | 8 +- docs/guides/coordinator.md | 4 +- docs/guides/deploy-migration.md | 8 +- docs/guides/deployment.md | 24 ++-- orchestrator/api.py | 55 +++++----- sandbox/egg_lib/setup_flow.py | 54 +++++++-- shared/egg_config/__init__.py | 2 + shared/egg_config/compose_config.py | 163 ++++++++++++++++++++++++++++ shared/egg_config/constants.py | 2 + 14 files changed, 479 insertions(+), 205 deletions(-) delete mode 100644 .env.example create mode 100644 config/config.yaml.example create mode 100644 shared/egg_config/compose_config.py diff --git a/.env.example b/.env.example deleted file mode 100644 index 85939133bb..0000000000 --- a/.env.example +++ /dev/null @@ -1,92 +0,0 @@ -# egg Docker Compose environment configuration -# Copy this file to .env and configure before running docker compose up - -# ============================================================================= -# Required Configuration -# ============================================================================= - -# Launcher secret for gateway API authentication -# Generate with: openssl rand -hex 32 -EGG_LAUNCHER_SECRET= - -# GitHub personal access token (for git operations) -# Required scopes: repo, read:org (if accessing org repos) -GITHUB_USER_TOKEN= - -# ============================================================================= -# Directory Paths -# ============================================================================= - -# Configuration directory containing repositories.yaml and secrets -# Default: current directory -EGG_CONFIG_DIR=. - -# Repos are auto-mounted from local_repos.paths in repositories.yaml. -# The egg launcher generates docker-compose.override.yml with per-repo mounts -# and sets EGG_HOST_REPO_MAP for the orchestrator automatically. - -# Host home directory (REQUIRED for worktree bind mounts) -# This MUST match your actual home directory for worktree path translation to work. -# The gateway and orchestrator use this to translate container paths to host paths -# for Docker volume mounts when spawning sandbox containers. -# Find with: echo $HOME -HOST_HOME=/home/your-username - -# ============================================================================= -# Container Identity -# ============================================================================= - -# Host UID/GID for file ownership (should match your user) -# Find with: id -u && id -g -HOST_UID=1000 -HOST_GID=1000 - -# Git identity for commits -EGG_USER_GIT_NAME=egg -EGG_USER_GIT_EMAIL=egg@localhost - -# ============================================================================= -# Network Ports -# ============================================================================= - -# Gateway API port (for git/gh operations from sandbox) -GATEWAY_API_PORT=9848 - -# Squid proxy port (for filtered internet access) -GATEWAY_PROXY_PORT=3129 - -# ============================================================================= -# Optional: Bot Mode (for GitHub App authentication) -# ============================================================================= - -# Bot GitHub token (if using GitHub App instead of PAT) -# BOT_GITHUB_TOKEN= - -# Bot identity for policy enforcement -# GATEWAY_BOT_NAME=your-bot-name -# GATEWAY_BOT_BRANCH_PREFIX=your-bot-name -# GATEWAY_TRUSTED_USERS= - -# ============================================================================= -# MCP Server (Coordinator) -# ============================================================================= - -# Enable MCP server for coordinator integration -# EGG_MCP_SERVER_ENABLED=false - -# MCP server port -# EGG_MCP_SERVER_PORT=9850 - -# MCP rate limit (requests per minute) -# EGG_MCP_RATE_LIMIT=30 - -# ============================================================================= -# Docker Configuration -# ============================================================================= - -# Compose project name (prefixes container/network names) -COMPOSE_PROJECT_NAME=egg - -# Image names (for pre-built images from GHCR) -# EGG_GATEWAY_IMAGE=ghcr.io/jwbron/egg-gateway:latest -# EGG_SANDBOX_IMAGE=ghcr.io/jwbron/egg-sandbox:latest diff --git a/.gitignore b/.gitignore index 4905c6b472..ea75d036e5 100644 --- a/.gitignore +++ b/.gitignore @@ -29,7 +29,7 @@ secrets.yaml .env .env.local .env.*.local -# Note: .env.example is NOT ignored (it's a template) +# Note: .env may be generated at runtime by egg --compose # IDE .idea/ diff --git a/bin/egg-deploy b/bin/egg-deploy index 74b57ee102..28306e56f3 100755 --- a/bin/egg-deploy +++ b/bin/egg-deploy @@ -15,7 +15,7 @@ # # Prerequisites: # - Docker and Docker Compose installed -# - .env file configured (copy from .env.example) +# - ~/.config/egg/config.yaml configured (run egg-deploy init) # - repositories.yaml in EGG_CONFIG_DIR set -e @@ -56,19 +56,23 @@ check_prerequisites() { fi } -# Load environment file -load_env() { - local env_file="$PROJECT_DIR/.env" +# Load configuration from ~/.config/egg/config.yaml via Python helper +load_config() { + local config_dir="${EGG_CONFIG_DIR:-$HOME/.config/egg}" + local helper="$PROJECT_DIR/shared/egg_config/compose_config.py" - if [ ! -f "$env_file" ]; then - warn ".env file not found. Run 'egg-deploy init' to create one." + if [ ! -f "$helper" ]; then + error "compose_config.py helper not found at $helper" return 1 fi - set -a - # shellcheck source=/dev/null - source "$env_file" - set +a + if [ ! -f "$config_dir/config.yaml" ]; then + warn "config.yaml not found in $config_dir. Run 'egg-deploy init' to create one." + return 1 + fi + + # Read config.yaml + secrets and export as env vars + eval "$(python3 "$helper" "$config_dir")" } # Validate configuration @@ -160,48 +164,87 @@ show_status() { init_config() { info "Initializing egg configuration..." - local env_file="$PROJECT_DIR/.env" - - # Create .env from example if it doesn't exist - if [ ! -f "$env_file" ]; then - if [ -f "$PROJECT_DIR/.env.example" ]; then - cp "$PROJECT_DIR/.env.example" "$env_file" - info "Created .env from .env.example" - else - error ".env.example not found" - exit 1 - fi + local config_dir="${EGG_CONFIG_DIR:-$HOME/.config/egg}" + local config_file="$config_dir/config.yaml" + local secrets_file="$config_dir/secrets.env" + + # Create config directory + mkdir -p "$config_dir" + + # Create config.yaml if it doesn't exist + if [ ! -f "$config_file" ]; then + local host_uid + host_uid=$(id -u) + local host_gid + host_gid=$(id -g) + + cat > "$config_file" << YAML_EOF +# Egg Configuration +# Generated by egg-deploy init + +# Host identity (required for worktree bind mounts) +host_home: $HOME +host_uid: $host_uid +host_gid: $host_gid + +# Git identity for commits +git_name: egg +git_email: egg@localhost + +# Docker Compose project name (prefixes container/network names) +compose_project_name: egg + +# Network ports +gateway_api_port: 9848 +gateway_proxy_port: 3129 +orchestrator_api_port: 9849 + +# MCP server (always enabled, configure port and rate limit) +mcp_server_port: 9850 +mcp_rate_limit: 30 + +# Anthropic authentication method +anthropic_auth_method: oauth + +# Image overrides (uncomment to use pre-built images) +# gateway_image: ghcr.io/jwbron/egg-gateway:latest +# orchestrator_image: ghcr.io/jwbron/egg-orchestrator:latest +# sandbox_image: ghcr.io/jwbron/egg-sandbox:latest + +# Bot mode (uncomment to enable GitHub App authentication) +# gateway_bot_name: your-bot-name +# gateway_bot_branch_prefix: your-bot-name +# gateway_trusted_users: user1,user2 +YAML_EOF + info "Created config.yaml at $config_file" else - warn ".env already exists, not overwriting" + warn "config.yaml already exists, not overwriting" fi - # Generate launcher secret if not set - if ! grep -q "^EGG_LAUNCHER_SECRET=." "$env_file" 2>/dev/null; then - local secret - secret=$(openssl rand -hex 32) - if [[ "$OSTYPE" == "darwin"* ]]; then - sed -i '' "s/^EGG_LAUNCHER_SECRET=.*/EGG_LAUNCHER_SECRET=$secret/" "$env_file" - else - sed -i "s/^EGG_LAUNCHER_SECRET=.*/EGG_LAUNCHER_SECRET=$secret/" "$env_file" - fi + # Generate launcher secret if not present + if [ ! -f "$config_dir/launcher-secret" ]; then + openssl rand -hex 32 > "$config_dir/launcher-secret" + chmod 600 "$config_dir/launcher-secret" success "Generated launcher secret" fi - # Set HOST_UID and HOST_GID - local host_uid - host_uid=$(id -u) - local host_gid - host_gid=$(id -g) - if [[ "$OSTYPE" == "darwin"* ]]; then - sed -i '' "s/^HOST_UID=.*/HOST_UID=$host_uid/" "$env_file" - sed -i '' "s/^HOST_GID=.*/HOST_GID=$host_gid/" "$env_file" - sed -i '' "s|^HOST_HOME=.*|HOST_HOME=$HOME|" "$env_file" - else - sed -i "s/^HOST_UID=.*/HOST_UID=$host_uid/" "$env_file" - sed -i "s/^HOST_GID=.*/HOST_GID=$host_gid/" "$env_file" - sed -i "s|^HOST_HOME=.*|HOST_HOME=$HOME|" "$env_file" + # Create secrets.env template if it doesn't exist + if [ ! -f "$secrets_file" ]; then + cat > "$secrets_file" << 'SECRETS_EOF' +# Egg Secrets +# Generated by egg-deploy init +# chmod 600 this file + +# GitHub personal access token (for git operations) +# Required scopes: repo, read:org (if accessing org repos) +GITHUB_USER_TOKEN= + +# Bot GitHub token (if using GitHub App instead of PAT) +# BOT_GITHUB_TOKEN= +SECRETS_EOF + chmod 600 "$secrets_file" + info "Created secrets.env at $secrets_file" fi - info "Set HOST_UID=$host_uid, HOST_GID=$host_gid, HOST_HOME=$HOME" # Create repos directory local repos_dir="$PROJECT_DIR/repos" @@ -214,15 +257,16 @@ init_config() { success "Configuration initialized!" echo "" echo "Next steps:" - echo " 1. Edit $env_file and set GITHUB_USER_TOKEN" - echo " 2. Create repositories.yaml (see egg.yaml.example for format)" + echo " 1. Set your GitHub token: echo 'YOUR_TOKEN' > $config_dir/github-token && chmod 600 $config_dir/github-token" + echo " Or edit $secrets_file and set GITHUB_USER_TOKEN" + echo " 2. Create repositories.yaml (see config/repositories.yaml.example)" echo " 3. Run: egg-deploy up" } # Command handlers cmd_up() { check_prerequisites - if ! load_env; then + if ! load_config; then error "Run 'egg-deploy init' first to create configuration" exit 1 fi @@ -250,7 +294,7 @@ cmd_up() { cmd_down() { check_prerequisites - load_env 2>/dev/null || true + load_config 2>/dev/null || true info "Stopping egg containers..." docker compose -f "$COMPOSE_FILE" down --remove-orphans @@ -260,21 +304,21 @@ cmd_down() { cmd_status() { check_prerequisites - load_env 2>/dev/null || true + load_config 2>/dev/null || true show_status } cmd_logs() { check_prerequisites - load_env 2>/dev/null || true + load_config 2>/dev/null || true docker compose -f "$COMPOSE_FILE" logs -f gateway } cmd_build() { check_prerequisites - load_env 2>/dev/null || true + load_config 2>/dev/null || true info "Building egg images..." docker compose -f "$COMPOSE_FILE" build diff --git a/config/README.md b/config/README.md index f7d672e312..1852ab1602 100644 --- a/config/README.md +++ b/config/README.md @@ -29,8 +29,9 @@ All persistent user configuration is consolidated under `~/.config/egg/`: ``` ~/.config/egg/ -├── config.yaml # Non-secret settings (Slack channel, sync intervals, etc.) +├── config.yaml # All non-secret settings (compose, ports, identity, etc.) ├── secrets.env # All secrets (Slack, GitHub, Confluence, JIRA tokens) +├── launcher-secret # Gateway launcher secret (dedicated file) ├── github-token # GitHub token (dedicated file) ├── github-app-id # GitHub App ID (if using App auth) ├── github-app-installation-id # GitHub App Installation ID @@ -58,7 +59,40 @@ This directory respects `$XDG_CACHE_HOME` if set. **Note**: Previously this was `~/.egg/`. The egg script auto-migrates on first run. +## config.yaml Schema + +The `config.yaml` file stores all non-secret settings for egg, including Docker Compose +configuration (previously in `.env`). See `config/config.yaml.example` for a full template. + +| Key | Env Var | Default | Description | +|-----|---------|---------|-------------| +| `host_home` | `HOST_HOME` | — | Host home directory (required for worktree mounts) | +| `host_uid` | `HOST_UID` | `1000` | Host UID for file ownership | +| `host_gid` | `HOST_GID` | `1000` | Host GID for file ownership | +| `git_name` | `EGG_USER_GIT_NAME` | `egg` | Git user name for commits | +| `git_email` | `EGG_USER_GIT_EMAIL` | `egg@localhost` | Git email for commits | +| `compose_project_name` | `COMPOSE_PROJECT_NAME` | `egg` | Docker Compose project name | +| `gateway_api_port` | `GATEWAY_API_PORT` | `9848` | Gateway API port | +| `gateway_proxy_port` | `GATEWAY_PROXY_PORT` | `3129` | Squid proxy port | +| `orchestrator_api_port` | `ORCHESTRATOR_API_PORT` | `9849` | Orchestrator API port | +| `mcp_server_port` | `EGG_MCP_SERVER_PORT` | `9850` | MCP server port | +| `mcp_rate_limit` | `EGG_MCP_RATE_LIMIT` | `30` | MCP requests per minute | +| `anthropic_auth_method` | — | `oauth` | Auth method: `oauth` or `api_key` | +| `gateway_image` | `EGG_GATEWAY_IMAGE` | — | Gateway image override | +| `orchestrator_image` | `EGG_ORCHESTRATOR_IMAGE` | — | Orchestrator image override | +| `sandbox_image` | `EGG_SANDBOX_IMAGE` | — | Sandbox image override | +| `gateway_bot_name` | `GATEWAY_BOT_NAME` | — | Bot name (GitHub App mode) | +| `gateway_bot_branch_prefix` | `GATEWAY_BOT_BRANCH_PREFIX` | — | Bot branch prefix | +| `gateway_trusted_users` | `GATEWAY_TRUSTED_USERS` | — | Trusted user list | + +Secrets (`EGG_LAUNCHER_SECRET`, `GITHUB_USER_TOKEN`, `BOT_GITHUB_TOKEN`) are stored in +`secrets.env` or dedicated files (`launcher-secret`, `github-token`), never in `config.yaml`. + +The `egg-deploy` script reads `config.yaml` via `shared/egg_config/compose_config.py` +and exports the values as environment variables for `docker-compose.yml`. + **Templates:** +- `config/config.yaml.example` - Full config.yaml template with all settings - `config/secrets.template.env` - Secrets template **In-repo modules:** diff --git a/config/config.yaml.example b/config/config.yaml.example new file mode 100644 index 0000000000..ddd26d5bf7 --- /dev/null +++ b/config/config.yaml.example @@ -0,0 +1,86 @@ +# Egg Configuration +# +# Copy this file to ~/.config/egg/config.yaml and configure. +# Run 'egg-deploy init' to generate this automatically with system defaults. +# +# Secrets (tokens, keys) go in ~/.config/egg/secrets.env or dedicated files +# (e.g., ~/.config/egg/github-token, ~/.config/egg/launcher-secret). +# They should NEVER be stored in config.yaml. + +# ============================================================================= +# Host Identity (required for worktree bind mounts) +# ============================================================================= + +# Must match your actual home directory for worktree path translation. +# Find with: echo $HOME +host_home: /home/your-username + +# File ownership UID/GID (should match your user) +# Find with: id -u && id -g +host_uid: 1000 +host_gid: 1000 + +# ============================================================================= +# Git Identity +# ============================================================================= + +# Git user for commits made by egg +git_name: egg +git_email: egg@localhost + +# ============================================================================= +# Docker Compose +# ============================================================================= + +# Compose project name (prefixes container and network names) +compose_project_name: egg + +# ============================================================================= +# Network Ports +# ============================================================================= + +# Gateway API port (for git/gh operations from sandbox) +gateway_api_port: 9848 + +# Squid proxy port (for filtered internet access) +gateway_proxy_port: 3129 + +# Orchestrator API port (for pipeline state management) +orchestrator_api_port: 9849 + +# ============================================================================= +# MCP Server +# ============================================================================= +# The MCP server runs alongside the orchestrator and bridges Claude Code +# sessions to the coordinator. It is always enabled. + +# MCP server port +mcp_server_port: 9850 + +# MCP rate limit (requests per minute) +mcp_rate_limit: 30 + +# ============================================================================= +# Anthropic Authentication +# ============================================================================= + +# Authentication method: "oauth" or "api_key" +anthropic_auth_method: oauth + +# ============================================================================= +# Optional: Image Overrides +# ============================================================================= +# Uncomment to use pre-built images from GHCR instead of building locally. + +# gateway_image: ghcr.io/jwbron/egg-gateway:latest +# orchestrator_image: ghcr.io/jwbron/egg-orchestrator:latest +# sandbox_image: ghcr.io/jwbron/egg-sandbox:latest + +# ============================================================================= +# Optional: Bot Mode (for GitHub App authentication) +# ============================================================================= +# Uncomment to enable bot mode with GitHub App authentication. + +# gateway_bot_name: your-bot-name +# gateway_bot_branch_prefix: your-bot-name +# gateway_trusted_users: user1,user2 diff --git a/docker-compose.yml b/docker-compose.yml index 7cceff107b..d31a088b1c 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -5,12 +5,12 @@ # container lifecycle via API. # # Usage: -# 1. Copy .env.example to .env and configure -# 2. docker compose up -d +# 1. Run 'egg-deploy init' to generate ~/.config/egg/config.yaml +# 2. egg-deploy up # 3. Run egg --compose to start sandbox sessions # -# Or use the unified launcher: -# bin/egg-deploy up +# Environment variables are loaded from config.yaml by egg-deploy. +# See config/config.yaml.example for all available settings. # # Network topology: # sandbox (172.32.0.x) -> gateway (172.32.0.2:3129) -> Internet (allowlisted) diff --git a/docs/guides/coordinator.md b/docs/guides/coordinator.md index e1bed45130..42b64e56d5 100644 --- a/docs/guides/coordinator.md +++ b/docs/guides/coordinator.md @@ -50,7 +50,7 @@ These fields in `PipelineConfig` control coordinator behavior: ## MCP Server -The MCP server runs alongside the orchestrator on port 9850, bridging Claude Code sessions to the coordinator via SSE transport. +The MCP server runs alongside the orchestrator on port 9850, bridging Claude Code sessions to the coordinator via SSE transport. It starts automatically — no configuration needed. ### Setup @@ -368,7 +368,7 @@ The coordinator runs with `phase="coordinator"` — a special phase value distin **Escalation not resolved**: Check pending decisions via `egg-orch decision list `. Escalations create standard HITL decisions that need human resolution. -**MCP connection failed**: Verify the MCP server is running on port 9850 (`curl http://localhost:9850/health`). The server starts as a background thread alongside the orchestrator. +**MCP connection failed**: Verify the MCP server is running on port 9850 (`curl http://localhost:9850/health`). The server starts automatically as a background thread alongside the orchestrator. Check the MCP port in `~/.config/egg/config.yaml` (`mcp_server_port`). **Coordinator crash loop**: Check the `coordinator_respawns` counter in coordinator state. If it equals `coordinator_max_respawns`, the pipeline has failed. Review container logs for the root cause: `egg-orch container logs `. diff --git a/docs/guides/deploy-migration.md b/docs/guides/deploy-migration.md index 69df63f28b..b18c151c79 100644 --- a/docs/guides/deploy-migration.md +++ b/docs/guides/deploy-migration.md @@ -143,13 +143,13 @@ bin/egg-deploy init ``` This creates: -- `.env` from `.env.example` -- Generates a new launcher secret -- Sets `HOST_UID` and `HOST_GID` +- `~/.config/egg/config.yaml` with system defaults +- Generates a new launcher secret at `~/.config/egg/launcher-secret` +- Sets `host_uid`, `host_gid`, and `host_home` in config.yaml ### 3. Migrate Secrets -Edit `.env` and set: +Add to `~/.config/egg/secrets.env`: ```bash # From your old secrets.env GITHUB_USER_TOKEN=ghp_xxxxx diff --git a/docs/guides/deployment.md b/docs/guides/deployment.md index 0405b14cb2..cce6637f5a 100644 --- a/docs/guides/deployment.md +++ b/docs/guides/deployment.md @@ -61,24 +61,20 @@ egg --public ### Configuration -1. **Copy the environment template:** +1. **Initialize configuration:** ```bash - cp .env.example .env + bin/egg-deploy init ``` + This creates `~/.config/egg/config.yaml` with system defaults and generates a launcher secret. -2. **Configure required variables:** +2. **Set your GitHub token:** ```bash - # Generate a session secret - EGG_LAUNCHER_SECRET=$(openssl rand -hex 32) - - # Set your GitHub token - GITHUB_USER_TOKEN=ghp_xxxxx - - # Set your user identity and home directory - HOST_UID=$(id -u) - HOST_GID=$(id -g) - HOST_HOME=$HOME # REQUIRED: orchestrator mounts $HOST_HOME/.egg-worktrees to read pipeline artifacts + echo 'ghp_xxxxx' > ~/.config/egg/github-token + chmod 600 ~/.config/egg/github-token ``` + Or add `GITHUB_USER_TOKEN=ghp_xxxxx` to `~/.config/egg/secrets.env`. + +3. **Review settings** in `~/.config/egg/config.yaml` (host_home, host_uid, host_gid are auto-detected). 3. **Create repositories.yaml:** ```yaml @@ -322,7 +318,7 @@ The orchestrator requires these environment variables to drop privileges before Fix: ```bash -# In your .env file (or hardcode the output of id -u / id -g) +# In your ~/.config/egg/config.yaml (host_uid / host_gid fields) HOST_UID=$(id -u) HOST_GID=$(id -g) ``` diff --git a/orchestrator/api.py b/orchestrator/api.py index e88ed65004..3a3b78c338 100644 --- a/orchestrator/api.py +++ b/orchestrator/api.py @@ -148,35 +148,34 @@ def index() -> tuple[Response, int]: ), 200 -# Start MCP server sidecar when coordinator support is enabled. +# Start MCP server sidecar unconditionally. # The MCP server runs in a background daemon thread and proxies tool calls # to the orchestrator's coordinator API endpoints. -def _maybe_start_mcp_server() -> None: - """Start the MCP server if enabled via environment variable.""" - if os.environ.get("EGG_MCP_SERVER_ENABLED", "").lower() in ("1", "true", "yes"): +def _start_mcp_server() -> None: + """Start the MCP server sidecar.""" + try: + from mcp_server import start_mcp_server + + mcp_port = int(os.environ.get("EGG_MCP_SERVER_PORT", "9850")) + mcp_rate_limit = int(os.environ.get("EGG_MCP_RATE_LIMIT", "30")) try: - from mcp_server import start_mcp_server - - mcp_port = int(os.environ.get("EGG_MCP_SERVER_PORT", "9850")) - mcp_rate_limit = int(os.environ.get("EGG_MCP_RATE_LIMIT", "30")) - try: - from egg_config import GATEWAY_PORT - except ImportError: - GATEWAY_PORT = 9848 # noqa: EGG002 - - gateway_url = os.environ.get("GATEWAY_URL", f"http://egg-gateway:{GATEWAY_PORT}") - launcher_secret = os.environ.get("EGG_LAUNCHER_SECRET", "") - start_mcp_server( - port=mcp_port, - rate_limit=mcp_rate_limit, - gateway_url=gateway_url, - launcher_secret=launcher_secret, - ) - logger.info("MCP server started", port=mcp_port) + from egg_config import GATEWAY_PORT except ImportError: - logger.warning("MCP server module not available, skipping startup") - except Exception as e: - logger.error("Failed to start MCP server", error=str(e)) - - -_maybe_start_mcp_server() + GATEWAY_PORT = 9848 # noqa: EGG002 + + gateway_url = os.environ.get("GATEWAY_URL", f"http://egg-gateway:{GATEWAY_PORT}") + launcher_secret = os.environ.get("EGG_LAUNCHER_SECRET", "") + start_mcp_server( + port=mcp_port, + rate_limit=mcp_rate_limit, + gateway_url=gateway_url, + launcher_secret=launcher_secret, + ) + logger.info("MCP server started", port=mcp_port) + except ImportError: + logger.warning("MCP server module not available, skipping startup") + except Exception as e: + logger.error("Failed to start MCP server", error=str(e)) + + +_start_mcp_server() diff --git a/sandbox/egg_lib/setup_flow.py b/sandbox/egg_lib/setup_flow.py index b2f6cb3ca1..93d7beca0a 100644 --- a/sandbox/egg_lib/setup_flow.py +++ b/sandbox/egg_lib/setup_flow.py @@ -503,28 +503,68 @@ def _create_general_config() -> bool: else: expected_method = "oauth" # Default to oauth - # Don't overwrite existing config unless auth method changed + # Merge into existing config, preserving user customizations if config_file.exists(): try: with open(config_file) as f: existing_config = yaml.safe_load(f) or {} - current_method = existing_config.get("anthropic_auth_method", "") - if current_method == expected_method: - return True + updated = False # Update auth method if it changed - existing_config["anthropic_auth_method"] = expected_method + if existing_config.get("anthropic_auth_method") != expected_method: + existing_config["anthropic_auth_method"] = expected_method + updated = True + # Add compose settings if missing + import os + + compose_defaults: dict[str, Any] = { + "host_home": str(Path.home()), + "host_uid": os.getuid(), + "host_gid": os.getgid(), + "git_name": "egg", + "git_email": "egg@localhost", + "compose_project_name": "egg", + "gateway_api_port": 9848, + "gateway_proxy_port": 3129, + "orchestrator_api_port": 9849, + "mcp_server_port": 9850, + "mcp_rate_limit": 30, + } + for key, default in compose_defaults.items(): + if key not in existing_config: + existing_config[key] = default + updated = True + if not updated: + return True with open(config_file, "w") as f: f.write("# Egg General Configuration\n") f.write("# Generated by egg --setup\n\n") yaml.dump(existing_config, f, default_flow_style=False, sort_keys=False) - info(f"Updated auth method to: {expected_method}") + info("Updated config.yaml with latest settings") return True except Exception: pass # Fall through to create new config - config = { + import os + + config: dict[str, Any] = { # Anthropic authentication method based on configured credentials "anthropic_auth_method": expected_method, + # Host identity for Docker Compose / worktree mounts + "host_home": str(Path.home()), + "host_uid": os.getuid(), + "host_gid": os.getgid(), + # Git identity + "git_name": "egg", + "git_email": "egg@localhost", + # Docker Compose + "compose_project_name": "egg", + # Network ports + "gateway_api_port": 9848, + "gateway_proxy_port": 3129, + "orchestrator_api_port": 9849, + # MCP server (always enabled) + "mcp_server_port": 9850, + "mcp_rate_limit": 30, } Config.USER_CONFIG_DIR.mkdir(parents=True, exist_ok=True) diff --git a/shared/egg_config/__init__.py b/shared/egg_config/__init__.py index 54541e2a03..62f658eccf 100644 --- a/shared/egg_config/__init__.py +++ b/shared/egg_config/__init__.py @@ -57,6 +57,7 @@ GATEWAY_ISOLATED_IP, GATEWAY_PORT, GATEWAY_PROXY_PORT, + MCP_SERVER_PORT, ORCHESTRATOR_CONTAINER_NAME, ORCHESTRATOR_EXTERNAL_IP, ORCHESTRATOR_IMAGE_NAME, @@ -98,6 +99,7 @@ "GATEWAY_ISOLATED_IP", "GATEWAY_PORT", "GATEWAY_PROXY_PORT", + "MCP_SERVER_PORT", # Service configurations "GatewayConfig", # Orchestrator constants diff --git a/shared/egg_config/compose_config.py b/shared/egg_config/compose_config.py new file mode 100644 index 0000000000..b4e8f04db2 --- /dev/null +++ b/shared/egg_config/compose_config.py @@ -0,0 +1,163 @@ +#!/usr/bin/env python3 +"""Read ~/.config/egg/config.yaml and emit KEY=VALUE lines for docker-compose. + +This module bridges the config.yaml settings to environment variables that +docker-compose.yml expects. It also reads secrets from secrets.env and +dedicated secret files (launcher-secret, github-token). + +Usage from shell: + eval "$(python3 shared/egg_config/compose_config.py [config_dir])" + +The config_dir defaults to ~/.config/egg if not provided. +""" + +import os +import sys +from pathlib import Path + +try: + import yaml +except ImportError: + print("Error: PyYAML is required. Install with: pip install pyyaml", file=sys.stderr) + sys.exit(1) + + +# Mapping from config.yaml keys to environment variable names. +# Each entry is (yaml_key, env_var_name, default_value). +CONFIG_KEY_MAP: list[tuple[str, str, str | None]] = [ + ("host_home", "HOST_HOME", None), + ("host_uid", "HOST_UID", "1000"), + ("host_gid", "HOST_GID", "1000"), + ("git_name", "EGG_USER_GIT_NAME", "egg"), + ("git_email", "EGG_USER_GIT_EMAIL", "egg@localhost"), + ("compose_project_name", "COMPOSE_PROJECT_NAME", "egg"), + ("gateway_api_port", "GATEWAY_API_PORT", "9848"), + ("gateway_proxy_port", "GATEWAY_PROXY_PORT", "3129"), + ("orchestrator_api_port", "ORCHESTRATOR_API_PORT", "9849"), + ("mcp_server_port", "EGG_MCP_SERVER_PORT", "9850"), + ("mcp_rate_limit", "EGG_MCP_RATE_LIMIT", "30"), + # Image overrides (optional) + ("gateway_image", "EGG_GATEWAY_IMAGE", None), + ("orchestrator_image", "EGG_ORCHESTRATOR_IMAGE", None), + ("sandbox_image", "EGG_SANDBOX_IMAGE", None), + # Bot mode (optional) + ("gateway_bot_name", "GATEWAY_BOT_NAME", None), + ("gateway_bot_branch_prefix", "GATEWAY_BOT_BRANCH_PREFIX", None), + ("gateway_trusted_users", "GATEWAY_TRUSTED_USERS", None), +] + +# Secret keys read from secrets.env (never stored in config.yaml) +SECRET_KEYS = [ + "EGG_LAUNCHER_SECRET", + "GITHUB_USER_TOKEN", + "BOT_GITHUB_TOKEN", +] + + +def _read_secrets_env(config_dir: Path) -> dict[str, str]: + """Read secrets from secrets.env file.""" + secrets: dict[str, str] = {} + secrets_file = config_dir / "secrets.env" + if not secrets_file.exists(): + return secrets + + with open(secrets_file) as f: + for line in f: + line = line.strip() + if not line or line.startswith("#"): + continue + if "=" in line: + key, _, value = line.partition("=") + key = key.strip() + value = value.strip() + # Strip surrounding quotes + if len(value) >= 2 and value[0] == value[-1] and value[0] in ('"', "'"): + value = value[1:-1] + secrets[key] = value + + return secrets + + +def _read_secret_file(config_dir: Path, filename: str) -> str | None: + """Read a single-value secret from a dedicated file.""" + path = config_dir / filename + if path.exists(): + content = path.read_text().strip() + if content: + return content + return None + + +def load_compose_env(config_dir: Path | None = None) -> dict[str, str]: + """Read config.yaml + secrets and return env vars for docker-compose. + + Priority (highest to lowest): + 1. Existing environment variables (not overridden) + 2. Secrets from secrets.env and dedicated files + 3. Values from config.yaml + 4. Built-in defaults + + Args: + config_dir: Path to config directory. Defaults to ~/.config/egg. + + Returns: + Dictionary of environment variable name -> value. + """ + if config_dir is None: + config_dir = Path(os.environ.get("EGG_CONFIG_DIR", Path.home() / ".config" / "egg")) + + config_dir = Path(config_dir) + result: dict[str, str] = {} + + # 1. Read config.yaml + config_file = config_dir / "config.yaml" + config: dict = {} + if config_file.exists(): + try: + with open(config_file) as f: + config = yaml.safe_load(f) or {} + except Exception as e: + print(f"Warning: Failed to read {config_file}: {e}", file=sys.stderr) + + # 2. Map config.yaml keys to env vars + for yaml_key, env_var, default in CONFIG_KEY_MAP: + value = config.get(yaml_key) + if value is not None: + result[env_var] = str(value) + elif default is not None: + result[env_var] = default + + # 3. Always set EGG_CONFIG_DIR + result["EGG_CONFIG_DIR"] = str(config_dir) + + # 4. Read secrets from secrets.env + secrets = _read_secrets_env(config_dir) + for key in SECRET_KEYS: + if key in secrets and secrets[key]: + result[key] = secrets[key] + + # 5. Read dedicated secret files (override secrets.env values) + launcher_secret = _read_secret_file(config_dir, "launcher-secret") + if launcher_secret: + result["EGG_LAUNCHER_SECRET"] = launcher_secret + + github_token = _read_secret_file(config_dir, "github-token") + if github_token: + result["GITHUB_USER_TOKEN"] = github_token + + return result + + +def main() -> None: + """CLI entrypoint: print KEY=VALUE lines for shell eval.""" + config_dir = Path(sys.argv[1]) if len(sys.argv) > 1 else None + env_vars = load_compose_env(config_dir) + + for key, value in sorted(env_vars.items()): + # Shell-safe quoting: single quotes with escaped single quotes + safe_value = value.replace("'", "'\\''") + print(f"export {key}='{safe_value}'") + + +if __name__ == "__main__": + main() diff --git a/shared/egg_config/constants.py b/shared/egg_config/constants.py index 9a7456b3e0..5fc662846c 100644 --- a/shared/egg_config/constants.py +++ b/shared/egg_config/constants.py @@ -29,6 +29,7 @@ ORCHESTRATOR_CONTAINER_NAME = "egg-orchestrator" ORCHESTRATOR_IMAGE_NAME = "egg-orchestrator" ORCHESTRATOR_PORT = 9849 +MCP_SERVER_PORT = 9850 ORCHESTRATOR_ISOLATED_IP = "172.32.0.3" # Orchestrator IP in isolated network ORCHESTRATOR_EXTERNAL_IP = "172.33.0.3" # Orchestrator IP in external network @@ -79,6 +80,7 @@ "GATEWAY_ISOLATED_IP", "GATEWAY_PORT", "GATEWAY_PROXY_PORT", + "MCP_SERVER_PORT", "ORCHESTRATOR_CONTAINER_NAME", "ORCHESTRATOR_EXTERNAL_IP", "ORCHESTRATOR_IMAGE_NAME", From 34ae2f60502f6210f97a5daa9154efd0c470f910 Mon Sep 17 00:00:00 2001 From: "james-in-a-box[bot]" <246424927+james-in-a-box[bot]@users.noreply.github.com> Date: Fri, 13 Mar 2026 02:32:52 +0000 Subject: [PATCH 2/8] Fix lint: replace hardcoded ports with constants, fix dict type annotation --- sandbox/egg_lib/setup_flow.py | 10 +++++----- shared/egg_config/compose_config.py | 16 +++++++++++++--- 2 files changed, 18 insertions(+), 8 deletions(-) diff --git a/sandbox/egg_lib/setup_flow.py b/sandbox/egg_lib/setup_flow.py index 93d7beca0a..051f886446 100644 --- a/sandbox/egg_lib/setup_flow.py +++ b/sandbox/egg_lib/setup_flow.py @@ -11,7 +11,7 @@ import yaml -from .config import Config +from .config import Config, GATEWAY_PORT, GATEWAY_PROXY_PORT from .docker import build_image from .output import error, info, success, warn @@ -523,8 +523,8 @@ def _create_general_config() -> bool: "git_name": "egg", "git_email": "egg@localhost", "compose_project_name": "egg", - "gateway_api_port": 9848, - "gateway_proxy_port": 3129, + "gateway_api_port": GATEWAY_PORT, + "gateway_proxy_port": GATEWAY_PROXY_PORT, "orchestrator_api_port": 9849, "mcp_server_port": 9850, "mcp_rate_limit": 30, @@ -559,8 +559,8 @@ def _create_general_config() -> bool: # Docker Compose "compose_project_name": "egg", # Network ports - "gateway_api_port": 9848, - "gateway_proxy_port": 3129, + "gateway_api_port": GATEWAY_PORT, + "gateway_proxy_port": GATEWAY_PROXY_PORT, "orchestrator_api_port": 9849, # MCP server (always enabled) "mcp_server_port": 9850, diff --git a/shared/egg_config/compose_config.py b/shared/egg_config/compose_config.py index b4e8f04db2..e8cdc003b5 100644 --- a/shared/egg_config/compose_config.py +++ b/shared/egg_config/compose_config.py @@ -14,6 +14,7 @@ import os import sys from pathlib import Path +from typing import Any try: import yaml @@ -21,6 +22,15 @@ print("Error: PyYAML is required. Install with: pip install pyyaml", file=sys.stderr) sys.exit(1) +try: + from egg_config.constants import GATEWAY_PORT, GATEWAY_PROXY_PORT +except ImportError: + # Standalone script execution: add shared/ to path + _shared_dir = Path(__file__).parent.parent + if str(_shared_dir) not in sys.path: + sys.path.insert(0, str(_shared_dir)) + from egg_config.constants import GATEWAY_PORT, GATEWAY_PROXY_PORT + # Mapping from config.yaml keys to environment variable names. # Each entry is (yaml_key, env_var_name, default_value). @@ -31,8 +41,8 @@ ("git_name", "EGG_USER_GIT_NAME", "egg"), ("git_email", "EGG_USER_GIT_EMAIL", "egg@localhost"), ("compose_project_name", "COMPOSE_PROJECT_NAME", "egg"), - ("gateway_api_port", "GATEWAY_API_PORT", "9848"), - ("gateway_proxy_port", "GATEWAY_PROXY_PORT", "3129"), + ("gateway_api_port", "GATEWAY_API_PORT", str(GATEWAY_PORT)), + ("gateway_proxy_port", "GATEWAY_PROXY_PORT", str(GATEWAY_PROXY_PORT)), ("orchestrator_api_port", "ORCHESTRATOR_API_PORT", "9849"), ("mcp_server_port", "EGG_MCP_SERVER_PORT", "9850"), ("mcp_rate_limit", "EGG_MCP_RATE_LIMIT", "30"), @@ -111,7 +121,7 @@ def load_compose_env(config_dir: Path | None = None) -> dict[str, str]: # 1. Read config.yaml config_file = config_dir / "config.yaml" - config: dict = {} + config: dict[str, Any] = {} if config_file.exists(): try: with open(config_file) as f: From 7a5f077255064465517fc617ea2c4944034e66ed Mon Sep 17 00:00:00 2001 From: "james-in-a-box[bot]" <246424927+james-in-a-box[bot]@users.noreply.github.com> Date: Fri, 13 Mar 2026 02:37:41 +0000 Subject: [PATCH 3/8] Fix lint: move inline import os to module level in setup_flow.py --- sandbox/egg_lib/setup_flow.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/sandbox/egg_lib/setup_flow.py b/sandbox/egg_lib/setup_flow.py index 051f886446..d755672d88 100644 --- a/sandbox/egg_lib/setup_flow.py +++ b/sandbox/egg_lib/setup_flow.py @@ -4,6 +4,7 @@ checking host setup, and adding standard mounts. """ +import os import secrets import shutil from pathlib import Path @@ -514,8 +515,6 @@ def _create_general_config() -> bool: existing_config["anthropic_auth_method"] = expected_method updated = True # Add compose settings if missing - import os - compose_defaults: dict[str, Any] = { "host_home": str(Path.home()), "host_uid": os.getuid(), From a1054bbe1b546947ed6a2e21d0d3debb06d7b620 Mon Sep 17 00:00:00 2001 From: "james-in-a-box[bot]" <246424927+james-in-a-box[bot]@users.noreply.github.com> Date: Fri, 13 Mar 2026 02:45:24 +0000 Subject: [PATCH 4/8] Fix lint: sort imports and remove redundant inline os imports in setup_flow.py --- sandbox/egg_lib/setup_flow.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/sandbox/egg_lib/setup_flow.py b/sandbox/egg_lib/setup_flow.py index d755672d88..b1f59cfb3f 100644 --- a/sandbox/egg_lib/setup_flow.py +++ b/sandbox/egg_lib/setup_flow.py @@ -12,7 +12,7 @@ import yaml -from .config import Config, GATEWAY_PORT, GATEWAY_PROXY_PORT +from .config import GATEWAY_PORT, GATEWAY_PROXY_PORT, Config from .docker import build_image from .output import error, info, success, warn @@ -543,8 +543,6 @@ def _create_general_config() -> bool: except Exception: pass # Fall through to create new config - import os - config: dict[str, Any] = { # Anthropic authentication method based on configured credentials "anthropic_auth_method": expected_method, @@ -656,8 +654,6 @@ def add_standard_mounts(mount_args: list[str], quiet: bool = False) -> None: These mounts are always added dynamically rather than relying on config files, ensuring they're always available even if setup hasn't been run recently. """ - import os - # Mount the shared certs Docker named volume for the gateway CA certificate. # The gateway (whether started via docker-compose or programmatically) writes # its CA cert to the '{project_name}-certs' named volume at /shared/certs. From 0d4bf3dcacb933c2640cefa28c05f23438073518 Mon Sep 17 00:00:00 2001 From: "egg-reviewer[bot]" <261018737+egg-reviewer[bot]@users.noreply.github.com> Date: Fri, 13 Mar 2026 03:00:48 +0000 Subject: [PATCH 5/8] Address review feedback on PR #1049 - Emit conditional exports in compose_config.py main() to honour pre-existing environment variables (blocking issue #1) - Replace leftover .env references in deployment.md with config.yaml equivalents (blocking issue #2) - Fix duplicate step numbering in deployment.md (blocking issue #3) - Add HOST_HOME validation in egg-deploy validate_config() (issue #4) - Import ORCHESTRATOR_PORT and MCP_SERVER_PORT from constants instead of hardcoding port defaults (issue #5) - Update test_doesnt_overwrite_unchanged to reflect merge semantics: include all compose defaults so the no-op path is tested (issue #7) - Convert shell syntax to YAML in deployment.md troubleshooting (issue #8) --- bin/egg-deploy | 5 ++++ docs/guides/deployment.md | 36 ++++++++++++++--------------- shared/egg_config/compose_config.py | 28 +++++++++++++++++----- tests/sandbox/test_setup_flow.py | 28 ++++++++++++++++++++-- 4 files changed, 71 insertions(+), 26 deletions(-) diff --git a/bin/egg-deploy b/bin/egg-deploy index 28306e56f3..2a39dab0cf 100755 --- a/bin/egg-deploy +++ b/bin/egg-deploy @@ -84,6 +84,11 @@ validate_config() { errors=$((errors + 1)) fi + if [ -z "${HOST_HOME:-}" ]; then + error "HOST_HOME is not set. Add 'host_home: $HOME' to config.yaml" + errors=$((errors + 1)) + fi + if [ -z "${GITHUB_USER_TOKEN:-}" ] && [ -z "${BOT_GITHUB_TOKEN:-}" ]; then warn "Neither GITHUB_USER_TOKEN nor BOT_GITHUB_TOKEN is set" warn "Git operations will not work without GitHub authentication" diff --git a/docs/guides/deployment.md b/docs/guides/deployment.md index cce6637f5a..aa93d80b47 100644 --- a/docs/guides/deployment.md +++ b/docs/guides/deployment.md @@ -49,8 +49,8 @@ cd egg # Initialize configuration bin/egg-deploy init -# Edit .env with your credentials -vim .env +# Review and edit configuration +vim ~/.config/egg/config.yaml # Start the gateway bin/egg-deploy up @@ -76,7 +76,7 @@ egg --public 3. **Review settings** in `~/.config/egg/config.yaml` (host_home, host_uid, host_gid are auto-detected). -3. **Create repositories.yaml:** +4. **Create repositories.yaml:** ```yaml github_username: your-github-username bot_username: your-bot-name # Required for bot operations @@ -212,25 +212,25 @@ For details on creating releases, see [RELEASING.md](../../RELEASING.md). ### Using Pre-built Images -For stability, pin to a major version in your `.env` file: +For stability, pin to a major version in `~/.config/egg/config.yaml`: -```bash -EGG_GATEWAY_IMAGE=ghcr.io/jwbron/egg-gateway:v0 -EGG_SANDBOX_IMAGE=ghcr.io/jwbron/egg-sandbox:v0 +```yaml +gateway_image: ghcr.io/jwbron/egg-gateway:v0 +sandbox_image: ghcr.io/jwbron/egg-sandbox:v0 ``` For full reproducibility, pin to an exact version: -```bash -EGG_GATEWAY_IMAGE=ghcr.io/jwbron/egg-gateway:v0.1.0 -EGG_SANDBOX_IMAGE=ghcr.io/jwbron/egg-sandbox:v0.1.0 +```yaml +gateway_image: ghcr.io/jwbron/egg-gateway:v0.1.0 +sandbox_image: ghcr.io/jwbron/egg-sandbox:v0.1.0 ``` Or use `latest` for automatic updates (not recommended for production): -```bash -EGG_GATEWAY_IMAGE=ghcr.io/jwbron/egg-gateway:latest -EGG_SANDBOX_IMAGE=ghcr.io/jwbron/egg-sandbox:latest +```yaml +gateway_image: ghcr.io/jwbron/egg-gateway:latest +sandbox_image: ghcr.io/jwbron/egg-sandbox:latest ``` You can also specify tags directly in a docker-compose.yml override. @@ -241,7 +241,7 @@ You can also specify tags directly in a docker-compose.yml override. | File | Purpose | |------|---------| -| `.env` | Environment variables for compose | +| `~/.config/egg/config.yaml` | Non-secret settings for compose | | `repositories.yaml` | Repository configuration | ### Optional Files @@ -317,10 +317,10 @@ If the orchestrator exits on startup with a root-related error, `HOST_UID` and ` The orchestrator requires these environment variables to drop privileges before starting. Running as root would create git artifacts with root:root ownership, breaking host git operations. Fix: -```bash -# In your ~/.config/egg/config.yaml (host_uid / host_gid fields) -HOST_UID=$(id -u) -HOST_GID=$(id -g) +```yaml +# In your ~/.config/egg/config.yaml +host_uid: 1000 # output of id -u +host_gid: 1000 # output of id -g ``` If `.git` directories already have root-owned files: diff --git a/shared/egg_config/compose_config.py b/shared/egg_config/compose_config.py index e8cdc003b5..891d1b0afb 100644 --- a/shared/egg_config/compose_config.py +++ b/shared/egg_config/compose_config.py @@ -23,13 +23,23 @@ sys.exit(1) try: - from egg_config.constants import GATEWAY_PORT, GATEWAY_PROXY_PORT + from egg_config.constants import ( + GATEWAY_PORT, + GATEWAY_PROXY_PORT, + MCP_SERVER_PORT, + ORCHESTRATOR_PORT, + ) except ImportError: # Standalone script execution: add shared/ to path _shared_dir = Path(__file__).parent.parent if str(_shared_dir) not in sys.path: sys.path.insert(0, str(_shared_dir)) - from egg_config.constants import GATEWAY_PORT, GATEWAY_PROXY_PORT + from egg_config.constants import ( + GATEWAY_PORT, + GATEWAY_PROXY_PORT, + MCP_SERVER_PORT, + ORCHESTRATOR_PORT, + ) # Mapping from config.yaml keys to environment variable names. @@ -43,8 +53,8 @@ ("compose_project_name", "COMPOSE_PROJECT_NAME", "egg"), ("gateway_api_port", "GATEWAY_API_PORT", str(GATEWAY_PORT)), ("gateway_proxy_port", "GATEWAY_PROXY_PORT", str(GATEWAY_PROXY_PORT)), - ("orchestrator_api_port", "ORCHESTRATOR_API_PORT", "9849"), - ("mcp_server_port", "EGG_MCP_SERVER_PORT", "9850"), + ("orchestrator_api_port", "ORCHESTRATOR_API_PORT", str(ORCHESTRATOR_PORT)), + ("mcp_server_port", "EGG_MCP_SERVER_PORT", str(MCP_SERVER_PORT)), ("mcp_rate_limit", "EGG_MCP_RATE_LIMIT", "30"), # Image overrides (optional) ("gateway_image", "EGG_GATEWAY_IMAGE", None), @@ -159,14 +169,20 @@ def load_compose_env(config_dir: Path | None = None) -> dict[str, str]: def main() -> None: - """CLI entrypoint: print KEY=VALUE lines for shell eval.""" + """CLI entrypoint: print conditional export statements for shell eval. + + Emits ``[ -z "${KEY+x}" ] && export KEY='value'`` so that pre-existing + environment variables are not overridden, matching the priority documented + in :func:`load_compose_env`. + """ config_dir = Path(sys.argv[1]) if len(sys.argv) > 1 else None env_vars = load_compose_env(config_dir) for key, value in sorted(env_vars.items()): # Shell-safe quoting: single quotes with escaped single quotes safe_value = value.replace("'", "'\\''") - print(f"export {key}='{safe_value}'") + # Only set if not already in the environment (honour existing vars) + print(f"[ -z \"${{{key}+x}}\" ] && export {key}='{safe_value}'") if __name__ == "__main__": diff --git a/tests/sandbox/test_setup_flow.py b/tests/sandbox/test_setup_flow.py index b17b6dec42..3b0f884fea 100644 --- a/tests/sandbox/test_setup_flow.py +++ b/tests/sandbox/test_setup_flow.py @@ -213,9 +213,31 @@ def test_creates_config_with_api_key(self, tmp_path): assert result is True def test_doesnt_overwrite_unchanged(self, tmp_path): - """Doesn't overwrite config when auth method hasn't changed.""" + """Doesn't overwrite config when all settings are already present.""" config_file = tmp_path / "config.yaml" - config_file.write_text("anthropic_auth_method: oauth\n") + # Include all compose defaults so the merge logic finds nothing to add + import os + + from egg_lib.config import GATEWAY_PORT, GATEWAY_PROXY_PORT + + full_config = { + "anthropic_auth_method": "oauth", + "host_home": str(Path.home()), + "host_uid": os.getuid(), + "host_gid": os.getgid(), + "git_name": "egg", + "git_email": "egg@localhost", + "compose_project_name": "egg", + "gateway_api_port": GATEWAY_PORT, + "gateway_proxy_port": GATEWAY_PROXY_PORT, + "orchestrator_api_port": 9849, + "mcp_server_port": 9850, + "mcp_rate_limit": 30, + } + import yaml + + config_file.write_text(yaml.dump(full_config)) + original_mtime = config_file.stat().st_mtime with patch("egg_lib.setup_flow.Config") as mock_config: mock_config.USER_CONFIG_DIR = tmp_path with patch( @@ -224,6 +246,8 @@ def test_doesnt_overwrite_unchanged(self, tmp_path): ): result = _create_general_config() assert result is True + # File should not have been rewritten + assert config_file.stat().st_mtime == original_mtime def test_updates_changed_auth_method(self, tmp_path): """Updates config when auth method changed.""" From ef14f4333044f0b22113edb66445739dbe2c3612 Mon Sep 17 00:00:00 2001 From: "egg-reviewer[bot]" <261018737+egg-reviewer[bot]@users.noreply.github.com> Date: Fri, 13 Mar 2026 03:22:07 +0000 Subject: [PATCH 6/8] Address re-review feedback: fix stale refs and hardcoded ports --- docs/architecture/orchestrator.md | 2 +- docs/guides/deploy-migration.md | 41 ++++++++++++++--------------- sandbox/egg_lib/config.py | 3 +++ sandbox/egg_lib/setup_flow.py | 16 +++++++---- shared/egg_config/compose_config.py | 3 ++- 5 files changed, 37 insertions(+), 28 deletions(-) diff --git a/docs/architecture/orchestrator.md b/docs/architecture/orchestrator.md index 626bd78334..be454853e9 100644 --- a/docs/architecture/orchestrator.md +++ b/docs/architecture/orchestrator.md @@ -414,7 +414,7 @@ Fixed IPs: - `POST /pipelines/{id}/deployment-check/teardown` - Tear down devserver - `GET /pipelines/{id}/health` - On-demand pipeline health check (all tiers) -**MCP Server (`/mcp/`)** *(optional, requires `EGG_MCP_SERVER_ENABLED=true`)* +**MCP Server (`/mcp/`)** - `GET /health` - MCP server health check - `GET /mcp/v1/tools` - List available coordinator tools - `POST /mcp/v1/tools/call` - Execute a coordinator tool call diff --git a/docs/guides/deploy-migration.md b/docs/guides/deploy-migration.md index b18c151c79..4a505a68c4 100644 --- a/docs/guides/deploy-migration.md +++ b/docs/guides/deploy-migration.md @@ -7,7 +7,7 @@ This guide helps you migrate from the legacy deployment scripts to the new Docke The new deployment approach offers: - **Simpler setup**: Single `docker compose up` vs. multiple manual steps -- **Consistent configuration**: All settings in one `.env` file +- **Consistent configuration**: All settings in `~/.config/egg/config.yaml` - **Better health checking**: Built-in Docker Compose health checks - **Easier cleanup**: `docker compose down` removes everything - **Pre-built images**: Optional use of GHCR images instead of local builds @@ -66,7 +66,7 @@ docker network connect --ip 172.33.0.2 egg-external egg-gateway ```bash # One-time setup bin/egg-deploy init -vim .env # Set GITHUB_USER_TOKEN +vim ~/.config/egg/secrets.env # Set GITHUB_USER_TOKEN # Start docker compose up -d @@ -104,23 +104,23 @@ services: ### Environment Variables -The `.env` file consolidates all configuration: +`~/.config/egg/config.yaml` consolidates all configuration: | Old Location | New Location | |--------------|--------------| -| `~/.config/egg/secrets.env` | `.env` (GITHUB_USER_TOKEN, etc.) | -| `gateway/start-gateway.sh` variables | `.env` | -| Command-line flags | `.env` | +| `~/.config/egg/secrets.env` | `~/.config/egg/secrets.env` (GITHUB_USER_TOKEN, etc.) | +| `gateway/start-gateway.sh` variables | `~/.config/egg/config.yaml` | +| Command-line flags | `~/.config/egg/config.yaml` | ### Secrets -1. **Launcher secret**: Auto-generated by `bin/egg-deploy init` -2. **GitHub token**: Set `GITHUB_USER_TOKEN` in `.env` +1. **Launcher secret**: Auto-generated by `bin/egg-deploy init` at `~/.config/egg/launcher-secret` +2. **GitHub token**: Set `GITHUB_USER_TOKEN` in `~/.config/egg/secrets.env` 3. **Anthropic credentials**: Configure in Claude Code directly ### Repositories -The `repositories.yaml` file is still used. Point to it via `EGG_CONFIG_DIR` in `.env`. +The `repositories.yaml` file is still used. Point to it via `egg_config_dir` in `~/.config/egg/config.yaml`. ## Step-by-Step Migration @@ -231,10 +231,10 @@ docker network rm egg-isolated egg-external Permission denied: /home/egg/repos ``` -Ensure `HOST_UID` and `HOST_GID` are set correctly: -```bash -id -u # Should match HOST_UID -id -g # Should match HOST_GID +Ensure `host_uid` and `host_gid` are set correctly in `~/.config/egg/config.yaml`: +```yaml +host_uid: 1000 # output of id -u +host_gid: 1000 # output of id -g ``` ### Missing repositories.yaml @@ -243,19 +243,18 @@ id -g # Should match HOST_GID Error: repositories.yaml not found ``` -Create it or point to existing location: -```bash -EGG_CONFIG_DIR=/path/to/existing/config +Create it or set the config directory in `~/.config/egg/config.yaml`: +```yaml +egg_config_dir: /path/to/existing/config ``` ## Pre-built Images -To use pre-built images instead of building locally: +To use pre-built images instead of building locally, add to `~/.config/egg/config.yaml`: -```bash -# In .env -EGG_GATEWAY_IMAGE=ghcr.io/jwbron/egg-gateway:latest -EGG_SANDBOX_IMAGE=ghcr.io/jwbron/egg-sandbox:latest +```yaml +gateway_image: ghcr.io/jwbron/egg-gateway:latest +sandbox_image: ghcr.io/jwbron/egg-sandbox:latest ``` Then pull before starting: diff --git a/sandbox/egg_lib/config.py b/sandbox/egg_lib/config.py index 324fbc2e0c..1c7842483a 100644 --- a/sandbox/egg_lib/config.py +++ b/sandbox/egg_lib/config.py @@ -27,6 +27,7 @@ GATEWAY_ISOLATED_IP, GATEWAY_PORT, GATEWAY_PROXY_PORT, + MCP_SERVER_PORT, ORCHESTRATOR_CONTAINER_NAME, ORCHESTRATOR_EXTERNAL_IP, ORCHESTRATOR_IMAGE_NAME, @@ -50,6 +51,7 @@ GATEWAY_ISOLATED_IP, GATEWAY_PORT, GATEWAY_PROXY_PORT, + MCP_SERVER_PORT, ORCHESTRATOR_CONTAINER_NAME, ORCHESTRATOR_EXTERNAL_IP, ORCHESTRATOR_IMAGE_NAME, @@ -114,6 +116,7 @@ class Config: "GATEWAY_ISOLATED_IP", "GATEWAY_PORT", "GATEWAY_PROXY_PORT", + "MCP_SERVER_PORT", "ORCHESTRATOR_CONTAINER_NAME", "ORCHESTRATOR_EXTERNAL_IP", "ORCHESTRATOR_IMAGE_NAME", diff --git a/sandbox/egg_lib/setup_flow.py b/sandbox/egg_lib/setup_flow.py index b1f59cfb3f..b3872ce502 100644 --- a/sandbox/egg_lib/setup_flow.py +++ b/sandbox/egg_lib/setup_flow.py @@ -12,7 +12,13 @@ import yaml -from .config import GATEWAY_PORT, GATEWAY_PROXY_PORT, Config +from .config import ( + GATEWAY_PORT, + GATEWAY_PROXY_PORT, + MCP_SERVER_PORT, + ORCHESTRATOR_PORT, + Config, +) from .docker import build_image from .output import error, info, success, warn @@ -524,8 +530,8 @@ def _create_general_config() -> bool: "compose_project_name": "egg", "gateway_api_port": GATEWAY_PORT, "gateway_proxy_port": GATEWAY_PROXY_PORT, - "orchestrator_api_port": 9849, - "mcp_server_port": 9850, + "orchestrator_api_port": ORCHESTRATOR_PORT, + "mcp_server_port": MCP_SERVER_PORT, "mcp_rate_limit": 30, } for key, default in compose_defaults.items(): @@ -558,9 +564,9 @@ def _create_general_config() -> bool: # Network ports "gateway_api_port": GATEWAY_PORT, "gateway_proxy_port": GATEWAY_PROXY_PORT, - "orchestrator_api_port": 9849, + "orchestrator_api_port": ORCHESTRATOR_PORT, # MCP server (always enabled) - "mcp_server_port": 9850, + "mcp_server_port": MCP_SERVER_PORT, "mcp_rate_limit": 30, } diff --git a/shared/egg_config/compose_config.py b/shared/egg_config/compose_config.py index 891d1b0afb..f0932f37b6 100644 --- a/shared/egg_config/compose_config.py +++ b/shared/egg_config/compose_config.py @@ -112,7 +112,8 @@ def load_compose_env(config_dir: Path | None = None) -> dict[str, str]: """Read config.yaml + secrets and return env vars for docker-compose. Priority (highest to lowest): - 1. Existing environment variables (not overridden) + 1. Existing environment variables (not overridden) — enforced by + the conditional export in :func:`main`, not by this function 2. Secrets from secrets.env and dedicated files 3. Values from config.yaml 4. Built-in defaults From b22895c9175cfc3158fdb4a1a48d6cf08eee80b4 Mon Sep 17 00:00:00 2001 From: "egg-reviewer[bot]" <261018737+egg-reviewer[bot]@users.noreply.github.com> Date: Fri, 13 Mar 2026 03:36:24 +0000 Subject: [PATCH 7/8] Address 2nd re-review: use port constants in test, fix stale egg_config_dir refs --- docs/guides/deploy-migration.md | 8 ++++---- tests/sandbox/test_setup_flow.py | 11 ++++++++--- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/docs/guides/deploy-migration.md b/docs/guides/deploy-migration.md index 4a505a68c4..ba01a170fe 100644 --- a/docs/guides/deploy-migration.md +++ b/docs/guides/deploy-migration.md @@ -120,7 +120,7 @@ services: ### Repositories -The `repositories.yaml` file is still used. Point to it via `egg_config_dir` in `~/.config/egg/config.yaml`. +The `repositories.yaml` file is still used. Set the `EGG_CONFIG_DIR` environment variable to point to its location, or place it in `~/.config/egg/`. ## Step-by-Step Migration @@ -243,9 +243,9 @@ host_gid: 1000 # output of id -g Error: repositories.yaml not found ``` -Create it or set the config directory in `~/.config/egg/config.yaml`: -```yaml -egg_config_dir: /path/to/existing/config +Create it in `~/.config/egg/`, or set the `EGG_CONFIG_DIR` environment variable to point to its location: +```bash +export EGG_CONFIG_DIR=/path/to/existing/config ``` ## Pre-built Images diff --git a/tests/sandbox/test_setup_flow.py b/tests/sandbox/test_setup_flow.py index 3b0f884fea..1db4b13569 100644 --- a/tests/sandbox/test_setup_flow.py +++ b/tests/sandbox/test_setup_flow.py @@ -218,7 +218,12 @@ def test_doesnt_overwrite_unchanged(self, tmp_path): # Include all compose defaults so the merge logic finds nothing to add import os - from egg_lib.config import GATEWAY_PORT, GATEWAY_PROXY_PORT + from egg_lib.config import ( + GATEWAY_PORT, + GATEWAY_PROXY_PORT, + MCP_SERVER_PORT, + ORCHESTRATOR_PORT, + ) full_config = { "anthropic_auth_method": "oauth", @@ -230,8 +235,8 @@ def test_doesnt_overwrite_unchanged(self, tmp_path): "compose_project_name": "egg", "gateway_api_port": GATEWAY_PORT, "gateway_proxy_port": GATEWAY_PROXY_PORT, - "orchestrator_api_port": 9849, - "mcp_server_port": 9850, + "orchestrator_api_port": ORCHESTRATOR_PORT, + "mcp_server_port": MCP_SERVER_PORT, "mcp_rate_limit": 30, } import yaml From bbe80842b90e5f712fd94ba41cbeb2613cc2bfcd Mon Sep 17 00:00:00 2001 From: "egg-reviewer[bot]" <261018737+egg-reviewer[bot]@users.noreply.github.com> Date: Fri, 13 Mar 2026 03:47:07 +0000 Subject: [PATCH 8/8] Remove stale EGG_CONFIG_DIR from secrets.env step in migration guide --- docs/guides/deploy-migration.md | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/docs/guides/deploy-migration.md b/docs/guides/deploy-migration.md index ba01a170fe..43f4406dd5 100644 --- a/docs/guides/deploy-migration.md +++ b/docs/guides/deploy-migration.md @@ -153,11 +153,10 @@ Add to `~/.config/egg/secrets.env`: ```bash # From your old secrets.env GITHUB_USER_TOKEN=ghp_xxxxx - -# From your old config -EGG_CONFIG_DIR=/path/to/your/config ``` +If you need a custom config directory for `repositories.yaml`, set `EGG_CONFIG_DIR` in your shell profile (see [Repositories](#repositories) above). + Repos are auto-mounted from `local_repos.paths` in your `repositories.yaml`. ### 4. Verify repositories.yaml