Skip to content

fix(copilot): resolve the gh host and key every credential source and the endpoint to it - #1438

Merged
junhoyeo merged 3 commits into
junhoyeo:mainfrom
kevin9327:fix/copilot-gh-host
Oct 15, 2026
Merged

junhoyeo merged 3 commits into
junhoyeo:mainfrom
kevin9327:fix/copilot-gh-host

Conversation

@kevin9327

@kevin9327 kevin9327 commented Oct 14, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #1260.

The Copilot usage provider was pinned to github.com in four places: the keyring service name, the hosts.yml section it read, the API URL, and the token environment variables. This PR resolves one host and keys all four to it, so a token can only go to the host that issued it.

Host resolution (same order as gh)

GH_HOST (normalised: scheme, trailing slash and case are stripped, and anything that is not a bare host[:port] is ignored) → the only host in hosts.yml → github.com. If hosts.yml lists several hosts and GH_HOST is unset, it uses github.com, which is what gh does too.

What is keyed to the host

Source Before After
Keyring / Windows Credential Manager gh:github.com gh:<host> (wincred targets gh:<host>:, gh:<host>)
hosts.yml only a literal github.com: section the <host>: section; an enterprise-only file now works
Env token GH_TOKEN/GITHUB_TOKEN always github.com and *.ghe.com: GH_TOKEN/GITHUB_TOKEN; GHES: GH_ENTERPRISE_TOKEN/GITHUB_ENTERPRISE_TOKEN (go-gh TokenForHost)
Endpoint https://api.github.com/copilot_internal/user https://api.<host>/copilot_internal/user

The endpoint uses the shape the official Copilot language server builds for an enterprise URI, api.<host>, as @aron-intframe found in @github/copilot-language-server. For github.com it gives the same URL as before.

The leak described in the issue is closed in both directions. A github.com GH_TOKEN is never read for an enterprise host, and an enterprise login is never sent to api.github.com. The probe output also reports the resolved host and endpoint (never token values), and the error messages give the host and the right variables, for example gh auth login --hostname <host>.

Verification (Linux, CPU)

  • 7 new tests: host precedence and normalisation, rejection of a malformed GH_HOST, hosts.yml host listing, per-host token extraction including the enterprise-only file from the issue, keyring/endpoint/wincred composition, env vars that never cross hosts, and an end-to-end check through GH_CONFIG_DIR + GH_HOST that also confirms the probe does not leak the token.
  • cargo test -p tokscale-cli: 1471 passed, 1 ignored (bin) / 192 / 8 / 1, 0 failed.
  • cargo clippy -p tokscale-cli --all-targets -- -D warnings and cargo fmt --check are clean.

Caveats

  • I don't have a GHES appliance, so the live response from api.<ghes-host>/copilot_internal/user is untested. That needs subdomain isolation on the appliance. The URL builder is one function (copilot_user_url), so switching to /api/v3 later is a one-line change.
  • The Windows-only paths were not compiled locally. They only gain a host argument.

Summary by cubic

Fixes #1260. The Copilot usage provider was pinned to github.com in four places: the keyring service name, the hosts.yml section it read, the API URL, and the token environment variables. It now resolves one host and keys all four to it, so a token can only go to the host that issued it.

The host is resolved the same way gh does: GH_HOST → the only host in hosts.yml → github.com. GH_HOST and configured hosts are parsed as a DNS name plus at most one port in 1..=65535, with :443 dropped so a key and its URL always share one spelling; anything else is ignored rather than spliced into a URL. hosts.yml is parsed with yaml_serde as a block mapping (quoted keys and values, inline comments, CRLF, aliases), and the token is read from the host's direct oauth_token child, not one nested under users:. An optional api_host record routes the API request to a different transport without rekeying credentials, which stay bound to the logical host.

What is keyed to the host

  • Keyring / Windows Credential Manager: gh:github.com → gh:<host>
  • hosts.yml: only a literal github.com: section → the <host>: section, so an enterprise-only file now works
  • Env token: always GH_TOKEN/GITHUB_TOKEN → GH_TOKEN/GITHUB_TOKEN for github.com and *.ghe.com, GH_ENTERPRISE_TOKEN/GITHUB_ENTERPRISE_TOKEN for GHES
  • Endpoint: https://api.github.com/copilot_internal/user → https://api.<host>/copilot_internal/user (or the configured api_host), the shape the official Copilot language server builds for an enterprise URI

The leak is closed in both directions: a github.com GH_TOKEN is never read for an enterprise host, and an enterprise login is never sent to api.github.com. The probe reports the resolved host and endpoint (never token values), and errors name the host and the right variables, e.g. gh auth login --hostname <host>.

Caveats

  • The live response from an enterprise /copilot_internal/user endpoint is untested; that needs subdomain isolation on the appliance.
  • The Windows-only paths were not compiled locally; they only gain a host argument.

Written for commit c7a4ec9. Summary will update on new commits.

View guided diff

@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@vercel

vercel Bot commented Oct 14, 2026 •

Copy link
Copy Markdown
Contributor

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated
tokscale Ignored Ignored Preview Oct 15, 2026 2:32am UTC

Request Review

@cubic-dev-ai cubic-dev-ai Bot left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All reported issues were addressed across 1 file

Reply with feedback, questions, or to request a fix.

View guided diff | Re-trigger cubic

@junhoyeo junhoyeo left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Requesting changes on 3def6c038c82e94474a33b18a376f586556edfca.

1. Host classification and request destination can select different credential audiences

normalize_gh_host accepts github.com:443, but is_dotcom_style_host compares the port-bearing string against github.com. This chooses GH_ENTERPRISE_TOKEN/GITHUB_ENTERPRISE_TOKEN, then constructs https://api.github.com:443/copilot_internal/user. Rust's URL parser canonicalizes that destination to https://api.github.com, and fetch_api attaches the selected enterprise credential to the request. The currently accepted malformed spelling github.com: takes the same path. This defeats the host-isolation guarantee this PR is meant to provide (copilot.rs:57-73,134-155,541-546,615-624).

Please reject ports entirely, or parse and validate the hostname/port once and classify credentials using the hostname without the port. Add regressions for github.com:443, tenant.ghe.com:443, empty and nonnumeric ports, and multiple-colon inputs. This was verified with source tracing and a no-network Rust URL probe; no real credentials or provider API calls were used.

2. Valid YAML comments break the enterprise fallback

Both host enumeration and token lookup require the host line to end exactly in :. A valid ghe.example.com: # comment section is therefore missed; oauth_token: fixture_token # comment instead includes the comment in the credential (copilot.rs:78-98,499-523). Please parse hosts.yml as a YAML mapping and select the requested host's top-level active token. Cover commented/quoted host keys and inline token comments.

Validation: the existing focused Copilot tests passed 22/22, formatting and diff checks passed. They do not cover these failure cases. The upstream Rust workflow runs are still awaiting maintainer authorization, so the lightweight green checks are not a Rust CI pass.

Address review on junhoyeo#1438.

- normalize_gh_host now parses a DNS hostname plus at most one port in
  1..=65535, writes the port back canonically and drops :443. Empty,
  non-numeric and multi-colon ports, empty labels and trailing dots are
  rejected. github.com:443 is github.com both when the credential is
  picked and when the URL is built.
- is_dotcom_style_host classifies on the hostname without the port.
- hosts.yml is read as a block mapping: quoted keys and values, inline
  comments, CRLF. The token is the host's direct oauth_token child, not
  one nested under users:.
@kevin9327

Copy link
Copy Markdown
Contributor Author

Thanks for the careful review. Both points are fixed in c7ee54c.

1. Host and port. normalize_gh_host now parses instead of filtering characters: a DNS hostname (no empty labels, so no trailing dot, no leading or trailing hyphen in a label) plus at most one :port in 1..=65535. The port is written back canonically and :443 is dropped, so github.com:443 and tenant.ghe.com:443 become github.com and tenant.ghe.com before anything is classified or built. github.com:, non-numeric ports, :0, :65536 and multi-colon inputs return None, so GH_HOST falls back as before. is_dotcom_style_host also classifies on the hostname without the port.

credential_audience_matches_the_host_the_request_reaches parses copilot_user_url(host) with reqwest::Url (the same parsing fetch_api goes through) and asserts that the host it reaches matches the credential class, for bare, :443 and :8443 inputs.

2. hosts.yml. It is now read as a block mapping by a small line parser. It handles quoted keys and values, inline comments (# after whitespace, so gho_a#b keeps its #) and CRLF. The token is the host's direct oauth_token child, so a per-account token under users: is no longer picked up even when it comes first. I kept this to the block style gh writes rather than adding a YAML crate, since the CLI is careful about its dependency tree. If you'd prefer serde_yaml/serde_norway I'm happy to switch.

Regression tests cover github.com:443, tenant.ghe.com:443, empty, non-numeric and multi-colon ports, commented, single-quoted and double-quoted host keys, inline token comments, and a users: token listed before the active one.

cargo test -p tokscale-cli --bin tokscale copilot gives 32 passed. cargo fmt --check and cargo clippy -p tokscale-cli --all-targets are clean.

@junhoyeo junhoyeo left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the careful follow-up. The port validation, hostname-only audience classification, and the comment/quoted-key cases from the earlier review are now covered well.

Two small items remain before this is ready:

  1. crates/tokscale-cli/src/commands/usage/copilot.rs:267-271 always guesses https://api.{host}. gh supports a per-host api_host mapping for API transport, so this can send the Copilot request to a guessed endpoint instead of the configured one. Please resolve the logical host's optional api_host when building the request URL, while continuing to select credentials by the logical host, and add a no-network regression where those two hosts differ.

  2. crates/tokscale-cli/src/commands/usage/copilot.rs:120-195 and :620-637 implement a handwritten line subset rather than parsing the YAML mapping requested in the previous review. It handles the listed examples, but valid YAML mapping features such as aliases or merge keys can yield a wrong active token. Please deserialize hosts.yml as a mapping, select only the requested host's direct active oauth_token, and add coverage for that case as well as the existing comments/quotes and nested users case.

The remaining minor compatibility items will be handled by maintainers in a follow-up after merge. Thanks again for carrying this through.

@junhoyeo

Copy link
Copy Markdown
Owner

Thanks again for the solid follow-up. I pushed c7a4ec9d (fix(copilot): parse configured GitHub host records) to finish the two remaining review items:

  • It resolves an optional api_host from the logical host record, so the Copilot request uses GitHub CLI’s configured API transport while credentials remain keyed to the logical host.
  • It replaces the handwritten YAML subset with yaml_serde 0.10.7, the maintained YAML Organization parser, and adds no-network coverage for YAML aliases, comments/quotes, nested users tokens, and a distinct API transport host.

The formatter, cargo clippy -p tokscale-cli --all-targets -- -D warnings, and the full cargo test -p tokscale-cli suite pass locally. I’ll merge once CI is green. Thanks for the contribution!

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 issue found across 4 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="crates/tokscale-cli/src/commands/usage/copilot.rs">

<violation number="1" location="crates/tokscale-cli/src/commands/usage/copilot.rs:147">
P2: Equivalent keys such as `ghe.example.com` and `ghe.example.com:443` both normalize to the same host but remain duplicated here. `select_gh_host` then treats the sole logical enterprise host as multiple and falls back to `github.com`; deduplicate normalized hosts before returning.</violation>
</file>

Tip: Review your code locally with the cubic CLI to iterate faster.

View guided diff | Re-trigger cubic

.keys()
.filter_map(yaml_serde::Value::as_str)
.filter_map(normalize_gh_host)
.collect()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Equivalent keys such as ghe.example.com and ghe.example.com:443 both normalize to the same host but remain duplicated here. select_gh_host then treats the sole logical enterprise host as multiple and falls back to github.com; deduplicate normalized hosts before returning.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At crates/tokscale-cli/src/commands/usage/copilot.rs, line 147:

<comment>Equivalent keys such as `ghe.example.com` and `ghe.example.com:443` both normalize to the same host but remain duplicated here. `select_gh_host` then treats the sole logical enterprise host as multiple and falls back to `github.com`; deduplicate normalized hosts before returning.</comment>

<file context>
@@ -117,98 +117,63 @@ fn gh_host_name(host: &str) -> &str {
+        .keys()
+        .filter_map(yaml_serde::Value::as_str)
+        .filter_map(normalize_gh_host)
+        .collect()
 }
 
</file context>
Suggested change
.collect()
.fold(Vec::new(), |mut hosts, host| {
if !hosts.contains(&host) {
hosts.push(host);
}
hosts
})

@junhoyeo junhoyeo left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the contribution and for the thorough follow-up. The maintainer completion is now covered by the full cross-platform CI matrix, and everything is green. Approved — I’ll squash-merge this now.

@junhoyeo
junhoyeo merged commit 0e422df into junhoyeo:main Oct 15, 2026
21 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

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

2 participants