Skip to content

Commit 58e6283

Browse files
Claudelpcox
andauthored
feat(cli): auto-populate GHES firewall domains from engine.api-target (#1306)
* Initial plan * feat(cli): add GHES domain auto-population from engine.api-target Co-authored-by: lpcox <15877973+lpcox@users.noreply.github.com> * feat(cli): add GHES auto-populate integration test and docs Co-authored-by: lpcox <15877973+lpcox@users.noreply.github.com> --------- Co-authored-by: anthropic-code-agent[bot] <242468646+Claude@users.noreply.github.com> Co-authored-by: lpcox <15877973+lpcox@users.noreply.github.com>
1 parent 02d71c3 commit 58e6283

4 files changed

Lines changed: 395 additions & 1 deletion

File tree

docs/enterprise-configuration.md

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,37 @@ When `GITHUB_SERVER_URL` is set to a non-github.com, non-ghe.com domain, AWF aut
124124
# AWF automatically uses: api.enterprise.githubcopilot.com
125125
```
126126

127+
### Auto-Population for GitHub Agentic Workflows
128+
129+
**New in v0.24.0:** When running agentic workflows with `engine.api-target` set (via the `ENGINE_API_TARGET` environment variable), AWF automatically adds GHES domains to the firewall allowlist. You no longer need to manually specify these domains in `--allow-domains` or `GH_AW_ALLOWED_DOMAINS`.
130+
131+
**Auto-added domains:**
132+
- The GHES base domain (e.g., `github.mycompany.com` from `https://api.github.mycompany.com`)
133+
- The GHES API subdomain (e.g., `api.github.mycompany.com`)
134+
- Copilot API domains required even on GHES:
135+
- `api.githubcopilot.com`
136+
- `api.enterprise.githubcopilot.com`
137+
- `telemetry.enterprise.githubcopilot.com`
138+
139+
**Example:**
140+
```bash
141+
# When ENGINE_API_TARGET=https://api.github.mycompany.com
142+
# AWF automatically adds these to the allowlist:
143+
# - github.mycompany.com
144+
# - api.github.mycompany.com
145+
# - api.githubcopilot.com
146+
# - api.enterprise.githubcopilot.com
147+
# - telemetry.enterprise.githubcopilot.com
148+
149+
# Before (manual configuration):
150+
export ENGINE_API_TARGET="https://api.github.mycompany.com"
151+
export GH_AW_ALLOWED_DOMAINS="github.mycompany.com,api.github.mycompany.com,api.githubcopilot.com,api.enterprise.githubcopilot.com,telemetry.enterprise.githubcopilot.com"
152+
153+
# After (automatic):
154+
export ENGINE_API_TARGET="https://api.github.mycompany.com"
155+
# No need to set GH_AW_ALLOWED_DOMAINS - domains are auto-populated!
156+
```
157+
127158
### Required Domains for GHES
128159

129160
```bash

src/cli.test.ts

Lines changed: 84 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { Command } from 'commander';
2-
import { parseEnvironmentVariables, parseDomains, parseDomainsFile, escapeShellArg, joinShellArgs, parseVolumeMounts, isValidIPv4, isValidIPv6, parseDnsServers, parseDnsOverHttps, validateAgentImage, isAgentImagePreset, AGENT_IMAGE_PRESETS, processAgentImageOption, processLocalhostKeyword, validateSkipPullWithBuildLocal, validateAllowHostPorts, parseMemoryLimit, validateFormat, validateApiProxyConfig, buildRateLimitConfig, validateRateLimitFlags, hasRateLimitOptions, collectRulesetFile, validateApiTargetInAllowedDomains, DEFAULT_OPENAI_API_TARGET, DEFAULT_ANTHROPIC_API_TARGET, DEFAULT_COPILOT_API_TARGET, emitApiProxyTargetWarnings, formatItem, program, parseAgentTimeout, applyAgentTimeout, handlePredownloadAction, resolveApiTargetsToAllowedDomains } from './cli';
2+
import { parseEnvironmentVariables, parseDomains, parseDomainsFile, escapeShellArg, joinShellArgs, parseVolumeMounts, isValidIPv4, isValidIPv6, parseDnsServers, parseDnsOverHttps, validateAgentImage, isAgentImagePreset, AGENT_IMAGE_PRESETS, processAgentImageOption, processLocalhostKeyword, validateSkipPullWithBuildLocal, validateAllowHostPorts, parseMemoryLimit, validateFormat, validateApiProxyConfig, buildRateLimitConfig, validateRateLimitFlags, hasRateLimitOptions, collectRulesetFile, validateApiTargetInAllowedDomains, DEFAULT_OPENAI_API_TARGET, DEFAULT_ANTHROPIC_API_TARGET, DEFAULT_COPILOT_API_TARGET, emitApiProxyTargetWarnings, formatItem, program, parseAgentTimeout, applyAgentTimeout, handlePredownloadAction, resolveApiTargetsToAllowedDomains, extractGhesDomainsFromEngineApiTarget } from './cli';
33
import { redactSecrets } from './redact-secrets';
44
import * as fs from 'fs';
55
import * as path from 'path';
@@ -2175,4 +2175,87 @@ describe('cli', () => {
21752175
expect(hasRateLimitOptions({ rateLimit: true })).toBe(false);
21762176
});
21772177
});
2178+
2179+
describe('extractGhesDomainsFromEngineApiTarget', () => {
2180+
it('should return empty array when ENGINE_API_TARGET is not set', () => {
2181+
const domains = extractGhesDomainsFromEngineApiTarget({});
2182+
expect(domains).toEqual([]);
2183+
});
2184+
2185+
it('should extract GHES domains from api.github.* format', () => {
2186+
const env = { ENGINE_API_TARGET: 'https://api.github.mycompany.com' };
2187+
const domains = extractGhesDomainsFromEngineApiTarget(env);
2188+
expect(domains).toContain('github.mycompany.com');
2189+
expect(domains).toContain('api.github.mycompany.com');
2190+
expect(domains).toContain('api.githubcopilot.com');
2191+
expect(domains).toContain('api.enterprise.githubcopilot.com');
2192+
expect(domains).toContain('telemetry.enterprise.githubcopilot.com');
2193+
});
2194+
2195+
it('should handle non-api.* hostnames', () => {
2196+
const env = { ENGINE_API_TARGET: 'https://github.mycompany.com' };
2197+
const domains = extractGhesDomainsFromEngineApiTarget(env);
2198+
expect(domains).toContain('github.mycompany.com');
2199+
expect(domains).toContain('api.githubcopilot.com');
2200+
expect(domains).toContain('api.enterprise.githubcopilot.com');
2201+
expect(domains).toContain('telemetry.enterprise.githubcopilot.com');
2202+
});
2203+
2204+
it('should handle invalid URL gracefully', () => {
2205+
const env = { ENGINE_API_TARGET: 'not-a-valid-url' };
2206+
const domains = extractGhesDomainsFromEngineApiTarget(env);
2207+
expect(domains).toEqual([]);
2208+
});
2209+
2210+
it('should always include Copilot API domains for GHES', () => {
2211+
const env = { ENGINE_API_TARGET: 'https://api.github.enterprise.local' };
2212+
const domains = extractGhesDomainsFromEngineApiTarget(env);
2213+
expect(domains).toContain('api.githubcopilot.com');
2214+
expect(domains).toContain('api.enterprise.githubcopilot.com');
2215+
expect(domains).toContain('telemetry.enterprise.githubcopilot.com');
2216+
});
2217+
});
2218+
2219+
describe('resolveApiTargetsToAllowedDomains with GHES', () => {
2220+
it('should auto-add GHES domains when ENGINE_API_TARGET is set', () => {
2221+
const domains: string[] = ['github.com'];
2222+
const env = { ENGINE_API_TARGET: 'https://api.github.mycompany.com' };
2223+
resolveApiTargetsToAllowedDomains({}, domains, env);
2224+
expect(domains).toContain('github.mycompany.com');
2225+
expect(domains).toContain('api.github.mycompany.com');
2226+
expect(domains).toContain('api.githubcopilot.com');
2227+
expect(domains).toContain('api.enterprise.githubcopilot.com');
2228+
expect(domains).toContain('telemetry.enterprise.githubcopilot.com');
2229+
});
2230+
2231+
it('should not duplicate GHES domains if already in allowlist', () => {
2232+
const domains: string[] = ['github.mycompany.com', 'api.githubcopilot.com'];
2233+
const env = { ENGINE_API_TARGET: 'https://api.github.mycompany.com' };
2234+
resolveApiTargetsToAllowedDomains({}, domains, env);
2235+
const ghesCount = domains.filter(d => d === 'github.mycompany.com').length;
2236+
const copilotCount = domains.filter(d => d === 'api.githubcopilot.com').length;
2237+
expect(ghesCount).toBe(1);
2238+
expect(copilotCount).toBe(1);
2239+
});
2240+
2241+
it('should combine GHES domains with API target domains', () => {
2242+
const domains: string[] = [];
2243+
const env = { ENGINE_API_TARGET: 'https://api.github.mycompany.com' };
2244+
resolveApiTargetsToAllowedDomains(
2245+
{ copilotApiTarget: 'custom.copilot.com' },
2246+
domains,
2247+
env
2248+
);
2249+
// GHES domains
2250+
expect(domains).toContain('github.mycompany.com');
2251+
expect(domains).toContain('api.github.mycompany.com');
2252+
// Copilot API domains
2253+
expect(domains).toContain('api.githubcopilot.com');
2254+
expect(domains).toContain('api.enterprise.githubcopilot.com');
2255+
expect(domains).toContain('telemetry.enterprise.githubcopilot.com');
2256+
// Custom API target
2257+
expect(domains).toContain('custom.copilot.com');
2258+
expect(domains).toContain('https://custom.copilot.com');
2259+
});
2260+
});
21782261
});

src/cli.ts

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -378,6 +378,52 @@ export function emitApiProxyTargetWarnings(
378378
}
379379
}
380380

381+
/**
382+
* Extracts GHES API domains from engine.api-target environment variable.
383+
* When engine.api-target is set (indicating GHES), returns the GHES hostname,
384+
* API subdomain, and required Copilot API domains.
385+
*
386+
* @param env - Environment variables (defaults to process.env)
387+
* @returns Array of domains to auto-add to allowlist, or empty array if not GHES
388+
*/
389+
export function extractGhesDomainsFromEngineApiTarget(
390+
env: Record<string, string | undefined> = process.env
391+
): string[] {
392+
const engineApiTarget = env['ENGINE_API_TARGET'];
393+
if (!engineApiTarget) {
394+
return [];
395+
}
396+
397+
const domains: string[] = [];
398+
399+
try {
400+
// Parse the engine.api-target URL (e.g., https://api.github.mycompany.com)
401+
const url = new URL(engineApiTarget);
402+
const hostname = url.hostname;
403+
404+
// Extract the base GHES domain from api.github.<ghes-domain>
405+
// For example: api.github.mycompany.com → github.mycompany.com
406+
if (hostname.startsWith('api.')) {
407+
const baseDomain = hostname.substring(4); // Remove 'api.' prefix
408+
domains.push(baseDomain);
409+
domains.push(hostname); // Also add the api subdomain itself
410+
} else {
411+
// If it doesn't start with 'api.', just add the hostname
412+
domains.push(hostname);
413+
}
414+
415+
// Add Copilot API domains (needed even on GHES since Copilot models run in GitHub's cloud)
416+
domains.push('api.githubcopilot.com');
417+
domains.push('api.enterprise.githubcopilot.com');
418+
domains.push('telemetry.enterprise.githubcopilot.com');
419+
} catch {
420+
// Invalid URL format - skip GHES domain extraction
421+
return [];
422+
}
423+
424+
return domains;
425+
}
426+
381427
/**
382428
* Resolves API target values from CLI options and environment variables, and merges them
383429
* into the allowed domains list. Also ensures each target is present as an https:// URL.
@@ -417,6 +463,17 @@ export function resolveApiTargetsToAllowedDomains(
417463
apiTargets.push(env['ANTHROPIC_API_TARGET']);
418464
}
419465

466+
// Auto-populate GHES domains when engine.api-target is set
467+
const ghesDomains = extractGhesDomainsFromEngineApiTarget(env);
468+
if (ghesDomains.length > 0) {
469+
for (const domain of ghesDomains) {
470+
if (!allowedDomains.includes(domain)) {
471+
allowedDomains.push(domain);
472+
}
473+
}
474+
debug(`Auto-added GHES domains from engine.api-target: ${ghesDomains.join(', ')}`);
475+
}
476+
420477
// Merge raw target values into the allowedDomains list so that later
421478
// checks/logs about "no allowed domains" see the final, expanded allowlist.
422479
const normalizedApiTargets = apiTargets.filter((t) => typeof t === 'string' && t.trim().length > 0);

0 commit comments

Comments
 (0)