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.
Follow-up to #1251, which I said I would open separately.
ghis a multi-host CLI:gh auth login --hostname ghe.example.comstores a token for that host, andGH_HOSTselects 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 togithub.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:38ghcomposes this as"gh:" + hostname(keyringServiceNamein cli/cli), so an enterprise user's token lives undergh:ghe.example.comand 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 atcopilot.rs:745asserting it works forgh:ghe.example.com— but the service name itself is still a constant, so that parameter is only ever passedgh:github.com.2. The
hosts.ymlfallback matches a literal section headercrates/tokscale-cli/src/commands/usage/copilot.rs:359-393, specifically:A
hosts.ymlcontaining onlyghe.example.com:walks every line, never setsin_github, and ends atanyhow::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:470GHES serves its API from the appliance, not from
api.github.com.4.
GH_HOSTis not readtoken_from_env(copilot.rs:158) readsGH_TOKENandGITHUB_TOKENand nothing else.GH_HOST— the variableghitself 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>:inhosts.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
Authorizationheader toapi.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_envis consulted first (copilot.rs:401), so a user who exportsGH_TOKENfor their enterprise host today already has it sent toapi.github.com. That is arguably a smaller version of the same bug and is in scope here.What a fix needs to decide
GH_HOST, or the set of keys present inhosts.yml, or a tokscale-side setting.gh's own precedence isGH_HOST> the single host inhosts.yml>github.com.hosts.ymlcan 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.copilot_internal/userexists there at all, and under which base (https://<host>/api/v3/…for GHES, vshttps://api.<host>/…).api.github.comis 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/userreturns anything with aghtoken. That is the piece I cannot supply.