Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
647 changes: 647 additions & 0 deletions .egg-state/contracts/280.json

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions .github/workflows/lint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -107,3 +107,6 @@ jobs:

- name: Check workflow secret safety
run: .venv/bin/python scripts/check-workflow-secrets.py

- name: Check hardcoded ports
run: .venv/bin/python scripts/check-hardcoded-ports.py
10 changes: 5 additions & 5 deletions docs/adr/implemented/ADR-Anthropic-API-Credential-Injection.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,8 @@ Both modes use the same credential injection mechanism, simplifying the architec
│ │
│ ┌─────────────────┐ ANTHROPIC_BASE_URL ┌─────────────────────┐ │
│ │ sandbox │ ─────────────────────────▶│ egg-gateway │ │
│ │ │ http://egg-gateway:9847 │ │ │
│ │ Claude Code │ /v1/messages │ Gateway API :9847 │──┼──▶ api.anthropic.com
│ │ │ http://egg-gateway:9848 │ │ │
│ │ Claude Code │ /v1/messages │ Gateway API :9848 │──┼──▶ api.anthropic.com
│ │ │ │ - Inject creds │ │
│ └────────┬────────┘ │ - Forward request │ │
│ │ └─────────────────────┘ │
Expand All @@ -41,8 +41,8 @@ Both modes use the same credential injection mechanism, simplifying the architec
│ │
│ ┌─────────────────┐ ANTHROPIC_BASE_URL ┌─────────────────────┐ │
│ │ sandbox │ ─────────────────────────▶│ egg-gateway │ │
│ │ │ http://egg-gateway:9847 │ │ │
│ │ Claude Code │ /v1/messages │ Gateway API :9847 │──┼──▶ api.anthropic.com
│ │ │ http://egg-gateway:9848 │ │ │
│ │ Claude Code │ /v1/messages │ Gateway API :9848 │──┼──▶ api.anthropic.com
│ │ │ │ - Inject creds │ │
│ └────────┬────────┘ │ - Forward request │ │
│ │ │ │ │
Expand Down Expand Up @@ -216,7 +216,7 @@ def proxy_count_tokens():
```python
def setup_anthropic_api(config: Config, logger: Logger) -> None:
"""Configure Claude Code to use gateway for Anthropic API."""
gateway_url = "http://egg-gateway:9847"
gateway_url = "http://egg-gateway:9848"

# Set ANTHROPIC_BASE_URL to route API calls through gateway
os.environ["ANTHROPIC_BASE_URL"] = gateway_url
Expand Down
6 changes: 3 additions & 3 deletions docs/adr/implemented/ADR-Gateway-Credential-Injection.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ The gateway already handles all git/GitHub authentication. Extending this to Ant
│ │
│ ┌─────────────────┐ ANTHROPIC_BASE_URL ┌─────────────────────┐ │
│ │ sandbox │ ─────────────────────────▶│ egg-gateway │ │
│ │ │ http://egg-gateway:9847 │ │ │
│ │ │ http://egg-gateway:9848 │ │ │
│ │ Claude Code │ /v1/messages │ 1. Receive request │ │
│ │ │ (no credentials) │ 2. Inject creds │ │
│ │ No API key │ │ 3. Filter tools │──┼──▶ api.anthropic.com
Expand All @@ -60,7 +60,7 @@ The gateway already handles all git/GitHub authentication. Extending this to Ant

Claude Code officially supports custom API endpoints via `ANTHROPIC_BASE_URL` ([docs](https://code.claude.com/docs/en/llm-gateway)). This enables a clean architecture:

- Container sets `ANTHROPIC_BASE_URL=http://egg-gateway:9847`
- Container sets `ANTHROPIC_BASE_URL=http://egg-gateway:9848`
- Claude Code sends requests to gateway over HTTP (internal network)
- Gateway adds credentials and forwards over HTTPS to api.anthropic.com
- **No SSL bump needed** - gateway receives plaintext, handles TLS outbound
Expand Down Expand Up @@ -92,7 +92,7 @@ The gateway exposes HTTP endpoints that proxy to Anthropic with credential injec
### Container Configuration

The container entrypoint:
1. Sets `ANTHROPIC_BASE_URL=http://egg-gateway:9847`
1. Sets `ANTHROPIC_BASE_URL=http://egg-gateway:9848`
2. Removes `ANTHROPIC_API_KEY` from environment (if present)
3. Removes `ANTHROPIC_OAUTH_TOKEN` from environment (if present)
4. Removes proxy environment variables for Node.js (Claude Code)
Expand Down
4 changes: 2 additions & 2 deletions docs/adr/implemented/ADR-Git-Isolation-Architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -1380,15 +1380,15 @@ sleep 3

# Test 1: Fetch from private repo (should succeed)
echo "Test 1: Fetch from private repo"
curl -X POST http://localhost:9847/api/v1/git/fetch \
curl -X POST http://localhost:9848/api/v1/git/fetch \
-H "Authorization: Bearer $GATEWAY_SECRET" \
-H "Content-Type: application/json" \
-d '{"repo_path": "/home/egg/repos/private-repo"}'
echo "✓ Private repo fetch allowed"

# Test 2: Clone public repo (should fail)
echo "Test 2: Clone public repo"
RESPONSE=$(curl -s -w "%{http_code}" -X POST http://localhost:9847/api/v1/git/clone \
RESPONSE=$(curl -s -w "%{http_code}" -X POST http://localhost:9848/api/v1/git/clone \
-H "Authorization: Bearer $GATEWAY_SECRET" \
-H "Content-Type: application/json" \
-d '{"url": "https://github.com/octocat/Hello-World"}')
Expand Down
2 changes: 1 addition & 1 deletion docs/adr/in-progress/ADR-GitHub-Actions-Support.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ The action's entrypoint script (`action/entrypoint.sh`) orchestrates:
3. **Detect mode** — If `mode=auto`, query `gh api repos/{owner}/{repo}` for visibility; set `private` for private/internal repos, `public` for public repos
4. **Generate ephemeral config** — Write a temporary `repositories.yaml` with the full config format (see Config Generation below), generate a launcher secret
5. **Start gateway** — Mount `$GITHUB_WORKSPACE/.git/` at a known path (e.g., `/repos/{repo-name}/.git`), inject the GitHub token via `GITHUB_USER_TOKEN` (and `BOT_GITHUB_TOKEN` if provided), inject the Anthropic OAuth token via `CLAUDE_CODE_OAUTH_TOKEN`, mount the launcher secret
6. **Wait for health** — Poll `http://egg-gateway:9847/api/v1/health`
6. **Wait for health** — Poll `http://egg-gateway:9848/api/v1/health`
7. **Allocate container IP + create session** — Pre-allocate a container IP on the `egg-isolated` network, then call `POST /api/v1/sessions/create` with `{container_id, container_ip, mode, repos, uid, gid}`. This single API call atomically: queries repo visibility, filters repos by mode, creates worktrees, and registers the session. Returns `session_token` and `worktrees` dict.
8. **Start sandbox** — Mount the worktree path returned by the session API, shadow `.git`, inject `EGG_SESSION_TOKEN`, run Claude Code in `--exec` mode with `--print --output-format=stream-json`. The sandbox does **not** receive the Anthropic OAuth token directly — all API calls route through the gateway at `ANTHROPIC_BASE_URL=http://egg-gateway:9848`, where the gateway injects credentials.
9. **Capture output** — Stream container logs, write to `$GITHUB_STEP_SUMMARY`, extract PR URLs for step outputs
Expand Down
14 changes: 7 additions & 7 deletions docs/adr/in-progress/ADR-Internet-Tool-Access-Lockdown.md
Original file line number Diff line number Diff line change
Expand Up @@ -669,7 +669,7 @@ ALLOWED_DOMAIN_PATTERNS = [
│ │ │ egg │ │ gateway │ │ │
│ │ │ 172.30.0.10 │◄────────────►│ 172.30.0.2 │ │ │
│ │ │ │ REST API │ │ │ │
│ │ │ NO EXTERNAL │ Port 9847 │ │ │ │
│ │ │ NO EXTERNAL │ Port 9848 │ │ │ │
│ │ │ ROUTE │ │ │ │ │
│ │ └─────────────┘ └────────┬────────┘ │ │
│ │ │ │ │
Expand Down Expand Up @@ -922,7 +922,7 @@ echo "Squid proxy started on port 3128"

# Start gateway API server
echo "Starting gateway API server..."
exec python -m waitress --port=9847 --host=0.0.0.0 gateway:app
exec python -m waitress --port=9848 --host=0.0.0.0 gateway:app
```

#### 4. egg Container Changes
Expand Down Expand Up @@ -1137,7 +1137,7 @@ wait_for_gateway() {

while [ $elapsed -lt $HEALTH_CHECK_TIMEOUT ]; do
# Check gateway API health endpoint
if docker exec "$GATEWAY_CONTAINER" curl -sf http://localhost:9847/api/v1/health >/dev/null 2>&1; then
if docker exec "$GATEWAY_CONTAINER" curl -sf http://localhost:9848/api/v1/health >/dev/null 2>&1; then
# Also verify Squid proxy is responding
if docker exec "$GATEWAY_CONTAINER" curl -sf --proxy http://localhost:3128 -o /dev/null https://api.github.com/ 2>&1; then
echo "Gateway is ready (API + Proxy healthy)"
Expand Down Expand Up @@ -1182,7 +1182,7 @@ def wait_for_gateway(timeout: int = 60, interval: int = 2) -> bool:
Returns:
True if gateway is ready, False if timeout
"""
gateway_url = "http://egg-gateway:9847/api/v1/health"
gateway_url = "http://egg-gateway:9848/api/v1/health"
proxy_test_url = "https://api.github.com/"
proxies = {"https": "http://gateway:3128"}

Expand Down Expand Up @@ -1228,7 +1228,7 @@ services:

gateway:
healthcheck:
test: ["CMD", "curl", "-sf", "http://localhost:9847/api/v1/health"]
test: ["CMD", "curl", "-sf", "http://localhost:9848/api/v1/health"]
interval: 5s
timeout: 5s
retries: 3
Expand Down Expand Up @@ -1343,7 +1343,7 @@ echo "Test 1: GitHub API unavailability"
# Mock GitHub API to return 503
docker exec egg-gateway iptables -A OUTPUT -d api.github.com -j DROP
# Attempt operation - should fail closed (treat as private, allow)
curl -X POST http://localhost:9847/api/v1/git/fetch \
curl -X POST http://localhost:9848/api/v1/git/fetch \
-H "Authorization: Bearer $GATEWAY_SECRET" \
-H "Content-Type: application/json" \
-d '{"repo_path": "/home/egg/repos/test-repo"}' || true
Expand All @@ -1354,7 +1354,7 @@ echo "✓ API unavailability handled (fail closed)"
# Test 2: Concurrent visibility checks
echo "Test 2: Concurrent visibility checks for same repo"
for i in {1..10}; do
curl -X POST http://localhost:9847/api/v1/git/fetch \
curl -X POST http://localhost:9848/api/v1/git/fetch \
-H "Authorization: Bearer $GATEWAY_SECRET" \
-H "Content-Type: application/json" \
-d '{"repo_path": "/home/egg/repos/test-repo"}' &
Expand Down
7 changes: 7 additions & 0 deletions gateway/entrypoint.sh
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,13 @@ set -e
#
# This allows private and public containers to run simultaneously without
# gateway restarts.
#
# PORT CONFIGURATION:
# The gateway uses hardcoded port values (9848 for API, 3129 for proxy).
# These are the source of truth - the Python constants in
# shared/egg_config/constants.py must match these values.
# Shell scripts cannot import Python modules, so gateway scripts define
# the ports directly. If ports need to change, update both locations.
# =============================================================================

echo "=== Gateway Sidecar Starting (Per-Container Mode Architecture) ==="
Expand Down
7 changes: 7 additions & 0 deletions gateway/start-gateway.sh
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,13 @@
# - egg-external: Gateway's external network for filtered internet access
#
# All network traffic from egg container is routed through Squid proxy for filtering.
#
# PORT CONFIGURATION:
# The gateway uses hardcoded port values (9848 for API, 3129 for proxy).
# These are the source of truth - the Python constants in
# shared/egg_config/constants.py must match these values.
# Shell scripts cannot import Python modules, so gateway scripts define
# the ports directly. If ports need to change, update both locations.

set -e

Expand Down
2 changes: 1 addition & 1 deletion gateway/tests/README-integration.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ For a working gateway sidecar setup:
```json
{
"timestamp": "2026-01-22T...",
"gateway_url": "http://egg-gateway:9847",
"gateway_url": "http://egg-gateway:9848",
"repo_path": "/home/egg/repos/egg",
"summary": {
"passed": 25,
Expand Down
7 changes: 4 additions & 3 deletions integration_tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@

import pytest
import requests
from egg_config import GATEWAY_PORT, GATEWAY_PROXY_PORT
from egg_container import (
ContainerNetworkConfig,
build_sandbox_docker_cmd,
Expand All @@ -45,8 +46,8 @@
EXTERNAL_SUBNET = "172.41.0.0/24"
GATEWAY_ISOLATED_IP = "172.40.0.2"
GATEWAY_EXTERNAL_IP = "172.41.0.2"
GATEWAY_PORT = 9848
PROXY_PORT = 3129
# Use constants from shared module for port configuration
PROXY_PORT = GATEWAY_PROXY_PORT

# Counter for allocating unique container IPs within the test subnet.
# Starts at 100 to leave room for gateway (.2) and other infrastructure.
Expand Down Expand Up @@ -187,7 +188,7 @@ def egg_stack() -> Generator[EggStack, None, None]:

# Get the mapped gateway port
result = subprocess.run(
[*compose_cmd, "port", "gateway", "9848"],
[*compose_cmd, "port", "gateway", str(GATEWAY_PORT)],
env=env,
capture_output=True,
text=True,
Expand Down
3 changes: 2 additions & 1 deletion sandbox/.claude/rules/environment.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,8 @@ Use `git push origin <branch>` (HTTPS). Operations are authenticated by the gate

If push fails:
- Check `git remote -v` is HTTPS
- Check gateway sidecar is running: `curl http://egg-gateway:9847/api/v1/health`
- Check gateway sidecar is running: `curl http://egg-gateway:9848/api/v1/health`
(Port 9848 is defined in shared/egg_config/constants.py)
- Ensure branch is egg-owned (egg-prefixed or has your open PR)

## File System
Expand Down
76 changes: 60 additions & 16 deletions sandbox/egg_lib/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,51 @@

This module contains the Config class, Colors, gateway constants,
and platform detection utilities.

Gateway and network constants are imported from the shared egg_config module
to ensure consistency across the codebase. See shared/egg_config/constants.py
for the authoritative definitions.
"""

import os
import sys
from pathlib import Path

# Import constants from shared module (installed as egg_config)
# Try installed package first, fall back to relative import for development
try:
from egg_config.constants import (
EGG_CONTAINER_IP,
EGG_EXTERNAL_NETWORK,
EGG_EXTERNAL_SUBNET,
EGG_ISOLATED_NETWORK,
EGG_ISOLATED_SUBNET,
GATEWAY_CONTAINER_NAME,
GATEWAY_EXTERNAL_IP,
GATEWAY_IMAGE_NAME,
GATEWAY_ISOLATED_IP,
GATEWAY_PORT,
GATEWAY_PROXY_PORT,
)
except ImportError:
# Development fallback: add shared/ to path
_shared_dir = Path(__file__).parent.parent.parent / "shared"
if str(_shared_dir) not in sys.path:
sys.path.insert(0, str(_shared_dir))
from egg_config.constants import (
EGG_CONTAINER_IP,
EGG_EXTERNAL_NETWORK,
EGG_EXTERNAL_SUBNET,
EGG_ISOLATED_NETWORK,
EGG_ISOLATED_SUBNET,
GATEWAY_CONTAINER_NAME,
GATEWAY_EXTERNAL_IP,
GATEWAY_IMAGE_NAME,
GATEWAY_ISOLATED_IP,
GATEWAY_PORT,
GATEWAY_PROXY_PORT,
)


class Colors:
"""ANSI color codes for terminal output"""
Expand Down Expand Up @@ -47,22 +87,26 @@ class Config:
]


# Gateway container constants (containerized gateway sidecar)
GATEWAY_CONTAINER_NAME = "egg-gateway"
GATEWAY_IMAGE_NAME = "egg-gateway"
GATEWAY_PORT = 9848
GATEWAY_PROXY_PORT = 3129

# Network lockdown configuration
# Dual-network architecture: egg-isolated (internal) + egg-external (for gateway)
# egg container connects only to egg-isolated and routes all traffic through gateway proxy
EGG_ISOLATED_NETWORK = "egg-isolated"
EGG_EXTERNAL_NETWORK = "egg-external"
EGG_ISOLATED_SUBNET = "172.32.0.0/24" # Subnet for egg-isolated network
EGG_EXTERNAL_SUBNET = "172.33.0.0/24" # Subnet for egg-external network
EGG_CONTAINER_IP = "172.32.0.10" # Fixed IP for egg container in isolated network
GATEWAY_ISOLATED_IP = "172.32.0.2" # Gateway IP in isolated network
GATEWAY_EXTERNAL_IP = "172.33.0.2" # Gateway IP in external network
# Re-export gateway and network constants for backward compatibility.
# These are imported from egg_config.constants at the top of this file.
# See shared/egg_config/constants.py for the authoritative definitions.
__all__ = [
"Colors",
"Config",
"EGG_CONTAINER_IP",
"EGG_EXTERNAL_NETWORK",
"EGG_EXTERNAL_SUBNET",
"EGG_ISOLATED_NETWORK",
"EGG_ISOLATED_SUBNET",
"GATEWAY_CONTAINER_NAME",
"GATEWAY_EXTERNAL_IP",
"GATEWAY_IMAGE_NAME",
"GATEWAY_ISOLATED_IP",
"GATEWAY_PORT",
"GATEWAY_PROXY_PORT",
"get_local_repos",
"get_platform",
]


def get_platform() -> str:
Expand Down
4 changes: 3 additions & 1 deletion sandbox/egg_lib/contract_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,13 +31,15 @@
from urllib.parse import urlencode
from urllib.request import Request, urlopen

from .config import GATEWAY_PORT

# Regex for validating git commit SHAs (7-40 hex characters)
COMMIT_SHA_PATTERN = re.compile(r"^[0-9a-fA-F]{7,40}$")


def get_gateway_url() -> str:
"""Get the gateway URL from environment or default."""
return os.environ.get("GATEWAY_URL", "http://egg-gateway:9848")
return os.environ.get("GATEWAY_URL", f"http://egg-gateway:{GATEWAY_PORT}")


def get_issue_number() -> int | None:
Expand Down
5 changes: 4 additions & 1 deletion sandbox/egg_lib/network_mode.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import urllib.request
from enum import Enum

from .config import GATEWAY_PORT
from .output import info


Expand Down Expand Up @@ -60,7 +61,9 @@ def get_gateway_current_mode() -> PrivateMode | None:
PrivateMode based on health response, or None if gateway is not reachable.
"""
try:
with urllib.request.urlopen("http://localhost:9848/api/v1/health", timeout=2) as response:
with urllib.request.urlopen(
f"http://localhost:{GATEWAY_PORT}/api/v1/health", timeout=2
) as response:
data = json.loads(response.read().decode("utf-8"))
# Gateway always reports private_mode=true now (locked Squid)
# But we still parse the response for backward compatibility
Expand Down
9 changes: 7 additions & 2 deletions sandbox/egg_lib/runtime.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@

from .auth import get_anthropic_api_key, get_anthropic_auth_method
from .config import (
GATEWAY_PORT,
get_local_repos,
)
from .container_logging import (
Expand Down Expand Up @@ -596,7 +597,9 @@ def run_claude(repo_mode: str | None = None) -> bool:
# Session creation failed - cannot proceed without a session
# since git/gh wrappers require EGG_SESSION_TOKEN (PR #666)
error("Session creation failed. Check that:")
error(" 1. Gateway sidecar is running: curl http://localhost:9848/api/v1/health")
error(
f" 1. Gateway sidecar is running: curl http://localhost:{GATEWAY_PORT}/api/v1/health"
)
error(" 2. Launcher secret exists: ~/.config/egg/launcher-secret")
error(" Fix: Re-run gateway/setup.sh to sync secrets")
return False
Expand Down Expand Up @@ -855,7 +858,9 @@ def exec_in_new_container(
# Session creation failed - cannot proceed without a session
# since git/gh wrappers require EGG_SESSION_TOKEN (PR #666)
error("Session creation failed. Check that:")
error(" 1. Gateway sidecar is running: curl http://localhost:9848/api/v1/health")
error(
f" 1. Gateway sidecar is running: curl http://localhost:{GATEWAY_PORT}/api/v1/health"
)
error(" 2. Launcher secret exists: ~/.config/egg/launcher-secret")
error(" Fix: Re-run gateway/setup.sh to sync secrets")
return False
Expand Down
Loading
Loading