Ensure api and auth commands record agentic invocations#13046
Conversation
The gh api command builds its own HTTP client inline without forwarding InvokingAgent, so the User-Agent header was missing the Agent/<name> suffix when invoked by AI coding agents. Thread InvokingAgent through Factory → ApiOptions → HTTPClientOptions, mirroring the existing AppVersion pattern. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
f97ba93 to
314ad6b
Compare
There was a problem hiding this comment.
Pull request overview
This PR ensures the detected invoking agent is consistently included in the User-Agent header for gh api requests and for the auth flow’s post-login getViewer request, addressing gaps from the original invoking-agent rollout.
Changes:
- Add
InvokingAgenttocmdutil.Factoryand wire it through the default factory constructor. - Thread
InvokingAgentintogh api’s HTTP client construction and add a regression test forUser-Agent. - Update auth flow
getViewerto reuse the provided HTTP client and add a test to confirmUser-Agent/auth header behavior.
Reviewed changes
Copilot reviewed 7 out of 7 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| pkg/cmdutil/factory.go | Adds InvokingAgent to the shared factory so commands can access it. |
| pkg/cmd/factory/default.go | Populates Factory.InvokingAgent from the factory constructor input. |
| pkg/cmd/api/api.go | Plumbs InvokingAgent into api.HTTPClientOptions used by gh api. |
| pkg/cmd/api/api_test.go | Adds coverage to assert gh api includes Agent/<name> in User-Agent. |
| internal/authflow/flow.go | Changes getViewer to reuse the caller’s HTTP client and wrap transport with auth. |
| internal/authflow/flow_test.go | Adds a regression test ensuring getViewer preserves UA and sets Authorization. |
| AGENTS.md | Documents running go test ./... and make lint before committing. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
api and auth commands record agentic invocations
getViewer was building a new HTTP client from scratch, losing AppVersion and InvokingAgent from the plain client already passed into AuthFlow. Reuse the existing client by shallow-copying it and wrapping its transport with AddAuthTokenHeader for the new token. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
314ad6b to
4e8aa56
Compare
babakks
left a comment
There was a problem hiding this comment.
LGTM. Just a comment on one of the tests.
| // Outer transport sets User-Agent, simulating the factory-built client's header middleware. | ||
| // Inner transport captures headers as-received to verify they survived the wrapping. | ||
| plainClient := &http.Client{ | ||
| Transport: &roundTripper{roundTrip: func(req *http.Request) (*http.Response, error) { | ||
| req.Header.Set("User-Agent", "GitHub CLI 1.2.3 Agent/copilot-cli") | ||
| return (&http.Client{ | ||
| Transport: &roundTripper{roundTrip: func(req *http.Request) (*http.Response, error) { | ||
| receivedUA = req.Header.Get("User-Agent") | ||
| receivedAuth = req.Header.Get("Authorization") | ||
| return &http.Response{ | ||
| StatusCode: 200, | ||
| Header: http.Header{"Content-Type": []string{"application/json"}}, | ||
| Body: io.NopCloser(bytes.NewBufferString(`{"data":{"viewer":{"login":"monalisa"}}}`)), | ||
| Request: req, | ||
| }, nil | ||
| }}, | ||
| }).Transport.RoundTrip(req) | ||
| }}, | ||
| } |
There was a problem hiding this comment.
I know it's meant to simulate the actual transport middlewares, but it seems like a trivial assertion, when we set the UA header in the outer transport and then we capture it in the inner one.
In other words, the test should only assert the auth header is set.
There was a problem hiding this comment.
I think the point with the user-agent assertion is to ensure the header isn't being mutated. I know it's a trivial assertion, but I guess tests are cheap. If we really care that this user agent isn't mutated in between somehow, it seems like a fair idea.
BTW, (nit) I would prefer if this test code was a bit more readable by breaking apart this code.
// Inner transport captures headers as-received to verify they survived the wrapping.
innerTransport := &roundTripper{
roundTrip: func(req *http.Request) (*http.Response, error) {
receivedUA = req.Header.Get("User-Agent")
receivedAuth = req.Header.Get("Authorization")
return &http.Response{
StatusCode: 200,
Header: http.Header{"Content-Type": []string{"application/json"}},
Body: io.NopCloser(bytes.NewBufferString(`{"data":{"viewer":{"login":"monalisa"}}}`)),
Request: req,
}, nil
},
}
// Outer transport sets User-Agent, simulating the factory-built client's header middleware.
plainClient := &http.Client{
Transport: &roundTripper{
roundTrip: func(req *http.Request) (*http.Response, error) {
req.Header.Set("User-Agent", "GitHub CLI 1.2.3 Agent/copilot-cli")
return innerTransport.RoundTrip(req)
},
},
}There was a problem hiding this comment.
Yeah, I thought about that, too. But it can't be the case either. Because the new transport that getViewer makes, will wrap the HTTP client's transport (the two-layer we build in the test). I mean, there's no way for getViewer to add a transport in the middle of the chain.
We don't need the two layers. Only one is enough. However, we can check the UA header is not set, as an assertion that getViewer has nothing to do wth it (and therefore further transport layers see the UA header is unset, and would then safely add it).
Signed-off-by: Babak K. Shandiz <babakks@github.com>
CLI: claude-review --pr <URL> GitHub Action: .github/workflows/review.yml for automated PR reviews Structured Markdown output: Summary / Risks / Suggestions / Confidence Score Tested on 2 real GitHub PRs (cli/cli#13046, cli/cli#13048) Payment address: eB51DWp1uECrLZRLsE2cnyZUzfRWvzUzaJzkatTpQV9
This MR contains the following updates: | Package | Update | Change | |---|---|---| | [cli/cli](https://github.com/cli/cli) | minor | `v2.89.0` → `v2.90.0` | MR created with the help of [el-capitano/tools/renovate-bot](https://gitlab.com/el-capitano/tools/renovate-bot). **Proposed changes to behavior should be submitted there as MRs.** --- ### Release Notes <details> <summary>cli/cli (cli/cli)</summary> ### [`v2.90.0`](https://github.com/cli/cli/releases/tag/v2.90.0): GitHub CLI 2.90.0 [Compare Source](cli/cli@v2.89.0...v2.90.0) #### Manage agent skills with `gh skill` (Public Preview) [Agent skills](https://agentskills.io) are portable sets of instructions, scripts, and resources that teach AI coding agents how to perform specific tasks. The new `gh skill` command makes it easy to discover, install, manage, and publish agent skills from GitHub repositories - right from the CLI. ``` # Discover skills gh skill search copilot # Preview a skill without installing gh skill preview github/awesome-copilot documentation-writer # Install a skill gh skill install github/awesome-copilot documentation-writer # Pin to a specific version gh skill install github/awesome-copilot documentation-writer --pin v1.2.0 # Check installed skills for updates gh skill update --all # Validate and publish your own skills gh skill publish --dry-run ``` Skills are automatically installed to the correct directory for your agent host. `gh skill` supports GitHub Copilot, Claude Code, Cursor, Codex, Gemini CLI, and Antigravity. Target a specific agent and scope with `--agent` and `--scope` flags. `gh skill publish` validates skills against the [Agent Skills specification](https://agentskills.io/specification) and checks remote settings like tag protection and immutable releases to improve supply chain security. Read the full announcement on the [GitHub Blog](https://github.blog/changelog/2026-04-16-manage-agent-skills-with-github-cli/). `gh skill` is launching in public preview and is subject to change without notice. #### Official extension suggestions When you run a command that matches a known official extension that isn't installed (e.g. `gh stack`), the CLI now offers to install it instead of showing a generic "unknown command" error. This feature is available for [github/gh-aw](https://github.com/github/gh-aw) and [github/gh-stack](https://github.com/github/gh-stack). When possible, you'll be prompted to install immediately. When prompting isn't possible, the CLI prints the `gh extension install` command to run. #### `gh extension install` no longer requires authentication `gh extension install` previously required a valid auth token even though it only needs to download a public release asset. The auth check has been removed, so you can install extensions without being logged in. #### What's Changed ##### ✨ Features - Add `gh skill` command group: install, preview, search, update, publish by [@​SamMorrowDrums](https://github.com/SamMorrowDrums) in [#​13165](cli/cli#13165) - Suggest and install official extensions for unknown commands by [@​BagToad](https://github.com/BagToad) in [#​13175](cli/cli#13175) - `gh skill publish`: auto-push unpushed commits before publish by [@​SamMorrowDrums](https://github.com/SamMorrowDrums) in [#​13171](cli/cli#13171) - Disable auth check for `gh extension install` by [@​BagToad](https://github.com/BagToad) in [#​13176](cli/cli#13176) ##### 🐛 Fixes - Fix infinite loop in `gh release list --limit 0` by [@​Bahtya](https://github.com/Bahtya) in [#​13097](cli/cli#13097) - Ensure `api` and `auth` commands record agentic invocations by [@​williammartin](https://github.com/williammartin) in [#​13046](cli/cli#13046) - Disable auth check for local-only skill flags by [@​SamMorrowDrums](https://github.com/SamMorrowDrums) in [#​13173](cli/cli#13173) - URL-encode parentPath in skills discovery API call by [@​SamMorrowDrums](https://github.com/SamMorrowDrums) in [#​13172](cli/cli#13172) - Fix: use target directory remotes in skills publish by [@​SamMorrowDrums](https://github.com/SamMorrowDrums) in [#​13169](cli/cli#13169) - Fix: preserve namespace in skills search deduplication by [@​SamMorrowDrums](https://github.com/SamMorrowDrums) in [#​13170](cli/cli#13170) ##### 📚 Docs & Chores - docs: include PGP key fingerprints by [@​babakks](https://github.com/babakks) in [#​13112](cli/cli#13112) - docs: add sha/md5 checksums of keyring files by [@​babakks](https://github.com/babakks) in [#​13150](cli/cli#13150) - docs: fix SHA512 checksum for GPG key by [@​timsu92](https://github.com/timsu92) in [#​13157](cli/cli#13157) - docs(skill): polish skill commandset docs by [@​babakks](https://github.com/babakks) in [#​13183](cli/cli#13183) - Document dependency CVE policy in SECURITY.md by [@​BagToad](https://github.com/BagToad) in [#​13119](cli/cli#13119) - Replace github.com/golang/snappy with klauspost/compress/snappy by [@​thaJeztah](https://github.com/thaJeztah) in [#​13048](cli/cli#13048) - chore: bump to go1.26.2 by [@​babakks](https://github.com/babakks) in [#​13116](cli/cli#13116) - chore: delete experimental script/debian-devel by [@​babakks](https://github.com/babakks) in [#​13127](cli/cli#13127) - Suggest first party extensions by [@​williammartin](https://github.com/williammartin) in [#​13182](cli/cli#13182) - Add cli/skill-reviewers as CODEOWNERS for skills packages by [@​BagToad](https://github.com/BagToad) in [#​13189](cli/cli#13189) - Add [@​cli/code-reviewers](https://github.com/cli/code-reviewers) to all CODEOWNERS rules by [@​BagToad](https://github.com/BagToad) in [#​13190](cli/cli#13190) - Address post-merge review feedback for skills commands by [@​SamMorrowDrums](https://github.com/SamMorrowDrums) in [#​13185](cli/cli#13185) - Fix skills-publish-dry-run acceptance test error message mismatch by [@​SamMorrowDrums](https://github.com/SamMorrowDrums) in [#​13187](cli/cli#13187) - Skills: replace real git in publish tests with CommandStubber by [@​SamMorrowDrums](https://github.com/SamMorrowDrums) in [#​13188](cli/cli#13188) - Remove redundant nil-client fallback in skills publish by [@​SamMorrowDrums](https://github.com/SamMorrowDrums) in [#​13168](cli/cli#13168) - Publish: use shared discovery logic instead of requiring skills/ directory by [@​SamMorrowDrums](https://github.com/SamMorrowDrums) in [#​13167](cli/cli#13167) #####Dependencies - chore(deps): bump github.com/klauspost/compress from 1.18.4 to 1.18.5 by [@​dependabot](https://github.com/dependabot)\[bot] in [#​13071](cli/cli#13071) - chore(deps): bump github.com/yuin/goldmark from 1.7.16 to 1.8.2 by [@​dependabot](https://github.com/dependabot)\[bot] in [#​13045](cli/cli#13045) - chore(deps): bump charm.land/bubbles/v2 from 2.0.0 to 2.1.0 by [@​dependabot](https://github.com/dependabot)\[bot] in [#​13051](cli/cli#13051) - chore(deps): bump github.com/sigstore/timestamp-authority/v2 from 2.0.3 to 2.0.6 by [@​dependabot](https://github.com/dependabot)\[bot] in [#​13152](cli/cli#13152) - chore(deps): bump github.com/google/go-containerregistry from 0.21.3 to 0.21.4 by [@​dependabot](https://github.com/dependabot)\[bot] in [#​13129](cli/cli#13129) - chore(deps): bump github.com/sigstore/protobuf-specs from 0.5.0 to 0.5.1 by [@​dependabot](https://github.com/dependabot)\[bot] in [#​13128](cli/cli#13128) - chore(deps): bump github.com/in-toto/attestation from 1.1.2 to 1.2.0 by [@​dependabot](https://github.com/dependabot)\[bot] in [#​13044](cli/cli#13044) - chore(deps): bump advanced-security/filter-sarif from 1.0.1 to 1.1 by [@​dependabot](https://github.com/dependabot)\[bot] in [#​12918](cli/cli#12918) - chore(deps): bump google.golang.org/grpc from 1.79.3 to 1.80.0 by [@​dependabot](https://github.com/dependabot)\[bot] in [#​13076](cli/cli#13076) - chore(deps): bump github.com/hashicorp/go-version from 1.8.0 to 1.9.0 by [@​dependabot](https://github.com/dependabot)\[bot] in [#​13065](cli/cli#13065) #### New Contributors - [@​thaJeztah](https://github.com/thaJeztah) made their first contribution in [#​13048](cli/cli#13048) - [@​Bahtya](https://github.com/Bahtya) made their first contribution in [#​13097](cli/cli#13097) - [@​timsu92](https://github.com/timsu92) made their first contribution in [#​13157](cli/cli#13157) - [@​SamMorrowDrums](https://github.com/SamMorrowDrums) made their first contribution in [#​13173](cli/cli#13173) **Full Changelog**: <cli/cli@v2.89.0...v2.90.0> </details> --- ### Configuration 📅 **Schedule**: (UTC) - Branch creation - At any time (no schedule defined) - Automerge - At any time (no schedule defined) 🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied. ♻ **Rebasing**: Whenever MR becomes conflicted, or you tick the rebase/retry checkbox. 🔕 **Ignore**: Close this MR and you won't be reminded about this update again. --- - [ ] <!-- rebase-check -->If you want to rebase/retry this MR, check this box --- This MR has been generated by [Renovate Bot](https://github.com/renovatebot/renovate). <!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0My4xMjMuNiIsInVwZGF0ZWRJblZlciI6IjQzLjEyMy42IiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6WyJSZW5vdmF0ZSBCb3QiLCJhdXRvbWF0aW9uOmJvdC1hdXRob3JlZCIsImRlcGVuZGVuY3ktdHlwZTo6bWlub3IiXX0=-->
Description
When we added the invoking agent to the
User-Agentheader in API requests, we missed two spots.Firsty,
gh apiconstructs its own http client inline, so we needed to thread that down through the factory like we do withappVersion.Secondly, the
gh auth loginstarts using theplainHTTPClient(which does have theInvokingAgentset) but after obtaining a token, it constructed a new client togetViewerthat dropped configuration on the existing client. This is why we had a lot of empty versions when looking at request logs. Now we copy it and wrap the transport to include the newly obtained token.