Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
4 changes: 2 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -389,8 +389,8 @@ DEBUG_COLORS=0 DEBUG=* ./awmg --config config.toml
- `GITHUB_TOKEN` - Second-priority GitHub auth token fallback after `GITHUB_MCP_SERVER_TOKEN`
- `GITHUB_PERSONAL_ACCESS_TOKEN` - Third-priority GitHub auth fallback
- `GH_TOKEN` - Lowest-priority GitHub auth fallback (set by GitHub CLI)
- `GITHUB_API_URL` - Explicit GitHub API endpoint (e.g., `https://copilot-api.mycompany.ghe.com`); used by proxy to set upstream target
- `GITHUB_SERVER_URL` - GitHub server URL; proxy auto-derives API endpoint: `*.ghe.com` → `copilot-api.*.ghe.com`, GHES → `<host>/api/v3`, `github.com` → `api.github.com`
- `GITHUB_API_URL` - Explicit GitHub API endpoint (e.g., `https://api.mycompany.ghe.com`); used by proxy to set upstream target
- `GITHUB_SERVER_URL` - GitHub server URL; proxy auto-derives API endpoint: `*.ghe.com` → `api.*.ghe.com`, GHES → `<host>/api/v3`, `github.com` → `api.github.com`
- `ACTIONS_ID_TOKEN_REQUEST_URL` - GitHub Actions OIDC token endpoint URL; required for `github-oidc` auth type
- `ACTIONS_ID_TOKEN_REQUEST_TOKEN` - GitHub Actions OIDC request token; required for `github-oidc` auth type
- `MCP_GATEWAY_PORT` - Used by environment validation (`--validate-env`) for container port-mapping checks (validated 1-65535); does not override the gateway listen address
Expand Down
2 changes: 1 addition & 1 deletion docs/AWF_PIPELINE_ENVIRONMENT_VARIABLES.md
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@ The `deriveCopilotApiTarget()` function in the API proxy sidecar (`server.js`) r
| `https://company.ghe.com` | GHEC | `copilot-api.company.ghe.com` |
| `https://github.company.com` | GHES | `api.enterprise.githubcopilot.com` |

> **Important**: GHEC uses `copilot-api.<slug>.ghe.com`, **not** `api.<slug>.ghe.com` (the REST API endpoint).
> **Important**: The Copilot inference sidecar uses `copilot-api.<slug>.ghe.com`, **not** `api.<slug>.ghe.com`. This is distinct from the **mcpg DIFC proxy**, which forwards REST/GraphQL `gh api` calls and therefore targets the REST API host `api.<slug>.ghe.com` (derived from `GITHUB_SERVER_URL`). Do not point the DIFC proxy at `copilot-api.*` — that endpoint does not serve the REST API (e.g. `/rate_limit`).

---

Expand Down
4 changes: 2 additions & 2 deletions docs/ENVIRONMENT_VARIABLES.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,8 +104,8 @@ When running `awmg proxy`, these variables configure the upstream GitHub API:

| Variable | Description | Default |
|----------|-------------|---------|
| `GITHUB_API_URL` | Explicit GitHub API endpoint (e.g., `https://copilot-api.mycompany.ghe.com`); used by proxy to set upstream target | (auto-derived) |
| `GITHUB_SERVER_URL` | GitHub server URL; proxy auto-derives API endpoint: `*.ghe.com` → `copilot-api.*.ghe.com`, GHES → `<host>/api/v3`, `github.com` → `api.github.com` | (falls back to `api.github.com`) |
| `GITHUB_API_URL` | Explicit GitHub API endpoint (e.g., `https://api.mycompany.ghe.com`); used by proxy to set upstream target | (auto-derived) |
| `GITHUB_SERVER_URL` | GitHub server URL; proxy auto-derives API endpoint: `*.ghe.com` → `api.*.ghe.com`, GHES → `<host>/api/v3`, `github.com` → `api.github.com` | (falls back to `api.github.com`) |

## GitHub Actions OIDC Variables

Expand Down
12 changes: 8 additions & 4 deletions internal/envutil/github.go
Original file line number Diff line number Diff line change
Expand Up @@ -77,9 +77,13 @@ func DeriveGitHubAPIURL(defaultURL string) string {
}

// deriveAPIFromServerURL converts a GITHUB_SERVER_URL to the corresponding API endpoint.
// GHEC tenants (*.ghe.com): https://tenant.ghe.com → https://copilot-api.tenant.ghe.com
// GHEC data-residency tenants (*.ghe.com): https://tenant.ghe.com → https://api.tenant.ghe.com
// GitHub.com: https://github.com → https://api.github.com
// GHES (all others): https://github.example.com → https://github.example.com/api/v3
//
// Note: GHEC data residency exposes its REST/GraphQL API at api.<tenant>.ghe.com.
// This is distinct from copilot-api.<tenant>.ghe.com, which is the Copilot inference
// endpoint and does not serve the REST API (e.g. /rate_limit) that the DIFC proxy forwards.
func deriveAPIFromServerURL(serverURL string) string {
logGitHub.Printf("Deriving API URL from server URL: %s", sanitize.RedactURL(serverURL))

Expand All @@ -102,11 +106,11 @@ func deriveAPIFromServerURL(serverURL string) string {
case strings.HasSuffix(hostname, ".ghe.com"):
var apiURL string
if port := parsed.Port(); port != "" {
apiURL = fmt.Sprintf("%s://copilot-api.%s:%s", parsed.Scheme, hostname, port)
apiURL = fmt.Sprintf("%s://api.%s:%s", parsed.Scheme, hostname, port)
} else {
apiURL = fmt.Sprintf("%s://copilot-api.%s", parsed.Scheme, hostname)
apiURL = fmt.Sprintf("%s://api.%s", parsed.Scheme, hostname)
Comment on lines 106 to +116
}
logGitHub.Printf("GHEC tenant detected, using copilot-api subdomain: hostname=%s, apiURL=%s", hostname, apiURL)
logGitHub.Printf("GHEC data-residency tenant detected, using api subdomain: hostname=%s, apiURL=%s", hostname, apiURL)
return apiURL
default:
apiURL := fmt.Sprintf("%s://%s/api/v3", parsed.Scheme, parsed.Host)
Expand Down
8 changes: 4 additions & 4 deletions internal/envutil/github_coverage_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,16 +30,16 @@ func TestDeriveAPIFromServerURL_HTTPSchemeAndEdgeCases(t *testing.T) {
{
// http scheme is explicitly allowed; schemes other than "http" and
// "https" are rejected.
// GHEC tenant: prepend "copilot-api." subdomain.
name: "http scheme GHEC tenant derives copilot-api subdomain",
// GHEC data-residency tenant: prepend "api." subdomain.
name: "http scheme GHEC tenant derives api subdomain",
serverURL: "http://mycompany.ghe.com",
expected: "http://copilot-api.mycompany.ghe.com",
expected: "http://api.mycompany.ghe.com",
},
{
// GHEC tenant with both http scheme and a port number.
name: "http scheme GHEC tenant with port",
serverURL: "http://mycompany.ghe.com:8080",
expected: "http://copilot-api.mycompany.ghe.com:8080",
expected: "http://api.mycompany.ghe.com:8080",
},
{
// http scheme is allowed for GHES hosts as well.
Expand Down
8 changes: 4 additions & 4 deletions internal/envutil/github_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -113,9 +113,9 @@ func TestDeriveAPIFromServerURL(t *testing.T) {
expected: DefaultGitHubAPIBaseURL,
},
{
name: "GHEC tenant derives copilot-api subdomain",
name: "GHEC data-residency tenant derives api subdomain",
serverURL: "https://mycompany.ghe.com",
expected: "https://copilot-api.mycompany.ghe.com",
expected: "https://api.mycompany.ghe.com",
},
{
name: "GHES uses /api/v3 path",
Expand All @@ -125,7 +125,7 @@ func TestDeriveAPIFromServerURL(t *testing.T) {
{
name: "GHEC tenant with port",
serverURL: "https://mycompany.ghe.com:8443",
expected: "https://copilot-api.mycompany.ghe.com:8443",
expected: "https://api.mycompany.ghe.com:8443",
},
{
name: "GHES with port",
Expand Down Expand Up @@ -190,7 +190,7 @@ func TestDeriveGitHubAPIURL(t *testing.T) {
envAPIURL: "",
envSrvURL: "https://mycompany.ghe.com",
defaultURL: DefaultGitHubAPIBaseURL,
expected: "https://copilot-api.mycompany.ghe.com",
expected: "https://api.mycompany.ghe.com",
},
{
name: "invalid GITHUB_SERVER_URL falls back to default",
Expand Down
17 changes: 9 additions & 8 deletions internal/proxy/forward_to_github_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -189,21 +189,22 @@ func TestForwardToGitHub_GraphQLPathRouting(t *testing.T) {
wantServerPath: "/api/graphql?ref=main&query=foo",
},
{
// GHE Cloud data residency (e.g. copilot-api.sj.ghe.com) has no /api/v3
// suffix but the gh CLI sends GraphQL requests to /api/graphql.
// forwardToGitHub must forward to base/api/graphql, not base/graphql.
name: "GHE Cloud data residency api/graphql path routes to base/api/graphql",
githubAPIURL: "http://copilot-api.sj.ghe.com",
// GHE Cloud data residency exposes its REST/GraphQL API at
// api.<tenant>.ghe.com (not copilot-api), so GraphQL lives at /graphql
// just like github.com. The gh CLI sends GraphQL requests to /api/graphql
// (rewritten GITHUB_GRAPHQL_URL); forwardToGitHub must normalise to base/graphql.
name: "GHE Cloud data residency api/graphql path routes to base/graphql",
githubAPIURL: "http://api.sj.ghe.com",
routeToUpstreamHost: true,
requestPath: "/api/graphql",
wantServerPath: "/api/graphql",
wantServerPath: "/graphql",
},
{
name: "GHE Cloud data residency api/graphql with query string preserves query",
githubAPIURL: "http://copilot-api.sj.ghe.com",
githubAPIURL: "http://api.sj.ghe.com",
routeToUpstreamHost: true,
requestPath: "/api/graphql?foo=bar",
wantServerPath: "/api/graphql?foo=bar",
wantServerPath: "/graphql?foo=bar",
},
{
name: "dotcom-style api graphql path normalises to graphql",
Expand Down
13 changes: 2 additions & 11 deletions internal/proxy/proxy.go
Original file line number Diff line number Diff line change
Expand Up @@ -342,29 +342,20 @@ func (s *Server) upstreamHost() string {
return host
}

func (s *Server) isGHECDataResidencyHost() bool {
host := strings.ToLower(s.upstreamHost())
return strings.HasPrefix(host, "copilot-api.") && strings.HasSuffix(host, ".ghe.com")
}

// forwardToGitHub sends a request to the upstream GitHub API.
// clientAuth is the Authorization header from the inbound client request;
// if non-empty it is forwarded as-is, otherwise the configured fallback token is used.
func (s *Server) forwardToGitHub(ctx context.Context, method, path string, body io.Reader, contentType string, clientAuth string) (*http.Response, error) {
url := s.githubAPIURL + path
pathOnly, query, hasQuery := strings.Cut(path, "?")
if IsGraphQLPath(pathOnly) {
normalizedPath := strings.TrimSuffix(pathOnly, "/")
var graphqlURL string
if strings.HasSuffix(s.githubAPIURL, "/api/v3") {
// GHES: strip /api/v3, GraphQL lives at /api/graphql
graphqlURL = strings.TrimSuffix(s.githubAPIURL, "/api/v3") + "/api/graphql"
} else if s.isGHECDataResidencyHost() && strings.HasSuffix(normalizedPath, "/api/graphql") {
// GHE Cloud data residency (e.g. copilot-api.sj.ghe.com):
// the client already sent /api/graphql — use the same path on upstream
graphqlURL = s.githubAPIURL + "/api/graphql"
} else {
// github.com: GraphQL lives at /graphql
// github.com and GHEC data residency (api.<tenant>.ghe.com):
// GraphQL lives at /graphql
graphqlURL = s.githubAPIURL + "/graphql"
}
url = graphqlURL
Expand Down
Loading