diff --git a/AGENTS.md b/AGENTS.md index 2328220b1..03b3471be 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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 → `/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 → `/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 diff --git a/docs/AWF_PIPELINE_ENVIRONMENT_VARIABLES.md b/docs/AWF_PIPELINE_ENVIRONMENT_VARIABLES.md index d689ec9f5..ea67c246a 100644 --- a/docs/AWF_PIPELINE_ENVIRONMENT_VARIABLES.md +++ b/docs/AWF_PIPELINE_ENVIRONMENT_VARIABLES.md @@ -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..ghe.com`, **not** `api..ghe.com` (the REST API endpoint). +> **Important**: The Copilot inference sidecar uses `copilot-api..ghe.com`, **not** `api..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..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`). --- diff --git a/docs/ENVIRONMENT_VARIABLES.md b/docs/ENVIRONMENT_VARIABLES.md index c6dd4bb92..e1080db2d 100644 --- a/docs/ENVIRONMENT_VARIABLES.md +++ b/docs/ENVIRONMENT_VARIABLES.md @@ -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 → `/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 → `/api/v3`, `github.com` → `api.github.com` | (falls back to `api.github.com`) | ## GitHub Actions OIDC Variables diff --git a/internal/envutil/github.go b/internal/envutil/github.go index 98dc13b34..19c3e2ca4 100644 --- a/internal/envutil/github.go +++ b/internal/envutil/github.go @@ -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..ghe.com. +// This is distinct from copilot-api..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)) @@ -100,13 +104,18 @@ func deriveAPIFromServerURL(serverURL string) string { logGitHub.Printf("GitHub.com detected, using default API URL: %s", DefaultGitHubAPIBaseURL) return DefaultGitHubAPIBaseURL case strings.HasSuffix(hostname, ".ghe.com"): + // If the hostname already starts with "api." (e.g. GITHUB_SERVER_URL was + // set to the API base directly), return it as-is to avoid producing + // "api.api..ghe.com". var apiURL string - if port := parsed.Port(); port != "" { - apiURL = fmt.Sprintf("%s://copilot-api.%s:%s", parsed.Scheme, hostname, port) + if strings.HasPrefix(hostname, "api.") { + apiURL = fmt.Sprintf("%s://%s", parsed.Scheme, parsed.Host) + } else if port := parsed.Port(); 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) } - 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) diff --git a/internal/envutil/github_coverage_test.go b/internal/envutil/github_coverage_test.go index 50586a5e0..462d09375 100644 --- a/internal/envutil/github_coverage_test.go +++ b/internal/envutil/github_coverage_test.go @@ -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. diff --git a/internal/envutil/github_test.go b/internal/envutil/github_test.go index 5a9146381..dce94216b 100644 --- a/internal/envutil/github_test.go +++ b/internal/envutil/github_test.go @@ -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", @@ -125,7 +125,17 @@ 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: "GHEC already-API hostname is returned as-is", + serverURL: "https://api.mycompany.ghe.com", + expected: "https://api.mycompany.ghe.com", + }, + { + name: "GHEC already-API hostname with port is returned as-is", + serverURL: "https://api.mycompany.ghe.com:8443", + expected: "https://api.mycompany.ghe.com:8443", }, { name: "GHES with port", @@ -190,7 +200,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", diff --git a/internal/proxy/forward_to_github_test.go b/internal/proxy/forward_to_github_test.go index 378a06520..e7644b166 100644 --- a/internal/proxy/forward_to_github_test.go +++ b/internal/proxy/forward_to_github_test.go @@ -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..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", diff --git a/internal/proxy/proxy.go b/internal/proxy/proxy.go index 127bf79ac..5a43e61f1 100644 --- a/internal/proxy/proxy.go +++ b/internal/proxy/proxy.go @@ -342,11 +342,6 @@ 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. @@ -354,17 +349,13 @@ func (s *Server) forwardToGitHub(ctx context.Context, method, path string, body 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..ghe.com): + // GraphQL lives at /graphql graphqlURL = s.githubAPIURL + "/graphql" } url = graphqlURL