diff --git a/.egg-state/contracts/280.json b/.egg-state/contracts/280.json new file mode 100644 index 0000000000..e104612aa8 --- /dev/null +++ b/.egg-state/contracts/280.json @@ -0,0 +1,647 @@ +{ + "schemaVersion": "1.0", + "issue": { + "number": 280, + "title": "get rid of hardccoded gatway and sandbox ports and uris", + "url": "https://github.com/jwbron/egg/issues/280" + }, + "current_phase": "implement", + "acceptance_criteria": [ + { + "id": "ac-1", + "description": "[TASK-1-1] Module exports GATEWAY_PORT, GATEWAY_PROXY_PORT, GATEWAY_CONTAINER_NAME, GATEWAY_IMAGE_NAME, network names, subnets, and IPs; includes TEST_GATEWAY_PORT = 1234", + "verified": false + }, + { + "id": "ac-2", + "description": "[TASK-1-2] Constants are importable via from egg_config import GATEWAY_PORT", + "verified": false + }, + { + "id": "ac-3", + "description": "[TASK-1-3] GatewayConfig.port default references GATEWAY_PORT constant, not hardcoded 9848", + "verified": false + }, + { + "id": "ac-4", + "description": "[TASK-2-1] Remove local constant definitions (lines 50-65); re-export from shared module", + "verified": false + }, + { + "id": "ac-5", + "description": "[TASK-2-2] RuntimeContext defaults reference shared constants", + "verified": false + }, + { + "id": "ac-6", + "description": "[TASK-2-3] get_gateway_url() uses GATEWAY_PORT from shared module", + "verified": false + }, + { + "id": "ac-7", + "description": "[TASK-2-4] Lines 516, 816, 920-921 reference constants instead of hardcoded 9848/3129", + "verified": false + }, + { + "id": "ac-8", + "description": "[TASK-2-5] Health check endpoint (line 63) uses constant", + "verified": false + }, + { + "id": "ac-9", + "description": "[TASK-2-6] Error messages at lines 599, 858 reference GATEWAY_PORT constant", + "verified": false + }, + { + "id": "ac-10", + "description": "[TASK-2-7] Line 114 hardcoded :3129 replaced with constant reference", + "verified": false + }, + { + "id": "ac-11", + "description": "[TASK-3-1] Remove hardcoded fallback on line 14; fail with clear error if GATEWAY_URL not set", + "verified": false + }, + { + "id": "ac-12", + "description": "[TASK-3-2] Remove hardcoded fallback on line 18; fail with clear error if GATEWAY_URL not set", + "verified": false + }, + { + "id": "ac-13", + "description": "[TASK-3-3] Add comment in gateway/entrypoint.sh and gateway/start-gateway.sh", + "verified": false + }, + { + "id": "ac-14", + "description": "[TASK-4-1] Lines 48-49 use constants from shared/egg_config/constants.py", + "verified": false + }, + { + "id": "ac-15", + "description": "[TASK-4-2] Mock contexts at lines 40, 356-411 use TEST_GATEWAY_PORT", + "verified": false + }, + { + "id": "ac-16", + "description": "[TASK-4-3] Mock context at line 36 uses TEST_GATEWAY_PORT", + "verified": false + }, + { + "id": "ac-17", + "description": "[TASK-4-4] Line 59 assertion uses GATEWAY_PORT constant", + "verified": false + }, + { + "id": "ac-18", + "description": "[TASK-4-5] Lines 677, 684, 688, 700 use constants", + "verified": false + }, + { + "id": "ac-19", + "description": "[TASK-4-6] Line 153 assertion uses constants", + "verified": false + }, + { + "id": "ac-20", + "description": "[TASK-5-1] Line 53 references correct port 9848 and mentions constants file", + "verified": false + }, + { + "id": "ac-21", + "description": "[TASK-5-2] All port 9847 references updated to 9848", + "verified": false + }, + { + "id": "ac-22", + "description": "[TASK-5-3] All port 9847 references updated to 9848", + "verified": false + }, + { + "id": "ac-23", + "description": "[TASK-5-4] All port 9847 references updated to 9848", + "verified": false + }, + { + "id": "ac-24", + "description": "[TASK-5-5] All port 9847 references updated to 9848", + "verified": false + }, + { + "id": "ac-25", + "description": "[TASK-5-6] Port 9847 references at lines 97, 99, 107 updated to 9848", + "verified": false + }, + { + "id": "ac-26", + "description": "[TASK-5-7] Port references are consistent (9848)", + "verified": false + }, + { + "id": "ac-27", + "description": "[TASK-6-1] Script greps for patterns like :9847, :9848, :3129 literals; returns non-zero if found", + "verified": false + }, + { + "id": "ac-28", + "description": "[TASK-6-2] CI fails if hardcoded ports detected in new code", + "verified": false + } + ], + "phases": [ + { + "id": "phase-1", + "name": "Create Centralized Constants Module", + "status": "pending", + "tasks": [ + { + "id": "task-1-1", + "description": "Create shared/egg_config/constants.py with all gateway/sandbox constants", + "status": "pending", + "acceptance_criteria": "Module exports GATEWAY_PORT, GATEWAY_PROXY_PORT, GATEWAY_CONTAINER_NAME, GATEWAY_IMAGE_NAME, network names, subnets, and IPs; includes TEST_GATEWAY_PORT = 1234", + "files_affected": [ + "shared/egg_config/constants.py" + ], + "commit": null, + "review_cycles": 0, + "max_cycles": 3, + "escalated": false, + "notes": "" + }, + { + "id": "task-1-2", + "description": "Update shared/egg_config/__init__.py to export the new constants", + "status": "pending", + "acceptance_criteria": "Constants are importable via from egg_config import GATEWAY_PORT", + "files_affected": [ + "shared/egg_config/__init__.py" + ], + "commit": null, + "review_cycles": 0, + "max_cycles": 3, + "escalated": false, + "notes": "" + }, + { + "id": "task-1-3", + "description": "Update shared/egg_config/configs/gateway.py to use constants from new module", + "status": "pending", + "acceptance_criteria": "GatewayConfig.port default references GATEWAY_PORT constant, not hardcoded 9848", + "files_affected": [ + "shared/egg_config/configs/gateway.py" + ], + "commit": null, + "review_cycles": 0, + "max_cycles": 3, + "escalated": false, + "notes": "" + } + ], + "review_cycles": 0, + "max_cycles": 3, + "escalated": false, + "escalation_reason": null, + "review_feedback": [] + }, + { + "id": "phase-2", + "name": "Update Sandbox/Launcher Code", + "status": "pending", + "tasks": [ + { + "id": "task-2-1", + "description": "Update sandbox/egg_lib/config.py to import from shared/egg_config/constants.py", + "status": "pending", + "acceptance_criteria": "Remove local constant definitions (lines 50-65); re-export from shared module", + "files_affected": [ + "sandbox/egg_lib/config.py" + ], + "commit": null, + "review_cycles": 0, + "max_cycles": 3, + "escalated": false, + "notes": "" + }, + { + "id": "task-2-2", + "description": "Update sandbox/egg_lib/context.py to import defaults from shared constants", + "status": "pending", + "acceptance_criteria": "RuntimeContext defaults reference shared constants", + "files_affected": [ + "sandbox/egg_lib/context.py" + ], + "commit": null, + "review_cycles": 0, + "max_cycles": 3, + "escalated": false, + "notes": "" + }, + { + "id": "task-2-3", + "description": "Update sandbox/egg_lib/contract_cli.py to use shared constants", + "status": "pending", + "acceptance_criteria": "get_gateway_url() uses GATEWAY_PORT from shared module", + "files_affected": [ + "sandbox/egg_lib/contract_cli.py" + ], + "commit": null, + "review_cycles": 0, + "max_cycles": 3, + "escalated": false, + "notes": "" + }, + { + "id": "task-2-4", + "description": "Update sandbox/entrypoint.py to use shared constants for fallback ports", + "status": "pending", + "acceptance_criteria": "Lines 516, 816, 920-921 reference constants instead of hardcoded 9848/3129", + "files_affected": [ + "sandbox/entrypoint.py" + ], + "commit": null, + "review_cycles": 0, + "max_cycles": 3, + "escalated": false, + "notes": "" + }, + { + "id": "task-2-5", + "description": "Update sandbox/egg_lib/network_mode.py to use shared constants", + "status": "pending", + "acceptance_criteria": "Health check endpoint (line 63) uses constant", + "files_affected": [ + "sandbox/egg_lib/network_mode.py" + ], + "commit": null, + "review_cycles": 0, + "max_cycles": 3, + "escalated": false, + "notes": "" + }, + { + "id": "task-2-6", + "description": "Update sandbox/egg_lib/runtime.py error messages to use constants", + "status": "pending", + "acceptance_criteria": "Error messages at lines 599, 858 reference GATEWAY_PORT constant", + "files_affected": [ + "sandbox/egg_lib/runtime.py" + ], + "commit": null, + "review_cycles": 0, + "max_cycles": 3, + "escalated": false, + "notes": "" + }, + { + "id": "task-2-7", + "description": "Update shared/egg_container/__init__.py fallback to use constant", + "status": "pending", + "acceptance_criteria": "Line 114 hardcoded :3129 replaced with constant reference", + "files_affected": [ + "shared/egg_container/__init__.py" + ], + "commit": null, + "review_cycles": 0, + "max_cycles": 3, + "escalated": false, + "notes": "" + } + ], + "review_cycles": 0, + "max_cycles": 3, + "escalated": false, + "escalation_reason": null, + "review_feedback": [] + }, + { + "id": "phase-3", + "name": "Update Shell Scripts", + "status": "pending", + "tasks": [ + { + "id": "task-3-1", + "description": "Update sandbox/scripts/gh to require GATEWAY_URL environment variable", + "status": "pending", + "acceptance_criteria": "Remove hardcoded fallback on line 14; fail with clear error if GATEWAY_URL not set", + "files_affected": [ + "sandbox/scripts/gh" + ], + "commit": null, + "review_cycles": 0, + "max_cycles": 3, + "escalated": false, + "notes": "" + }, + { + "id": "task-3-2", + "description": "Update sandbox/scripts/git to require GATEWAY_URL environment variable", + "status": "pending", + "acceptance_criteria": "Remove hardcoded fallback on line 18; fail with clear error if GATEWAY_URL not set", + "files_affected": [ + "sandbox/scripts/git" + ], + "commit": null, + "review_cycles": 0, + "max_cycles": 3, + "escalated": false, + "notes": "" + }, + { + "id": "task-3-3", + "description": "Document that gateway shell scripts keep hardcoded values as source of truth", + "status": "pending", + "acceptance_criteria": "Add comment in gateway/entrypoint.sh and gateway/start-gateway.sh", + "files_affected": [ + "gateway/entrypoint.sh", + "gateway/start-gateway.sh" + ], + "commit": null, + "review_cycles": 0, + "max_cycles": 3, + "escalated": false, + "notes": "" + } + ], + "review_cycles": 0, + "max_cycles": 3, + "escalated": false, + "escalation_reason": null, + "review_feedback": [] + }, + { + "id": "phase-4", + "name": "Update Test Fixtures", + "status": "pending", + "tasks": [ + { + "id": "task-4-1", + "description": "Update integration_tests/conftest.py to use TEST_GATEWAY_PORT constant", + "status": "pending", + "acceptance_criteria": "Lines 48-49 use constants from shared/egg_config/constants.py", + "files_affected": [ + "integration_tests/conftest.py" + ], + "commit": null, + "review_cycles": 0, + "max_cycles": 3, + "escalated": false, + "notes": "" + }, + { + "id": "task-4-2", + "description": "Update tests/sandbox/test_gateway_helpers.py to use test port 1234", + "status": "pending", + "acceptance_criteria": "Mock contexts at lines 40, 356-411 use TEST_GATEWAY_PORT", + "files_affected": [ + "tests/sandbox/test_gateway_helpers.py" + ], + "commit": null, + "review_cycles": 0, + "max_cycles": 3, + "escalated": false, + "notes": "" + }, + { + "id": "task-4-3", + "description": "Update tests/sandbox/test_runtime_helpers.py to use test port 1234", + "status": "pending", + "acceptance_criteria": "Mock context at line 36 uses TEST_GATEWAY_PORT", + "files_affected": [ + "tests/sandbox/test_runtime_helpers.py" + ], + "commit": null, + "review_cycles": 0, + "max_cycles": 3, + "escalated": false, + "notes": "" + }, + { + "id": "task-4-4", + "description": "Update tests/sandbox/test_context.py assertions to use constants", + "status": "pending", + "acceptance_criteria": "Line 59 assertion uses GATEWAY_PORT constant", + "files_affected": [ + "tests/sandbox/test_context.py" + ], + "commit": null, + "review_cycles": 0, + "max_cycles": 3, + "escalated": false, + "notes": "" + }, + { + "id": "task-4-5", + "description": "Update tests/sandbox/test_entrypoint.py fixtures to use constants", + "status": "pending", + "acceptance_criteria": "Lines 677, 684, 688, 700 use constants", + "files_affected": [ + "tests/sandbox/test_entrypoint.py" + ], + "commit": null, + "review_cycles": 0, + "max_cycles": 3, + "escalated": false, + "notes": "" + }, + { + "id": "task-4-6", + "description": "Update tests/sandbox/test_contract_cli.py assertions to use constants", + "status": "pending", + "acceptance_criteria": "Line 153 assertion uses constants", + "files_affected": [ + "tests/sandbox/test_contract_cli.py" + ], + "commit": null, + "review_cycles": 0, + "max_cycles": 3, + "escalated": false, + "notes": "" + } + ], + "review_cycles": 0, + "max_cycles": 3, + "escalated": false, + "escalation_reason": null, + "review_feedback": [] + }, + { + "id": "phase-5", + "name": "Update Documentation", + "status": "pending", + "tasks": [ + { + "id": "task-5-1", + "description": "Update sandbox/.claude/rules/environment.md", + "status": "pending", + "acceptance_criteria": "Line 53 references correct port 9848 and mentions constants file", + "files_affected": [ + "sandbox/.claude/rules/environment.md" + ], + "commit": null, + "review_cycles": 0, + "max_cycles": 3, + "escalated": false, + "notes": "" + }, + { + "id": "task-5-2", + "description": "Update docs/adr/implemented/ADR-Gateway-Credential-Injection.md", + "status": "pending", + "acceptance_criteria": "All port 9847 references updated to 9848", + "files_affected": [ + "docs/adr/implemented/ADR-Gateway-Credential-Injection.md" + ], + "commit": null, + "review_cycles": 0, + "max_cycles": 3, + "escalated": false, + "notes": "" + }, + { + "id": "task-5-3", + "description": "Update docs/adr/implemented/ADR-Anthropic-API-Credential-Injection.md", + "status": "pending", + "acceptance_criteria": "All port 9847 references updated to 9848", + "files_affected": [ + "docs/adr/implemented/ADR-Anthropic-API-Credential-Injection.md" + ], + "commit": null, + "review_cycles": 0, + "max_cycles": 3, + "escalated": false, + "notes": "" + }, + { + "id": "task-5-4", + "description": "Update docs/adr/implemented/ADR-Git-Isolation-Architecture.md", + "status": "pending", + "acceptance_criteria": "All port 9847 references updated to 9848", + "files_affected": [ + "docs/adr/implemented/ADR-Git-Isolation-Architecture.md" + ], + "commit": null, + "review_cycles": 0, + "max_cycles": 3, + "escalated": false, + "notes": "" + }, + { + "id": "task-5-5", + "description": "Update docs/adr/in-progress/ADR-Internet-Tool-Access-Lockdown.md", + "status": "pending", + "acceptance_criteria": "All port 9847 references updated to 9848", + "files_affected": [ + "docs/adr/in-progress/ADR-Internet-Tool-Access-Lockdown.md" + ], + "commit": null, + "review_cycles": 0, + "max_cycles": 3, + "escalated": false, + "notes": "" + }, + { + "id": "task-5-6", + "description": "Update docs/adr/in-progress/ADR-GitHub-Actions-Support.md", + "status": "pending", + "acceptance_criteria": "Port 9847 references at lines 97, 99, 107 updated to 9848", + "files_affected": [ + "docs/adr/in-progress/ADR-GitHub-Actions-Support.md" + ], + "commit": null, + "review_cycles": 0, + "max_cycles": 3, + "escalated": false, + "notes": "" + }, + { + "id": "task-5-7", + "description": "Update gateway/tests/README-integration.md", + "status": "pending", + "acceptance_criteria": "Port references are consistent (9848)", + "files_affected": [ + "gateway/tests/README-integration.md" + ], + "commit": null, + "review_cycles": 0, + "max_cycles": 3, + "escalated": false, + "notes": "" + } + ], + "review_cycles": 0, + "max_cycles": 3, + "escalated": false, + "escalation_reason": null, + "review_feedback": [] + }, + { + "id": "phase-6", + "name": "Add CI Lint Check", + "status": "pending", + "tasks": [ + { + "id": "task-6-1", + "description": "Create lint script to detect hardcoded gateway ports", + "status": "pending", + "acceptance_criteria": "Script greps for patterns like :9847, :9848, :3129 literals; returns non-zero if found", + "files_affected": [ + "scripts/lint-hardcoded-ports.sh" + ], + "commit": null, + "review_cycles": 0, + "max_cycles": 3, + "escalated": false, + "notes": "" + }, + { + "id": "task-6-2", + "description": "Add lint check to CI workflow", + "status": "pending", + "acceptance_criteria": "CI fails if hardcoded ports detected in new code", + "files_affected": [ + ".github/workflows/ci.yml" + ], + "commit": null, + "review_cycles": 0, + "max_cycles": 3, + "escalated": false, + "notes": "" + } + ], + "review_cycles": 0, + "max_cycles": 3, + "escalated": false, + "escalation_reason": null, + "review_feedback": [] + } + ], + "decisions": [], + "circuit_breaker": { + "total_cycles": 0, + "max_total_cycles": 10, + "status": "closed" + }, + "workflow_owner": "jwbron", + "audit_log": [ + { + "timestamp": "2026-02-08T09:29:31Z", + "actor": "jwbron", + "role": "human", + "action": "transition", + "field_path": "current_phase", + "old_value": "refine", + "new_value": "plan", + "reason": "Phase approved by human" + }, + { + "timestamp": "2026-02-08T09:40:57Z", + "actor": "jwbron", + "role": "human", + "action": "transition", + "field_path": "current_phase", + "old_value": "plan", + "new_value": "implement", + "reason": "Phase approved by human" + } + ] +} diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 3ce8ca826d..ac6e5a5ff8 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -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 diff --git a/docs/adr/implemented/ADR-Anthropic-API-Credential-Injection.md b/docs/adr/implemented/ADR-Anthropic-API-Credential-Injection.md index 8ca689ce78..1479aa8639 100644 --- a/docs/adr/implemented/ADR-Anthropic-API-Credential-Injection.md +++ b/docs/adr/implemented/ADR-Anthropic-API-Credential-Injection.md @@ -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 │ │ │ │ └─────────────────────┘ │ @@ -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 │ │ │ │ │ │ │ @@ -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 diff --git a/docs/adr/implemented/ADR-Gateway-Credential-Injection.md b/docs/adr/implemented/ADR-Gateway-Credential-Injection.md index e88efe329a..1a60172a62 100644 --- a/docs/adr/implemented/ADR-Gateway-Credential-Injection.md +++ b/docs/adr/implemented/ADR-Gateway-Credential-Injection.md @@ -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 @@ -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 @@ -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) diff --git a/docs/adr/implemented/ADR-Git-Isolation-Architecture.md b/docs/adr/implemented/ADR-Git-Isolation-Architecture.md index b60710f3c9..3b760e36f7 100644 --- a/docs/adr/implemented/ADR-Git-Isolation-Architecture.md +++ b/docs/adr/implemented/ADR-Git-Isolation-Architecture.md @@ -1380,7 +1380,7 @@ 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"}' @@ -1388,7 +1388,7 @@ 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"}') diff --git a/docs/adr/in-progress/ADR-GitHub-Actions-Support.md b/docs/adr/in-progress/ADR-GitHub-Actions-Support.md index 42e96c54b7..d536c92b76 100644 --- a/docs/adr/in-progress/ADR-GitHub-Actions-Support.md +++ b/docs/adr/in-progress/ADR-GitHub-Actions-Support.md @@ -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 diff --git a/docs/adr/in-progress/ADR-Internet-Tool-Access-Lockdown.md b/docs/adr/in-progress/ADR-Internet-Tool-Access-Lockdown.md index d57d4c5a2f..079bb3d4fb 100644 --- a/docs/adr/in-progress/ADR-Internet-Tool-Access-Lockdown.md +++ b/docs/adr/in-progress/ADR-Internet-Tool-Access-Lockdown.md @@ -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 │ │ │ │ │ │ │ └─────────────┘ └────────┬────────┘ │ │ │ │ │ │ │ @@ -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 @@ -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)" @@ -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"} @@ -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 @@ -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 @@ -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"}' & diff --git a/gateway/entrypoint.sh b/gateway/entrypoint.sh index 1c2c21892c..5decc52eeb 100644 --- a/gateway/entrypoint.sh +++ b/gateway/entrypoint.sh @@ -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) ===" diff --git a/gateway/start-gateway.sh b/gateway/start-gateway.sh index b9e7274ac4..14e4be6d76 100755 --- a/gateway/start-gateway.sh +++ b/gateway/start-gateway.sh @@ -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 diff --git a/gateway/tests/README-integration.md b/gateway/tests/README-integration.md index eb0fe6c15e..703e923f77 100644 --- a/gateway/tests/README-integration.md +++ b/gateway/tests/README-integration.md @@ -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, diff --git a/integration_tests/conftest.py b/integration_tests/conftest.py index fe448297c1..9b6142e609 100644 --- a/integration_tests/conftest.py +++ b/integration_tests/conftest.py @@ -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, @@ -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. @@ -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, diff --git a/sandbox/.claude/rules/environment.md b/sandbox/.claude/rules/environment.md index d14c9b4f3d..321f1f0808 100644 --- a/sandbox/.claude/rules/environment.md +++ b/sandbox/.claude/rules/environment.md @@ -50,7 +50,8 @@ Use `git push origin ` (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 diff --git a/sandbox/egg_lib/config.py b/sandbox/egg_lib/config.py index 494f37549a..ab545648f1 100644 --- a/sandbox/egg_lib/config.py +++ b/sandbox/egg_lib/config.py @@ -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""" @@ -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: diff --git a/sandbox/egg_lib/contract_cli.py b/sandbox/egg_lib/contract_cli.py index c232991da7..c2ae15e637 100755 --- a/sandbox/egg_lib/contract_cli.py +++ b/sandbox/egg_lib/contract_cli.py @@ -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: diff --git a/sandbox/egg_lib/network_mode.py b/sandbox/egg_lib/network_mode.py index fd14917901..6b6e3aa0b3 100644 --- a/sandbox/egg_lib/network_mode.py +++ b/sandbox/egg_lib/network_mode.py @@ -18,6 +18,7 @@ import urllib.request from enum import Enum +from .config import GATEWAY_PORT from .output import info @@ -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 diff --git a/sandbox/egg_lib/runtime.py b/sandbox/egg_lib/runtime.py index feb298fb35..86747bbe2f 100644 --- a/sandbox/egg_lib/runtime.py +++ b/sandbox/egg_lib/runtime.py @@ -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 ( @@ -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 @@ -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 diff --git a/sandbox/entrypoint.py b/sandbox/entrypoint.py index be7e49f678..9e2128bcd0 100644 --- a/sandbox/entrypoint.py +++ b/sandbox/entrypoint.py @@ -30,6 +30,8 @@ from pathlib import Path from typing import Any, ClassVar +from egg_config import GATEWAY_PORT, GATEWAY_PROXY_PORT + # ============================================================================= # Startup Timing (Debug) # ============================================================================= @@ -513,7 +515,7 @@ def setup_anthropic_api(config: Config, logger: Logger) -> None: Reference: PR #701 - ANTHROPIC_BASE_URL credential injection plan """ - gateway_url = os.environ.get("GATEWAY_URL", "http://egg-gateway:9848") + gateway_url = os.environ.get("GATEWAY_URL", f"http://egg-gateway:{GATEWAY_PORT}") # Placeholder OAuth token to satisfy Claude Code's startup validation # Must match sk-ant-oat01-* format for Claude Code to accept it @@ -813,7 +815,7 @@ def check_gateway_health(config: Config, logger: Logger) -> bool: import requests from requests.exceptions import RequestException - gateway_url = os.environ.get("GATEWAY_URL", "http://egg-gateway:9848") + gateway_url = os.environ.get("GATEWAY_URL", f"http://egg-gateway:{GATEWAY_PORT}") proxy_url = os.environ.get("HTTPS_PROXY") # Parse gateway hostname from URL (supports dynamic names in GHA) @@ -917,8 +919,8 @@ def test_tcp_port(host: str, port: int, timeout: float = 2.0) -> tuple[bool, str except Exception as e: return False, f"error: {e}" - api_port = parsed.port or 9848 - proxy_port = 3129 + api_port = parsed.port or GATEWAY_PORT + proxy_port = GATEWAY_PROXY_PORT api_tcp_ok, api_tcp_msg = test_tcp_port(gateway_host, api_port) proxy_tcp_ok, proxy_tcp_msg = test_tcp_port(gateway_host, proxy_port) @@ -1096,7 +1098,7 @@ def test_tcp_port(host: str, port: int, timeout: float = 2.0) -> tuple[bool, str elif not api_health_passed: logger.error(" [API issue] TCP works but HTTP fails - gateway may be starting:") logger.error(" 1. Check gateway logs: docker logs egg-gateway") - logger.error(" 2. Test from host: curl http://localhost:9848/api/v1/health") + logger.error(f" 2. Test from host: curl http://localhost:{GATEWAY_PORT}/api/v1/health") logger.error(" 3. Verify gateway.py is running in container") elif is_private_mode: logger.error(" [Proxy issue] Gateway API works but proxy check failed:") @@ -1105,7 +1107,9 @@ def test_tcp_port(host: str, port: int, timeout: float = 2.0) -> tuple[bool, str " 2. Check Squid logs: docker exec egg-gateway cat /var/log/squid/cache.log" ) logger.error(" 3. Test proxy from host:") - logger.error(" curl -x http://localhost:3129 https://api.anthropic.com/") + logger.error( + f" curl -x http://localhost:{GATEWAY_PROXY_PORT} https://api.anthropic.com/" + ) logger.error(" 4. Verify allowed_domains.txt includes api.anthropic.com") return False diff --git a/sandbox/scripts/gh b/sandbox/scripts/gh index 228a5ab74c..3d327f9648 100755 --- a/sandbox/scripts/gh +++ b/sandbox/scripts/gh @@ -10,8 +10,16 @@ # - Read-only operations are passed through # -# Gateway runs as container on egg-network, reachable by container name -GATEWAY_URL="${GATEWAY_URL:-http://egg-gateway:9848}" +# Gateway runs as container on egg-network, reachable by container name. +# GATEWAY_URL is set by the container launcher (egg_container.build_sandbox_docker_cmd). +# We require it to be set - no hardcoded fallback to avoid port drift. +# See shared/egg_config/constants.py for port definitions. +if [ -z "$GATEWAY_URL" ]; then + echo "ERROR: GATEWAY_URL environment variable is not set." >&2 + echo "This variable must be set by the container launcher." >&2 + echo "If running manually, set GATEWAY_URL=http://egg-gateway:" >&2 + exit 1 +fi # Session token for per-container authentication (required) EGG_SESSION_TOKEN="${EGG_SESSION_TOKEN:-}" # Worktree container ID - set by egg launcher to translate container paths @@ -67,7 +75,7 @@ Please ensure: systemctl --user status gateway-sidecar 2. The container can reach the gateway: - curl http://egg-gateway:9848/api/v1/health + curl $GATEWAY_URL/api/v1/health ================================================================================ diff --git a/sandbox/scripts/git b/sandbox/scripts/git index f8af55dd8f..233ed42119 100755 --- a/sandbox/scripts/git +++ b/sandbox/scripts/git @@ -15,7 +15,16 @@ # Real git binary - relocated to hidden path (used only for gateway fallback testing) REAL_GIT=/opt/.egg-internal/git -GATEWAY_URL="${GATEWAY_URL:-http://egg-gateway:9848}" + +# GATEWAY_URL is set by the container launcher (egg_container.build_sandbox_docker_cmd). +# We require it to be set - no hardcoded fallback to avoid port drift. +# See shared/egg_config/constants.py for port definitions. +if [ -z "$GATEWAY_URL" ]; then + echo "ERROR: GATEWAY_URL environment variable is not set." >&2 + echo "This variable must be set by the container launcher." >&2 + echo "If running manually, set GATEWAY_URL=http://egg-gateway:" >&2 + exit 1 +fi CONTAINER_ID="${CONTAINER_ID:-unknown}" # shellcheck disable=SC2034 # available for future path translation CONTAINER_REPOS_DIR="${HOME}/repos" @@ -38,7 +47,7 @@ In the gateway-managed worktree architecture: - The gateway holds credentials and enforces policies Please ensure the gateway sidecar is running: - curl http://egg-gateway:9848/api/v1/health + curl $GATEWAY_URL/api/v1/health ================================================================================ diff --git a/scripts/check-hardcoded-ports.py b/scripts/check-hardcoded-ports.py new file mode 100644 index 0000000000..d118f8bd77 --- /dev/null +++ b/scripts/check-hardcoded-ports.py @@ -0,0 +1,226 @@ +#!/usr/bin/env python3 +""" +Lint check: Detect hardcoded gateway/proxy port numbers in code. + +Port values should be imported from shared/egg_config/constants.py, not +hardcoded throughout the codebase. This prevents port drift and makes +the configuration easier to maintain. + +Approved locations for port definitions: +- shared/egg_config/constants.py (the source of truth) +- gateway/*.sh (shell scripts that cannot import Python modules) +- gateway/squid.conf.template (proxy configuration) +- .github/workflows/*.yml (CI/CD workflows) + +All other code should import GATEWAY_PORT and GATEWAY_PROXY_PORT from +egg_config.constants or egg_config (which re-exports them). + +Usage: + python3 scripts/check-hardcoded-ports.py + +Exit codes: + 0 - No violations found + 1 - Found hardcoded port numbers +""" + +import re +import sys +from pathlib import Path + +# Ports to check for +GATEWAY_PORT = 9848 +PROXY_PORT = 3129 + +# Files/directories where hardcoded ports are allowed +ALLOWLIST_PATHS = [ + # Source of truth + "shared/egg_config/constants.py", + # Shell scripts cannot import Python modules + "gateway/entrypoint.sh", + "gateway/start-gateway.sh", + "gateway/setup.sh", + # Gateway Python module has its own DEFAULT_PORT (source of truth for gateway) + "gateway/gateway.py", + # Gateway tests may need hardcoded values + "gateway/tests/", + # Squid configuration template + "gateway/squid.conf.template", + "gateway/squid.conf", + # Docker compose files + "docker-compose.yml", + "docker-compose.yaml", + # Integration test infrastructure (compose files, conftest, network tests) + "integration_tests/conftest.py", + "integration_tests/docker-compose.yml", + "integration_tests/test_network_", + # CI/CD workflows (YAML cannot import Python) + ".github/workflows/", + # This lint script itself + "scripts/check-hardcoded-ports.py", + # Documentation is allowed to have examples + "docs/", + # Test data and fixtures (mock data may need specific values) + "tests/fixtures/", + # Test files that validate config defaults or use hardcoded values in assertions + "tests/egg_config/test_configs.py", + "tests/functional/conftest.py", + "tests/shared/egg_container/test_build_cmd.py", + # Integration tests that need hardcoded values + "integration_tests/test_network_isolation.py", + "integration_tests/test_network_security.py", + # Contract state files (generated JSON) + ".egg-state/", +] + +# Additional patterns that are allowed (e.g., version numbers, line numbers) +ALLOWLIST_PATTERNS = [ + # Port in a comment explaining the constant + r"#.*GATEWAY_PORT.*=.*9848", + r"#.*GATEWAY_PROXY_PORT.*=.*3129", +] + + +def is_allowlisted(file_path: Path, repo_root: Path) -> bool: + """Check if file is in the allowlist.""" + rel_path = str(file_path.relative_to(repo_root)) + + for allowed in ALLOWLIST_PATHS: + if allowed.endswith("/"): + if rel_path.startswith(allowed): + return True + else: + if rel_path == allowed: + return True + + return False + + +def check_line(line: str, lineno: int) -> list[tuple[int, str, str]]: + """Check a line for hardcoded ports. + + Returns list of (lineno, port_found, context) tuples. + """ + violations = [] + + # Skip comment-only lines and noqa lines + stripped = line.strip() + if stripped.startswith("#") or stripped.startswith("//"): + return violations + if "noqa: EGG002" in line or "noqa: hardcoded-port" in line: + return violations + + # Check for allowlisted patterns + for pattern in ALLOWLIST_PATTERNS: + if re.search(pattern, line): + return violations + + # Check for gateway port + if str(GATEWAY_PORT) in line: + # Make sure it's not part of a larger number + if re.search(rf"(? list[tuple[int, str, str]]: + """Check a file for hardcoded ports.""" + violations = [] + + try: + content = file_path.read_text() + lines = content.split("\n") + + for i, line in enumerate(lines, 1): + line_violations = check_line(line, i) + violations.extend(line_violations) + + except UnicodeDecodeError: + pass # Skip binary files + except Exception as e: + print(f"Warning: Could not check {file_path}: {e}", file=sys.stderr) + + return violations + + +def main() -> int: + """Run the hardcoded ports lint check.""" + script_dir = Path(__file__).resolve().parent + repo_root = script_dir.parent + + # File extensions to check + extensions = {".py", ".sh", ".ts", ".js", ".tsx", ".jsx", ".json", ".yml", ".yaml"} + + all_violations: list[tuple[Path, list[tuple[int, str, str]]]] = [] + + # Walk the repository + for file_path in repo_root.rglob("*"): + # Skip directories + if file_path.is_dir(): + continue + + # Skip files not in our extension list + if file_path.suffix.lower() not in extensions: + continue + + # Skip files in .git, .venv, node_modules + rel_parts = file_path.relative_to(repo_root).parts + if any( + part in {".git", ".venv", "node_modules", "__pycache__", ".mypy_cache"} + for part in rel_parts + ): + continue + + # Skip allowlisted files + if is_allowlisted(file_path, repo_root): + continue + + violations = check_file(file_path) + if violations: + rel_path = file_path.relative_to(repo_root) + all_violations.append((rel_path, violations)) + + if all_violations: + print("ERROR: Found hardcoded gateway/proxy port numbers!\n") + print("=" * 76) + print("Port values should be imported from shared/egg_config/constants.py") + print("to ensure consistency across the codebase.") + print() + print("Approved sources of truth:") + print(" - shared/egg_config/constants.py (Python code)") + print(" - gateway/*.sh (shell scripts document they match Python constants)") + print("=" * 76) + print() + + for file_path, violations in sorted(all_violations): + print(f"File: {file_path}") + for lineno, port, context in violations: + print(f" Line {lineno}: Found port {port}") + print(f" {context}") + print() + + print("How to fix:") + print(" 1. For Python code, import from egg_config:") + print(" from egg_config import GATEWAY_PORT, GATEWAY_PROXY_PORT") + print() + print(" 2. For shell scripts in sandbox/, use GATEWAY_URL env var:") + print(" GATEWAY_URL is set by the container launcher") + print() + print(" 3. For test code, use TEST_GATEWAY_PORT from egg_config.constants") + print() + print(" 4. To suppress a false positive, add: # noqa: EGG002") + print() + + return 1 + else: + print("OK: No hardcoded port numbers found") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/shared/egg_config/__init__.py b/shared/egg_config/__init__.py index d39f31094c..6a6acdbea2 100644 --- a/shared/egg_config/__init__.py +++ b/shared/egg_config/__init__.py @@ -7,9 +7,11 @@ - HealthCheckResult: Result of service health checks - ConfigRegistry: Central registry for all configs - Validators: Reusable validation functions +- Constants: Centralized gateway/sandbox constants (ports, networks, etc.) Usage: from egg_config import BaseConfig, ValidationResult, get_registry + from egg_config import GATEWAY_PORT, GATEWAY_PROXY_PORT # Constants from egg_config.validators import validate_url, mask_secret # Register a config @@ -40,6 +42,23 @@ from .configs.gateway import GatewayConfig from .configs.github import GitHubConfig from .configs.llm import LLMConfig + +# Centralized constants +from .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, + TEST_GATEWAY_PORT, + TEST_GATEWAY_PROXY_PORT, +) from .registry import ( AggregateHealthResult, AggregateValidationResult, @@ -58,11 +77,26 @@ # Registry "ConfigRegistry", "ConfigStatus", + # Constants + "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", # Service configurations "GatewayConfig", "GitHubConfig", "HealthCheckResult", "LLMConfig", + # Test constants + "TEST_GATEWAY_PORT", + "TEST_GATEWAY_PROXY_PORT", "ValidationResult", "get_local_repos", "get_registry", diff --git a/shared/egg_config/configs/gateway.py b/shared/egg_config/configs/gateway.py index bf40c6d1f6..bdc88d9409 100644 --- a/shared/egg_config/configs/gateway.py +++ b/shared/egg_config/configs/gateway.py @@ -12,6 +12,7 @@ from typing import Any from ..base import BaseConfig, HealthCheckResult, ValidationResult +from ..constants import GATEWAY_PORT from ..validators import mask_secret, validate_non_empty, validate_port @@ -40,7 +41,7 @@ class GatewayConfig(BaseConfig): """ host: str = "0.0.0.0" - port: int = 9848 + port: int = GATEWAY_PORT secret: str = "" rate_limits: RateLimitConfig = field(default_factory=RateLimitConfig) @@ -169,11 +170,11 @@ def from_env(cls) -> "GatewayConfig": # Host and port from environment config.host = os.environ.get("GATEWAY_HOST", "0.0.0.0") - port_str = os.environ.get("GATEWAY_PORT", "9848") + port_str = os.environ.get("GATEWAY_PORT", str(GATEWAY_PORT)) try: config.port = int(port_str) except ValueError: - config.port = 9848 + config.port = GATEWAY_PORT # Load launcher secret from environment env_secret = os.environ.get("EGG_LAUNCHER_SECRET", "") diff --git a/shared/egg_config/constants.py b/shared/egg_config/constants.py new file mode 100644 index 0000000000..1c75c38a07 --- /dev/null +++ b/shared/egg_config/constants.py @@ -0,0 +1,48 @@ +"""Centralized constants for egg gateway and sandbox configuration. + +This module is the single source of truth for port numbers, network names, +container names, and other constants used across the egg ecosystem. + +Usage: + from egg_config import GATEWAY_PORT, GATEWAY_PROXY_PORT + from egg_config.constants import TEST_GATEWAY_PORT # For tests only +""" + +# Gateway container constants +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 + +# Test constants - use these in unit tests to avoid coupling to production values +# Using a clearly fake port (1234) makes it obvious when tests accidentally +# connect to real services +TEST_GATEWAY_PORT = 1234 +TEST_GATEWAY_PROXY_PORT = 5678 + +__all__ = [ + "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", + "TEST_GATEWAY_PORT", + "TEST_GATEWAY_PROXY_PORT", +] diff --git a/shared/egg_container/__init__.py b/shared/egg_container/__init__.py index db44f2045b..9279bc8b24 100644 --- a/shared/egg_container/__init__.py +++ b/shared/egg_container/__init__.py @@ -10,6 +10,8 @@ from dataclasses import dataclass +from egg_config import GATEWAY_PROXY_PORT + # Index where lifecycle flags (--rm, -it, -d) should be inserted. # The returned command always has ["docker", "run", ...], so index 2 is # immediately after "run". This constant makes the convention explicit @@ -26,7 +28,7 @@ class ContainerNetworkConfig: gateway_ip: str gateway_port: int repo_mode: str # "private" or "public" - proxy_url: str | None = None # e.g. "http://egg-gateway:3129" + proxy_url: str | None = None # Full proxy URL if provided; default uses GATEWAY_PROXY_PORT def build_sandbox_docker_cmd( @@ -111,7 +113,7 @@ def build_sandbox_docker_cmd( # --- Mode-specific network settings --- if network.repo_mode == "private": - proxy = network.proxy_url or f"http://{network.gateway_hostname}:3129" + proxy = network.proxy_url or f"http://{network.gateway_hostname}:{GATEWAY_PROXY_PORT}" no_proxy = f"localhost,127.0.0.1,{network.gateway_hostname}" cmd.extend( [ diff --git a/tests/sandbox/test_context.py b/tests/sandbox/test_context.py index 5be4384cd1..976b9e4422 100644 --- a/tests/sandbox/test_context.py +++ b/tests/sandbox/test_context.py @@ -4,9 +4,13 @@ from pathlib import Path from unittest.mock import patch +# Add shared module to path for imports +shared_path = Path(__file__).parent.parent.parent / "shared" +sys.path.insert(0, str(shared_path)) sandbox_path = Path(__file__).parent.parent.parent / "sandbox" sys.path.insert(0, str(sandbox_path)) +from egg_config import GATEWAY_PORT, GATEWAY_PROXY_PORT from egg_lib.context import AUTO, RuntimeContext, get_context, set_context @@ -56,8 +60,8 @@ def test_default_launcher_secret_none(self): def test_default_ports(self): ctx = RuntimeContext() - assert ctx.gateway_port == 9848 - assert ctx.gateway_proxy_port == 3129 + assert ctx.gateway_port == GATEWAY_PORT + assert ctx.gateway_proxy_port == GATEWAY_PROXY_PORT class TestRuntimeContextFromEnvironment: diff --git a/tests/sandbox/test_contract_cli.py b/tests/sandbox/test_contract_cli.py index f4a6a3443b..2f822ed7ba 100644 --- a/tests/sandbox/test_contract_cli.py +++ b/tests/sandbox/test_contract_cli.py @@ -11,9 +11,11 @@ import pytest -# Add sandbox to path for import +# Add shared and sandbox to path for import +sys.path.insert(0, str(Path(__file__).parent.parent.parent / "shared")) sys.path.insert(0, str(Path(__file__).parent.parent.parent / "sandbox")) +from egg_config import GATEWAY_PORT from egg_lib.contract_cli import ( create_parser, format_decision_markdown, @@ -150,7 +152,7 @@ def test_get_gateway_url_default(self): """Test default gateway URL.""" with patch.dict("os.environ", {}, clear=True): url = get_gateway_url() - assert url == "http://egg-gateway:9848" + assert url == f"http://egg-gateway:{GATEWAY_PORT}" def test_get_gateway_url_from_env(self): """Test gateway URL from environment.""" diff --git a/tests/sandbox/test_entrypoint.py b/tests/sandbox/test_entrypoint.py index d98f97743c..ee4c79d0b6 100644 --- a/tests/sandbox/test_entrypoint.py +++ b/tests/sandbox/test_entrypoint.py @@ -19,6 +19,11 @@ import pytest +# Add shared module to path for imports +sys.path.insert(0, str(Path(__file__).parent.parent.parent / "shared")) + +from egg_config import GATEWAY_PORT + # Load the entrypoint module sys.path.insert(0, str(Path(__file__).parent.parent.parent / "sandbox")) import entrypoint @@ -674,18 +679,18 @@ class TestSetupAnthropicApi: def test_sets_anthropic_base_url(self, monkeypatch): """Sets ANTHROPIC_BASE_URL to gateway.""" - monkeypatch.setenv("GATEWAY_URL", "http://test-gateway:9848") + monkeypatch.setenv("GATEWAY_URL", f"http://test-gateway:{GATEWAY_PORT}") config = MagicMock() logger = entrypoint.Logger(quiet=True) entrypoint.setup_anthropic_api(config, logger) - assert os.environ["ANTHROPIC_BASE_URL"] == "http://test-gateway:9848" + assert os.environ["ANTHROPIC_BASE_URL"] == f"http://test-gateway:{GATEWAY_PORT}" def test_sets_placeholder_oauth_token(self, monkeypatch): """Sets placeholder OAuth token for Claude Code validation.""" - monkeypatch.setenv("GATEWAY_URL", "http://test-gateway:9848") + monkeypatch.setenv("GATEWAY_URL", f"http://test-gateway:{GATEWAY_PORT}") config = MagicMock() logger = entrypoint.Logger(quiet=True) @@ -697,7 +702,7 @@ def test_sets_placeholder_oauth_token(self, monkeypatch): def test_removes_api_key_from_env(self, monkeypatch): """Removes ANTHROPIC_API_KEY from environment for security.""" - monkeypatch.setenv("GATEWAY_URL", "http://test-gateway:9848") + monkeypatch.setenv("GATEWAY_URL", f"http://test-gateway:{GATEWAY_PORT}") monkeypatch.setenv("ANTHROPIC_API_KEY", "sk-test-key") config = MagicMock() diff --git a/tests/sandbox/test_gateway_helpers.py b/tests/sandbox/test_gateway_helpers.py index 8e80893580..8c8da48f0f 100644 --- a/tests/sandbox/test_gateway_helpers.py +++ b/tests/sandbox/test_gateway_helpers.py @@ -7,9 +7,13 @@ from unittest.mock import MagicMock, patch from urllib.error import URLError +# Add shared module to path for imports +shared_path = Path(__file__).parent.parent.parent / "shared" +sys.path.insert(0, str(shared_path)) sandbox_path = Path(__file__).parent.parent.parent / "sandbox" sys.path.insert(0, str(sandbox_path)) +from egg_config import TEST_GATEWAY_PORT, TEST_GATEWAY_PROXY_PORT from egg_lib.gateway import ( _get_user_git_config, _hash_directory, @@ -32,16 +36,20 @@ def _mock_context(**overrides): - """Create a mock context with sensible defaults.""" + """Create a mock context with sensible defaults. + + Uses TEST_GATEWAY_PORT (1234) to make it obvious when tests + accidentally connect to real services. + """ ctx = MagicMock() ctx.config_dir = Path("/tmp/test-config") ctx.launcher_secret = None ctx.publish_ports = True - ctx.gateway_port = 9847 + ctx.gateway_port = TEST_GATEWAY_PORT ctx.gateway_isolated_ip = "172.32.0.2" ctx.gateway_container_name = "egg-gateway" ctx.gateway_image = "egg-gateway:latest" - ctx.gateway_proxy_port = 3128 + ctx.gateway_proxy_port = TEST_GATEWAY_PROXY_PORT for k, v in overrides.items(): setattr(ctx, k, v) return ctx @@ -353,7 +361,9 @@ def _make_mock_response(self, data): def test_get_request(self): """Makes GET request with auth header.""" - ctx = _mock_context(launcher_secret="test-secret", publish_ports=True, gateway_port=9847) + ctx = _mock_context( + launcher_secret="test-secret", publish_ports=True, gateway_port=TEST_GATEWAY_PORT + ) response_data = {"success": True, "status": "ok"} mock_response = self._make_mock_response(response_data) @@ -365,7 +375,9 @@ def test_get_request(self): def test_post_request(self): """Makes POST request with JSON body.""" - ctx = _mock_context(launcher_secret="test-secret", publish_ports=True, gateway_port=9847) + ctx = _mock_context( + launcher_secret="test-secret", publish_ports=True, gateway_port=TEST_GATEWAY_PORT + ) response_data = {"success": True, "created": True} mock_response = self._make_mock_response(response_data) @@ -378,7 +390,9 @@ def test_post_request(self): def test_returns_false_without_success_key(self): """Returns False when response lacks success: true.""" - ctx = _mock_context(launcher_secret="test-secret", publish_ports=True, gateway_port=9847) + ctx = _mock_context( + launcher_secret="test-secret", publish_ports=True, gateway_port=TEST_GATEWAY_PORT + ) response_data = {"status": "ok"} # No "success" key mock_response = self._make_mock_response(response_data) @@ -389,7 +403,9 @@ def test_returns_false_without_success_key(self): def test_handles_url_error(self): """Returns failure on URL error.""" - ctx = _mock_context(launcher_secret="test-secret", publish_ports=True, gateway_port=9847) + ctx = _mock_context( + launcher_secret="test-secret", publish_ports=True, gateway_port=TEST_GATEWAY_PORT + ) with patch("egg_lib.gateway.get_context", return_value=ctx): with patch("egg_lib.gateway.urlopen", side_effect=URLError("connection refused")): success, data = launcher_api_call("/api/v1/health") @@ -397,7 +413,9 @@ def test_handles_url_error(self): def test_handles_timeout(self): """Returns failure on timeout.""" - ctx = _mock_context(launcher_secret="test-secret", publish_ports=True, gateway_port=9847) + ctx = _mock_context( + launcher_secret="test-secret", publish_ports=True, gateway_port=TEST_GATEWAY_PORT + ) with patch("egg_lib.gateway.get_context", return_value=ctx): with patch("egg_lib.gateway.urlopen", side_effect=TimeoutError("timed out")): success, data = launcher_api_call("/api/v1/health") @@ -408,7 +426,7 @@ def test_uses_container_ip_when_no_publish_ports(self): ctx = _mock_context( launcher_secret="test-secret", publish_ports=False, - gateway_port=9847, + gateway_port=TEST_GATEWAY_PORT, gateway_isolated_ip="172.32.0.2", ) response_data = {"success": True} diff --git a/tests/sandbox/test_runtime_helpers.py b/tests/sandbox/test_runtime_helpers.py index 65b8383013..7f984e554a 100644 --- a/tests/sandbox/test_runtime_helpers.py +++ b/tests/sandbox/test_runtime_helpers.py @@ -8,9 +8,13 @@ import pytest +# Add shared module to path for imports +shared_path = Path(__file__).parent.parent.parent / "shared" +sys.path.insert(0, str(shared_path)) sandbox_path = Path(__file__).parent.parent.parent / "sandbox" sys.path.insert(0, str(sandbox_path)) +from egg_config import TEST_GATEWAY_PORT, TEST_GATEWAY_PROXY_PORT from egg_lib.runtime import ( VALID_REPO_MODES, _allocate_container_ip, @@ -24,7 +28,11 @@ def _mock_context(**overrides): - """Create a mock context with sensible defaults.""" + """Create a mock context with sensible defaults. + + Uses TEST_GATEWAY_PORT (1234) to make it obvious when tests + accidentally connect to real services. + """ ctx = MagicMock() ctx.isolated_network = "egg-isolated" ctx.external_network = "egg-external" @@ -33,8 +41,8 @@ def _mock_context(**overrides): ctx.gateway_isolated_ip = "172.32.0.2" ctx.gateway_external_ip = "172.33.0.2" ctx.gateway_container_name = "egg-gateway" - ctx.gateway_port = 9847 - ctx.gateway_proxy_port = 3128 + ctx.gateway_port = TEST_GATEWAY_PORT + ctx.gateway_proxy_port = TEST_GATEWAY_PROXY_PORT for k, v in overrides.items(): setattr(ctx, k, v) return ctx