diff --git a/.github/workflows/smoke-codex.md b/.github/workflows/smoke-codex.md index 641d109b..3573cdf7 100644 --- a/.github/workflows/smoke-codex.md +++ b/.github/workflows/smoke-codex.md @@ -72,6 +72,8 @@ post-steps: echo "Safe output validation passed" --- +> **⚡ EXECUTE NOW:** Run all steps 1–8 below and call `add_comment` on this pull request with results. This is a mandatory smoke test execution—not documentation to read. + # Smoke Test: Codex Engine Validation **IMPORTANT: Keep all outputs extremely short and concise. Use single-line responses where possible. No verbose explanations.** diff --git a/containers/api-proxy/server.js b/containers/api-proxy/server.js index 9c6acc4f..4589becb 100644 --- a/containers/api-proxy/server.js +++ b/containers/api-proxy/server.js @@ -60,6 +60,14 @@ function deriveCopilotApiTarget() { try { const hostname = new URL(serverUrl).hostname; if (hostname !== 'github.com') { + // For GitHub Enterprise Cloud with data residency (*.ghe.com), + // derive the API endpoint as api.SUBDOMAIN.ghe.com + // Example: mycompany.ghe.com -> api.mycompany.ghe.com + if (hostname.endsWith('.ghe.com')) { + const subdomain = hostname.replace('.ghe.com', ''); + return `api.${subdomain}.ghe.com`; + } + // For other enterprise hosts (GHES), use the generic enterprise endpoint return 'api.enterprise.githubcopilot.com'; } } catch { @@ -387,6 +395,13 @@ function handleManagementEndpoint(req, res) { return false; } +// Export for testing +module.exports = { + deriveCopilotApiTarget, +}; + +// Only start servers if this file is run directly (not required for tests) +if (require.main === module) { // Health port is always 10000 — this is what Docker healthcheck hits const HEALTH_PORT = 10000; @@ -506,3 +521,5 @@ process.on('SIGINT', () => { logRequest('info', 'shutdown', { message: 'Received SIGINT, shutting down gracefully' }); process.exit(0); }); + +} // End of if (require.main === module) diff --git a/containers/api-proxy/server.test.js b/containers/api-proxy/server.test.js new file mode 100644 index 00000000..a38ee2e6 --- /dev/null +++ b/containers/api-proxy/server.test.js @@ -0,0 +1,95 @@ +/** + * Tests for API Proxy Server functions + */ + +const { deriveCopilotApiTarget } = require('./server'); + +describe('deriveCopilotApiTarget', () => { + let originalEnv; + + beforeEach(() => { + // Save original environment + originalEnv = { ...process.env }; + }); + + afterEach(() => { + // Restore original environment + process.env = originalEnv; + }); + + test('should return default api.githubcopilot.com when no env vars set', () => { + delete process.env.COPILOT_API_TARGET; + delete process.env.GITHUB_SERVER_URL; + + const target = deriveCopilotApiTarget(); + expect(target).toBe('api.githubcopilot.com'); + }); + + test('should use COPILOT_API_TARGET when explicitly set', () => { + process.env.COPILOT_API_TARGET = 'custom.api.example.com'; + const target = deriveCopilotApiTarget(); + expect(target).toBe('custom.api.example.com'); + }); + + test('should prioritize COPILOT_API_TARGET over GITHUB_SERVER_URL', () => { + process.env.COPILOT_API_TARGET = 'custom.api.example.com'; + process.env.GITHUB_SERVER_URL = 'https://mycompany.ghe.com'; + const target = deriveCopilotApiTarget(); + expect(target).toBe('custom.api.example.com'); + }); + + test('should return api.githubcopilot.com for github.com', () => { + process.env.GITHUB_SERVER_URL = 'https://github.com'; + const target = deriveCopilotApiTarget(); + expect(target).toBe('api.githubcopilot.com'); + }); + + test('should derive api.SUBDOMAIN.ghe.com for *.ghe.com domains', () => { + process.env.GITHUB_SERVER_URL = 'https://mycompany.ghe.com'; + const target = deriveCopilotApiTarget(); + expect(target).toBe('api.mycompany.ghe.com'); + }); + + test('should derive api.SUBDOMAIN.ghe.com for different *.ghe.com subdomain', () => { + process.env.GITHUB_SERVER_URL = 'https://acme-corp.ghe.com'; + const target = deriveCopilotApiTarget(); + expect(target).toBe('api.acme-corp.ghe.com'); + }); + + test('should use api.enterprise.githubcopilot.com for GHES (non-.ghe.com enterprise)', () => { + process.env.GITHUB_SERVER_URL = 'https://github.enterprise.com'; + const target = deriveCopilotApiTarget(); + expect(target).toBe('api.enterprise.githubcopilot.com'); + }); + + test('should use api.enterprise.githubcopilot.com for custom GHES domain', () => { + process.env.GITHUB_SERVER_URL = 'https://git.mycompany.com'; + const target = deriveCopilotApiTarget(); + expect(target).toBe('api.enterprise.githubcopilot.com'); + }); + + test('should handle GITHUB_SERVER_URL without protocol gracefully', () => { + process.env.GITHUB_SERVER_URL = 'mycompany.ghe.com'; + const target = deriveCopilotApiTarget(); + // Invalid URL, should fall back to default + expect(target).toBe('api.githubcopilot.com'); + }); + + test('should handle invalid GITHUB_SERVER_URL gracefully', () => { + process.env.GITHUB_SERVER_URL = 'not-a-valid-url'; + const target = deriveCopilotApiTarget(); + expect(target).toBe('api.githubcopilot.com'); + }); + + test('should handle GITHUB_SERVER_URL with port', () => { + process.env.GITHUB_SERVER_URL = 'https://mycompany.ghe.com:443'; + const target = deriveCopilotApiTarget(); + expect(target).toBe('api.mycompany.ghe.com'); + }); + + test('should handle GITHUB_SERVER_URL with path', () => { + process.env.GITHUB_SERVER_URL = 'https://mycompany.ghe.com/some/path'; + const target = deriveCopilotApiTarget(); + expect(target).toBe('api.mycompany.ghe.com'); + }); +}); diff --git a/docs/api-proxy-sidecar.md b/docs/api-proxy-sidecar.md index dc9b5c81..e0a092c6 100644 --- a/docs/api-proxy-sidecar.md +++ b/docs/api-proxy-sidecar.md @@ -101,6 +101,41 @@ sudo awf --enable-api-proxy \ -- your-multi-llm-tool ``` +### GitHub Enterprise Cloud (*.ghe.com) configuration + +For GitHub Enterprise Cloud with data residency (e.g., `mycompany.ghe.com`), the API proxy automatically derives the correct Copilot API endpoint: + +```bash +export COPILOT_GITHUB_TOKEN="your-token" +export GITHUB_SERVER_URL="https://mycompany.ghe.com" + +sudo -E awf --enable-api-proxy \ + --allow-domains '*.mycompany.ghe.com' \ + -- npx @github/copilot --prompt "your prompt" +``` + +**How it works:** +- The api-proxy reads `GITHUB_SERVER_URL` and extracts the subdomain +- For `*.ghe.com` domains, it automatically routes to `api.SUBDOMAIN.ghe.com` +- Example: `mycompany.ghe.com` → `api.mycompany.ghe.com` +- For other enterprise hosts (GHES), it routes to `api.enterprise.githubcopilot.com` + +**Domain matching:** +- `*.mycompany.ghe.com` matches all subdomains (e.g., `api.mycompany.ghe.com`, `github.mycompany.ghe.com`) +- Add additional domains only if your workflow needs to access other services (e.g., `mycompany.ghe.com` for the base domain) + +**Important:** Use `sudo -E` to preserve the `GITHUB_SERVER_URL` environment variable when running awf. + +You can also explicitly set the Copilot API target: + +```bash +export COPILOT_API_TARGET="api.mycompany.ghe.com" +sudo -E awf --enable-api-proxy \ + --copilot-api-target api.mycompany.ghe.com \ + --allow-domains '*.mycompany.ghe.com' \ + -- npx @github/copilot --prompt "your prompt" +``` + ## Environment variables AWF manages environment variables differently across the three containers (squid, api-proxy, agent) to ensure secure credential isolation. @@ -123,6 +158,8 @@ The API proxy sidecar receives **real credentials** and routing configuration: | `OPENAI_API_KEY` | Real API key | `--enable-api-proxy` and env set | OpenAI API key (injected into requests) | | `ANTHROPIC_API_KEY` | Real API key | `--enable-api-proxy` and env set | Anthropic API key (injected into requests) | | `COPILOT_GITHUB_TOKEN` | Real token | `--enable-api-proxy` and env set | GitHub Copilot token (injected into requests) | +| `COPILOT_API_TARGET` | Target hostname | `--copilot-api-target` flag or host env `COPILOT_API_TARGET` | Override Copilot API endpoint (default: auto-derived) | +| `GITHUB_SERVER_URL` | GitHub server URL | Passed from host env | Auto-derives Copilot API endpoint for enterprise (*.ghe.com → api.SUBDOMAIN.ghe.com) | | `HTTP_PROXY` | `http://172.30.0.10:3128` | Always | Routes through Squid for domain filtering | | `HTTPS_PROXY` | `http://172.30.0.10:3128` | Always | Routes through Squid for domain filtering |