Skip to content
Open
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
2 changes: 2 additions & 0 deletions .github/workflows/smoke-codex.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.**
Expand Down
17 changes: 17 additions & 0 deletions containers/api-proxy/server.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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;

Expand Down Expand Up @@ -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)
95 changes: 95 additions & 0 deletions containers/api-proxy/server.test.js
Original file line number Diff line number Diff line change
@@ -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');
});
});
37 changes: 37 additions & 0 deletions docs/api-proxy-sidecar.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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 |

Expand Down
Loading