Skip to content

copilot: GitHub Enterprise hosts are unreachable — every token discovery path is pinned to github.com #1260

Description

@junhoyeo

Follow-up to #1251, which I said I would open separately.

gh is a multi-host CLI: gh auth login --hostname ghe.example.com stores a token for that host, and GH_HOST selects which host the rest of the CLI operates against. Tokscale's Copilot integration has no concept of a host at all. Every discovery path is pinned to github.com, in four independent places, and they have to be fixed together rather than one at a time.

All line numbers are on 3f4fbd73.

1. The keyring service name is a constant

crates/tokscale-cli/src/commands/usage/copilot.rs:38

const GH_KEYRING_SERVICE: &str = "gh:github.com";

gh composes this as "gh:" + hostname (keyringServiceName in cli/cli), so an enterprise user's token lives under gh:ghe.example.com and nothing here ever asks for it. #1251 parameterised the target composition on the service name — gh_wincred_targets(service) takes an argument and there is a test at copilot.rs:745 asserting it works for gh:ghe.example.com — but the service name itself is still a constant, so that parameter is only ever passed gh:github.com.

2. The hosts.yml fallback matches a literal section header

crates/tokscale-cli/src/commands/usage/copilot.rs:359-393, specifically:

if trimmed == "github.com:" {
    in_github = true;

A hosts.yml containing only ghe.example.com: walks every line, never sets in_github, and ends at anyhow::bail!("No oauth_token found in hosts.yml"). So the file fallback fails for the same reason the keyring lookup does, and a user who has authenticated correctly gets told there is no token.

3. The API endpoint is hardcoded

crates/tokscale-cli/src/commands/usage/copilot.rs:470

.get("https://api.github.com/copilot_internal/user")

GHES serves its API from the appliance, not from api.github.com.

4. GH_HOST is not read

token_from_env (copilot.rs:158) reads GH_TOKEN and GITHUB_TOKEN and nothing else. GH_HOST — the variable gh itself uses to pick a host — is not consulted anywhere in the repo.

Why this should land as one change, not three

Points 1 and 2 are the easy half: derive the host, compose gh:<host>, match <host>: in hosts.yml. Point 3 is the hard half, because I cannot verify the GHES Copilot endpoint shape without an appliance to test against.

Shipping the easy half alone would make things worse. Today a GHE-issued OAuth token is never discovered, so it is never sent anywhere. Add host-aware discovery without host-aware routing and the very next thing that happens is that token going out in an Authorization header to api.github.com — a host it was not issued for and does not authenticate against. It would 401, so the user gets no working feature and has handed an enterprise credential to a third-party host.

Worth noting the same path is already reachable a different way: token_from_env is consulted first (copilot.rs:401), so a user who exports GH_TOKEN for their enterprise host today already has it sent to api.github.com. That is arguably a smaller version of the same bug and is in scope here.

What a fix needs to decide

  • Where the host comes from. GH_HOST, or the set of keys present in hosts.yml, or a tokscale-side setting. gh's own precedence is GH_HOST > the single host in hosts.yml > github.com.
  • Whether multiple hosts are tried or one is selected. hosts.yml can hold several. Trying all of them means sending each token to its own endpoint, which is fine, but it multiplies the requests and the failure modes.
  • The GHES endpoint. Needs confirmation from someone with an appliance: whether copilot_internal/user exists there at all, and under which base (https://<host>/api/v3/… for GHES, vs https://api.<host>/…).
  • What happens when the host is unknown. Refusing to send is the safe default; falling back to api.github.com is what causes the problem above.

Help wanted

If you use Copilot through GitHub Enterprise, the single most useful thing you can post here is the base URL your appliance serves its API from, and whether GET <base>/copilot_internal/user returns anything with a gh token. That is the piece I cannot supply.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions