Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
7 changes: 5 additions & 2 deletions containers/api-proxy/server.js
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,8 @@ function deriveCopilotApiTarget() {
return process.env.COPILOT_API_TARGET;
}
// Auto-derive from GITHUB_SERVER_URL:
// - GitHub Enterprise Cloud (*.ghe.com) → api.<subdomain>.ghe.com
// - GitHub Enterprise Cloud (*.ghe.com): Copilot inference/models/MCP are served at
// copilot-api.<subdomain>.ghe.com (separate from the GitHub REST API at api.*)
// - GitHub Enterprise Server (non-github.com, non-ghe.com) → api.enterprise.githubcopilot.com
// - github.com → api.githubcopilot.com
const serverUrl = process.env.GITHUB_SERVER_URL;
Expand All @@ -69,7 +70,9 @@ function deriveCopilotApiTarget() {
if (hostname.endsWith('.ghe.com')) {
// Extract subdomain: mycompany.ghe.com → mycompany
const subdomain = hostname.slice(0, -8); // Remove '.ghe.com'
return `api.${subdomain}.ghe.com`;
// GHEC routes Copilot inference to copilot-api.<subdomain>.ghe.com,
// not to api.<subdomain>.ghe.com (which is the GitHub REST API)
return `copilot-api.${subdomain}.ghe.com`;
}
// GHES (any other non-github.com hostname)
return 'api.enterprise.githubcopilot.com';
Expand Down
10 changes: 5 additions & 5 deletions containers/api-proxy/server.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,24 +46,24 @@ describe('deriveCopilotApiTarget', () => {
});

describe('GitHub Enterprise Cloud (*.ghe.com)', () => {
it('should derive api.<subdomain>.ghe.com for GHEC tenants', () => {
it('should derive copilot-api.<subdomain>.ghe.com for GHEC tenants', () => {
process.env.GITHUB_SERVER_URL = 'https://mycompany.ghe.com';
expect(deriveCopilotApiTarget()).toBe('api.mycompany.ghe.com');
expect(deriveCopilotApiTarget()).toBe('copilot-api.mycompany.ghe.com');
});

it('should handle GHEC URLs with trailing slash', () => {
process.env.GITHUB_SERVER_URL = 'https://example.ghe.com/';
expect(deriveCopilotApiTarget()).toBe('api.example.ghe.com');
expect(deriveCopilotApiTarget()).toBe('copilot-api.example.ghe.com');
});

it('should handle GHEC URLs with path components', () => {
process.env.GITHUB_SERVER_URL = 'https://acme.ghe.com/some/path';
expect(deriveCopilotApiTarget()).toBe('api.acme.ghe.com');
expect(deriveCopilotApiTarget()).toBe('copilot-api.acme.ghe.com');
});

it('should handle multi-part subdomain for GHEC', () => {
process.env.GITHUB_SERVER_URL = 'https://dev.mycompany.ghe.com';
expect(deriveCopilotApiTarget()).toBe('api.dev.mycompany.ghe.com');
expect(deriveCopilotApiTarget()).toBe('copilot-api.dev.mycompany.ghe.com');
});
});

Expand Down
32 changes: 19 additions & 13 deletions docs/enterprise-configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,14 @@ When `GITHUB_SERVER_URL` is set to a `*.ghe.com` domain, AWF automatically deriv

```bash
# Example: GITHUB_SERVER_URL=https://acme.ghe.com
# AWF automatically uses: api.acme.ghe.com
# AWF automatically uses: copilot-api.acme.ghe.com
```

**How it works:**
1. AWF reads `GITHUB_SERVER_URL` from your environment
2. Detects that the hostname ends with `.ghe.com`
3. Extracts the subdomain (e.g., `acme` from `acme.ghe.com`)
4. Routes Copilot API traffic to `api.<subdomain>.ghe.com`
4. Routes Copilot API traffic to `copilot-api.<subdomain>.ghe.com`
5. **Auto-injects `GH_HOST` environment variable** in the agent container so the `gh` CLI targets your GHEC instance

**GH_HOST Auto-Injection:**
Expand All @@ -45,16 +45,22 @@ export GITHUB_SERVER_URL="https://acme.ghe.com"
export GITHUB_TOKEN="<your-copilot-cli-token>"

sudo -E awf \
--allow-domains acme.ghe.com,api.acme.ghe.com,raw.githubusercontent.com \
--allow-domains acme.ghe.com,copilot-api.acme.ghe.com,copilot-telemetry-service.acme.ghe.com,raw.githubusercontent.com \
--enable-api-proxy \
-- npx @github/copilot@latest --prompt "your prompt here"
```

**Domain breakdown:**
- `acme.ghe.com` - Your GHEC tenant domain (git operations, web UI)
- `api.acme.ghe.com` - Your tenant-specific Copilot API endpoint (automatically routed by AWF)
- `api.acme.ghe.com` - GitHub REST API endpoint (AWF auto-adds this for `*.ghe.com` instances)
- `copilot-api.acme.ghe.com` - Copilot inference, models, and MCP endpoint (AWF auto-adds this for `*.ghe.com` instances)
- `copilot-telemetry-service.acme.ghe.com` - Copilot telemetry endpoint (AWF auto-adds this for `*.ghe.com` instances)
- `raw.githubusercontent.com` - Raw content access (if using GitHub MCP server)

:::note
AWF automatically adds `api.<slug>.ghe.com`, `copilot-api.<slug>.ghe.com`, and `copilot-telemetry-service.<slug>.ghe.com` to the firewall allowlist when `GITHUB_SERVER_URL` points to a `*.ghe.com` domain. They are listed above for transparency; you do not need to include them manually.
:::

### GitHub Actions (GHEC)

In GitHub Actions workflows running on GHEC, the `GITHUB_SERVER_URL` environment variable is automatically set by GitHub Actions. No additional configuration is needed:
Expand All @@ -73,7 +79,7 @@ jobs:
# GITHUB_SERVER_URL is automatically set by GitHub Actions
run: |
sudo -E awf \
--allow-domains ${{ github.server_url_hostname }},api.${{ github.server_url_hostname }},raw.githubusercontent.com \
--allow-domains ${{ github.server_url_hostname }},copilot-api.${{ github.server_url_hostname }},copilot-telemetry-service.${{ github.server_url_hostname }},raw.githubusercontent.com \
Comment thread
lpcox marked this conversation as resolved.
Outdated
--enable-api-proxy \
-- npx @github/copilot@latest --prompt "generate tests"
```
Expand Down Expand Up @@ -114,7 +120,7 @@ export GITHUB_TOKEN="<your-copilot-cli-token>"
export GITHUB_PERSONAL_ACCESS_TOKEN="<your-github-pat>"

sudo -E awf \
--allow-domains acme.ghe.com,api.acme.ghe.com,raw.githubusercontent.com,registry.npmjs.org \
--allow-domains acme.ghe.com,copilot-api.acme.ghe.com,copilot-telemetry-service.acme.ghe.com,raw.githubusercontent.com,registry.npmjs.org \
--enable-api-proxy \
"npx @github/copilot@latest \
--disable-builtin-mcps \
Expand Down Expand Up @@ -211,8 +217,8 @@ If automatic detection doesn't work for your setup, you can manually specify the
```bash
# For GHEC with custom configuration
sudo awf \
--allow-domains acme.ghe.com,api.acme.ghe.com \
--copilot-api-target api.acme.ghe.com \
--allow-domains acme.ghe.com,copilot-api.acme.ghe.com,copilot-telemetry-service.acme.ghe.com \
--copilot-api-target copilot-api.acme.ghe.com \
--enable-api-proxy \
-- your-command

Expand All @@ -231,7 +237,7 @@ The `--copilot-api-target` flag takes precedence over automatic detection.
AWF determines the Copilot API endpoint in this order:

1. **`--copilot-api-target` flag** (highest priority) - Manual override
2. **`GITHUB_SERVER_URL` with `*.ghe.com`** - Automatic GHEC detection → `api.<subdomain>.ghe.com`
2. **`GITHUB_SERVER_URL` with `*.ghe.com`** - Automatic GHEC detection → `copilot-api.<subdomain>.ghe.com`
3. **`GITHUB_SERVER_URL` non-github.com** - Automatic GHES detection → `api.enterprise.githubcopilot.com`
4. **Default** - Public GitHub → `api.githubcopilot.com`

Expand All @@ -252,7 +258,7 @@ Add `--keep-containers` to inspect the configuration:

```bash
sudo -E awf \
--allow-domains acme.ghe.com,api.acme.ghe.com \
--allow-domains acme.ghe.com,copilot-api.acme.ghe.com \
--enable-api-proxy \
--keep-containers \
-- npx @github/copilot@latest --prompt "test"
Expand All @@ -265,7 +271,7 @@ sudo -E awf \
docker logs awf-api-proxy | grep "Copilot proxy"

# Expected for GHEC:
# Copilot proxy listening on port 10002 (target: api.acme.ghe.com)
# Copilot proxy listening on port 10002 (target: copilot-api.acme.ghe.com)

# Expected for GHES:
# Copilot proxy listening on port 10002 (target: api.enterprise.githubcopilot.com)
Expand Down Expand Up @@ -304,7 +310,7 @@ sudo cat /tmp/squid-logs-*/access.log | grep TCP_DENIED

# Add the blocked domain to your allowlist
sudo -E awf \
--allow-domains acme.ghe.com,api.acme.ghe.com,<blocked-domain> \
--allow-domains acme.ghe.com,copilot-api.acme.ghe.com,copilot-telemetry-service.acme.ghe.com,<blocked-domain> \
--enable-api-proxy \
-- your-command
```
Expand Down Expand Up @@ -366,7 +372,7 @@ docker pull ghcr.io/github/github-mcp-server:latest

# 4. Run Copilot with AWF
sudo -E awf \
--allow-domains acme.ghe.com,api.acme.ghe.com,raw.githubusercontent.com,registry.npmjs.org \
--allow-domains acme.ghe.com,copilot-api.acme.ghe.com,copilot-telemetry-service.acme.ghe.com,raw.githubusercontent.com,registry.npmjs.org \
--enable-api-proxy \
"npx @github/copilot@latest \
--disable-builtin-mcps \
Expand Down
10 changes: 9 additions & 1 deletion src/cli.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,14 +48,14 @@

afterEach(() => {
// Clean up the test directory
if (fs.existsSync(testDir)) {

Check warning on line 51 in src/cli.test.ts

View workflow job for this annotation

GitHub Actions / Build and Lint (Node 20)

Found existsSync from package "fs" with non literal argument at index 0

Check warning on line 51 in src/cli.test.ts

View workflow job for this annotation

GitHub Actions / ESLint

Found existsSync from package "fs" with non literal argument at index 0

Check warning on line 51 in src/cli.test.ts

View workflow job for this annotation

GitHub Actions / Build and Lint (Node 22)

Found existsSync from package "fs" with non literal argument at index 0
fs.rmSync(testDir, { recursive: true, force: true });
}
});

it('should parse domains from file with one domain per line', () => {
const filePath = path.join(testDir, 'domains.txt');
fs.writeFileSync(filePath, 'github.com\napi.github.com\nnpmjs.org');

Check warning on line 58 in src/cli.test.ts

View workflow job for this annotation

GitHub Actions / Build and Lint (Node 20)

Found writeFileSync from package "fs" with non literal argument at index 0

Check warning on line 58 in src/cli.test.ts

View workflow job for this annotation

GitHub Actions / ESLint

Found writeFileSync from package "fs" with non literal argument at index 0

Check warning on line 58 in src/cli.test.ts

View workflow job for this annotation

GitHub Actions / Build and Lint (Node 22)

Found writeFileSync from package "fs" with non literal argument at index 0

const result = parseDomainsFile(filePath);

Expand All @@ -64,7 +64,7 @@

it('should parse comma-separated domains from file', () => {
const filePath = path.join(testDir, 'domains.txt');
fs.writeFileSync(filePath, 'github.com, api.github.com, npmjs.org');

Check warning on line 67 in src/cli.test.ts

View workflow job for this annotation

GitHub Actions / Build and Lint (Node 20)

Found writeFileSync from package "fs" with non literal argument at index 0

Check warning on line 67 in src/cli.test.ts

View workflow job for this annotation

GitHub Actions / ESLint

Found writeFileSync from package "fs" with non literal argument at index 0

Check warning on line 67 in src/cli.test.ts

View workflow job for this annotation

GitHub Actions / Build and Lint (Node 22)

Found writeFileSync from package "fs" with non literal argument at index 0

const result = parseDomainsFile(filePath);

Expand All @@ -73,7 +73,7 @@

it('should handle mixed formats (lines and commas)', () => {
const filePath = path.join(testDir, 'domains.txt');
fs.writeFileSync(filePath, 'github.com\napi.github.com, npmjs.org\nexample.com');

Check warning on line 76 in src/cli.test.ts

View workflow job for this annotation

GitHub Actions / Build and Lint (Node 20)

Found writeFileSync from package "fs" with non literal argument at index 0

Check warning on line 76 in src/cli.test.ts

View workflow job for this annotation

GitHub Actions / ESLint

Found writeFileSync from package "fs" with non literal argument at index 0

Check warning on line 76 in src/cli.test.ts

View workflow job for this annotation

GitHub Actions / Build and Lint (Node 22)

Found writeFileSync from package "fs" with non literal argument at index 0

const result = parseDomainsFile(filePath);

Expand All @@ -82,7 +82,7 @@

it('should skip empty lines', () => {
const filePath = path.join(testDir, 'domains.txt');
fs.writeFileSync(filePath, 'github.com\n\n\napi.github.com\n\nnpmjs.org');

Check warning on line 85 in src/cli.test.ts

View workflow job for this annotation

GitHub Actions / Build and Lint (Node 20)

Found writeFileSync from package "fs" with non literal argument at index 0

Check warning on line 85 in src/cli.test.ts

View workflow job for this annotation

GitHub Actions / ESLint

Found writeFileSync from package "fs" with non literal argument at index 0

Check warning on line 85 in src/cli.test.ts

View workflow job for this annotation

GitHub Actions / Build and Lint (Node 22)

Found writeFileSync from package "fs" with non literal argument at index 0

const result = parseDomainsFile(filePath);

Expand All @@ -91,7 +91,7 @@

it('should skip lines with only whitespace', () => {
const filePath = path.join(testDir, 'domains.txt');
fs.writeFileSync(filePath, 'github.com\n \n\t\napi.github.com');

Check warning on line 94 in src/cli.test.ts

View workflow job for this annotation

GitHub Actions / Build and Lint (Node 20)

Found writeFileSync from package "fs" with non literal argument at index 0

Check warning on line 94 in src/cli.test.ts

View workflow job for this annotation

GitHub Actions / ESLint

Found writeFileSync from package "fs" with non literal argument at index 0

Check warning on line 94 in src/cli.test.ts

View workflow job for this annotation

GitHub Actions / Build and Lint (Node 22)

Found writeFileSync from package "fs" with non literal argument at index 0

const result = parseDomainsFile(filePath);

Expand All @@ -100,7 +100,7 @@

it('should skip comments starting with #', () => {
const filePath = path.join(testDir, 'domains.txt');
fs.writeFileSync(filePath, '# This is a comment\ngithub.com\n# Another comment\napi.github.com');

Check warning on line 103 in src/cli.test.ts

View workflow job for this annotation

GitHub Actions / Build and Lint (Node 20)

Found writeFileSync from package "fs" with non literal argument at index 0

Check warning on line 103 in src/cli.test.ts

View workflow job for this annotation

GitHub Actions / ESLint

Found writeFileSync from package "fs" with non literal argument at index 0

Check warning on line 103 in src/cli.test.ts

View workflow job for this annotation

GitHub Actions / Build and Lint (Node 22)

Found writeFileSync from package "fs" with non literal argument at index 0

const result = parseDomainsFile(filePath);

Expand All @@ -109,7 +109,7 @@

it('should handle inline comments (after domain)', () => {
const filePath = path.join(testDir, 'domains.txt');
fs.writeFileSync(filePath, 'github.com # GitHub main domain\napi.github.com # API endpoint');

Check warning on line 112 in src/cli.test.ts

View workflow job for this annotation

GitHub Actions / Build and Lint (Node 20)

Found writeFileSync from package "fs" with non literal argument at index 0

Check warning on line 112 in src/cli.test.ts

View workflow job for this annotation

GitHub Actions / ESLint

Found writeFileSync from package "fs" with non literal argument at index 0

Check warning on line 112 in src/cli.test.ts

View workflow job for this annotation

GitHub Actions / Build and Lint (Node 22)

Found writeFileSync from package "fs" with non literal argument at index 0

const result = parseDomainsFile(filePath);

Expand All @@ -118,7 +118,7 @@

it('should handle domains with inline comments in comma-separated format', () => {
const filePath = path.join(testDir, 'domains.txt');
fs.writeFileSync(filePath, 'github.com, api.github.com # GitHub domains\nnpmjs.org');

Check warning on line 121 in src/cli.test.ts

View workflow job for this annotation

GitHub Actions / Build and Lint (Node 20)

Found writeFileSync from package "fs" with non literal argument at index 0

Check warning on line 121 in src/cli.test.ts

View workflow job for this annotation

GitHub Actions / ESLint

Found writeFileSync from package "fs" with non literal argument at index 0

Check warning on line 121 in src/cli.test.ts

View workflow job for this annotation

GitHub Actions / Build and Lint (Node 22)

Found writeFileSync from package "fs" with non literal argument at index 0

const result = parseDomainsFile(filePath);

Expand All @@ -133,7 +133,7 @@

it('should return empty array for file with only comments and whitespace', () => {
const filePath = path.join(testDir, 'domains.txt');
fs.writeFileSync(filePath, '# Comment 1\n\n# Comment 2\n \n');

Check warning on line 136 in src/cli.test.ts

View workflow job for this annotation

GitHub Actions / Build and Lint (Node 20)

Found writeFileSync from package "fs" with non literal argument at index 0

Check warning on line 136 in src/cli.test.ts

View workflow job for this annotation

GitHub Actions / ESLint

Found writeFileSync from package "fs" with non literal argument at index 0

Check warning on line 136 in src/cli.test.ts

View workflow job for this annotation

GitHub Actions / Build and Lint (Node 22)

Found writeFileSync from package "fs" with non literal argument at index 0

const result = parseDomainsFile(filePath);

Expand Down Expand Up @@ -2236,19 +2236,25 @@
const domains = extractGhecDomainsFromServerUrl({ GITHUB_SERVER_URL: 'https://myorg.ghe.com' });
expect(domains).toContain('myorg.ghe.com');
expect(domains).toContain('api.myorg.ghe.com');
expect(domains).toHaveLength(2);
expect(domains).toContain('copilot-api.myorg.ghe.com');
Comment thread
lpcox marked this conversation as resolved.
Outdated
expect(domains).toContain('copilot-telemetry-service.myorg.ghe.com');
expect(domains).toHaveLength(4);
});

it('should handle GITHUB_SERVER_URL with trailing slash', () => {
const domains = extractGhecDomainsFromServerUrl({ GITHUB_SERVER_URL: 'https://myorg.ghe.com/' });
expect(domains).toContain('myorg.ghe.com');
expect(domains).toContain('api.myorg.ghe.com');
expect(domains).toContain('copilot-api.myorg.ghe.com');
expect(domains).toContain('copilot-telemetry-service.myorg.ghe.com');
});

it('should handle GITHUB_SERVER_URL with path components', () => {
const domains = extractGhecDomainsFromServerUrl({ GITHUB_SERVER_URL: 'https://acme.ghe.com/some/path' });
expect(domains).toContain('acme.ghe.com');
expect(domains).toContain('api.acme.ghe.com');
expect(domains).toContain('copilot-api.acme.ghe.com');
expect(domains).toContain('copilot-telemetry-service.acme.ghe.com');
});

it('should extract from GITHUB_API_URL for GHEC', () => {
Expand Down Expand Up @@ -2296,6 +2302,8 @@
resolveApiTargetsToAllowedDomains({}, domains, env);
expect(domains).toContain('myorg.ghe.com');
expect(domains).toContain('api.myorg.ghe.com');
expect(domains).toContain('copilot-api.myorg.ghe.com');
expect(domains).toContain('copilot-telemetry-service.myorg.ghe.com');
});

it('should not duplicate GHEC domains if already in allowlist', () => {
Expand Down
6 changes: 5 additions & 1 deletion src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -397,10 +397,14 @@ export function extractGhecDomainsFromServerUrl(
try {
const hostname = new URL(serverUrl).hostname;
if (hostname !== 'github.com' && hostname.endsWith('.ghe.com')) {
// GHEC tenant: add the tenant domain and its API subdomain
// GHEC tenant with data residency: add the tenant domain, API subdomain,
// Copilot inference subdomain, and Copilot telemetry subdomain.
// e.g., company.ghe.com → company.ghe.com + api.company.ghe.com
// + copilot-api.company.ghe.com + copilot-telemetry-service.company.ghe.com
Comment thread
lpcox marked this conversation as resolved.
domains.push(hostname);
domains.push(`api.${hostname}`);
domains.push(`copilot-api.${hostname}`);
domains.push(`copilot-telemetry-service.${hostname}`);
}
} catch {
// Invalid URL — skip
Expand Down
Loading