Repository navigation
feat(auth): send the real CLI identity on usage reads (MASTRA-787) - #8
Merged
Merged
Conversation
The quota usage reads now go through each provider's executor with the identity the proxy already sends for that provider's real CLI traffic. Claude reads use the OAuth control-plane uTLS transport, the native header set and order, and the baseline Claude Code User-Agent, and decode compressed responses. Codex reads share the inference path's credential, User-Agent, Originator, account, custom-header and cloaking logic. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
MASTRA-787 Read Codex and Claude usage on a timer so soonest-reset sees quota used outside the proxy
OutcomeThe soonest-reset selector in WhyToday the selector knows a credential's quota only from the proxy's own request traffic: response headers, and Codex websocket quota events (MASTRA-765). On 2026-10-04, reading the provider usage endpoints on studio2 showed two problems (details on MASTRA-766):
Eric chose (2026-10-04) to read usage for every credential on a timer, rather than only once for credentials with no observation. Scope
Acceptance criteria
Constraints
Source refs
Design pass (Astra, gpt-6-astra high, 2026-10-04) and adjudicationAccepted, verified against code:
Added by me:
Rejected:
|
…TRA-787) The quota sweep now asks the provider executor for the optional QuotaUsageReader capability instead of executors matching the usage URL inside HttpRequest, which keeps its 5fde8ee contract. The Claude usage read reuses fetchOAuthControlPlaneJSON through a header hook, and its header order and path now live in the fork file. The Claude usage read resolves its proxy with the same precedence as helps.NewUtlsHTTPClient through an exported helps.EffectiveProxyURL. The shared Codex identity helper skips Chatgpt-Account-Id for an empty account id, the usage read no longer sends Accept, and the helper comment describes HTTP inference only. The Claude User-Agent comment says the read uses the configured CLI baseline, which inference also sends for unconfirmed clients with stabilize-device-profile off. The self-comparing ClientHello and Codex identity tests are replaced by a CONNECT-tunnel ALPN check through the production transport and literal Codex identity expectations, including the cloaking-off and empty-account cases. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…MASTRA-787) KimiExecutor embeds ClaudeExecutor and so inherits ReadQuotaUsage; without a provider check the sweep sent Kimi OAuth tokens to api.anthropic.com every five minutes. The sweep now reads only enabled codex and claude credentials, matching the refresh loop's skip of disabled ones, and a regression test asserts which tokens reach the transport. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Linear: MASTRA-787, AC7. Follows #7.
The usage reads from #7 went out with only the token and one provider header, so they presented Go's default User-Agent. Eric decided on 2026-10-04 that, before deploy, they must present the same client identity as each account's real CLI traffic. This PR does that by having each executor send its own usage read.
What changed
a298c45c: eligibility fix after re-review. The service reads usage only for enabledcodexandclaudeOAuth credentials.KimiExecutorembedsClaudeExecutor, so it inheritsReadQuotaUsage. At 20e624c the sweep therefore sent Kimi OAuth tokens toapi.anthropic.comevery 5 minutes (found by the Opus re-review, which reproduced it).TestQuotaUsageReadSkipsKimiAndDisabledCredentialsasserts the exact tokens that reach the transport (only the enabled Claude credential's). Removing the provider check makes the test reportkimi-token. Removing the disabled check makes it reportclaude-off-token.Chatgpt-Account-Id, matching the websocket path.20e624cc, the design after review. Usage reads are an explicit optional executor capability,coreauth.QuotaUsageReader(ReadQuotaUsage(ctx, auth) (http.Header, error), in the fork filesdk/cliproxy/auth/quota_usage_reader.go).ClaudeExecutor,CodexExecutorandCodexAutoExecutorimplement it.CodexWebsocketsExecutorgets it by embedding.service_quota.gogets the executor throughManager.Executorand owns eligibility: OAuth, a valid token, and the capability. It no longer has a URL switch, parser selection or response handling.HttpRequestmethods are back to their upstream contract.fetchOAuthControlPlaneJSONthrough a header hook instead of copying it.EffectiveProxyURLprecedence.Chatgpt-Account-Id, which now matches the websocket path. The usage read sets noAccept.470363a0, first versionClaudeExecutor.HttpRequestrecognizes an OAuthGET https://api.anthropic.com/api/oauth/usageand sends it as Claude Code 2.1.288's API client does.Accept: application/json, text/plain, */*,Content-Type: application/json,User-Agent: claude-cli/2.1.280 (external, cli),Authorization,anthropic-beta: oauth-2025-04-20,Accept-Encoding: gzip, compress, deflate, br,Host,Connection: close.defaultClaudeDeviceProfile), so one account shows one CLI version.readClaudeOAuthResponseBody, because the request advertises compression.internal/auth/claude/oauth_usage.goandinternal/runtime/executor/claude_executor_usage.go. Upstream files gained 3 lines inclaude_executor.goand the header-order entry inutls_transport.go.Chatgpt-Account-Id, custom-header and cloaking logic moved out ofapplyCodexHeadersFromSourcesintoapplyCodexIdentityHeaders. Inference and the usage GET both call it, and the usage GET addsAccept: application/json. Codex keeps the executor's Chrome uTLS transport. Inference behaviour is unchanged.service_quota.gono longer sets any headers. The provider switch now selects only the URL and the parser.Size: 159 non-test lines added, 21 removed. Tests: 372 lines.
Where the header set comes from
The source is the installed CLIs, through a design pass by Astra (gpt-6-astra, high). The adjudication is on MASTRA-787.
CF -> qt.get -> axios, with User-AgentnO()=claude-cli/<ver> (external, cli)and the OAuth beta header. It sends noCache-Control./wham/usagepath and thecodex_cli_rs/codex-tuiidentity code. The executor's inference identity is the closest evidenced match.Acceptance criterion AC7
TestUsageReadTransportReceivesCLIIdentitycompares the method, URL and full header map for each provider with a literal fixture.TestFetchOAuthUsageSerializesNativeHeaderOrderchecks the exact Claude request lines, including order and casing.TestClaudeUsageReadDialsControlPlaneTLSThroughCredentialProxysetsauth.ProxyURLto a local CONNECT listener and runs the production path with no injected transport. It assertsCONNECT api.anthropic.com:443and a ClientHello with no ALPN, which is the control-plane profile. The inference profile carries ALPN, and adding ALPN makes the test fail.TestCodexUsageReadMatchesInferenceIdentitycovers six cases with literal expectations built fromcodexUserAgentandcodexOriginator, including cloaking off with no configured User-Agent, and an empty account ID.TestClaudeUsageReadDecodesCompressedResponsechecks that a gzip body still parses.HttpRequestwith no caller headers. I re-ran it from 20e624c throughReadQuotaUsage, and all 6 succeeded with 4 or 5 mapped signals and no auth-file mtime change.Checks
I ran these myself on the committed tree a298c45, and the same results held on 20e624c and 470363a:
go build ./...: exit 0.go vet ./sdk/cliproxy/... ./internal/...: exit 0.go test ./...: exit 0.go test -race -count=1oversdk/cliproxy,sdk/cliproxy/auth,internal/runtime/executor/...andinternal/auth/claude: two of three runs exited 0, and one exited 1. The failures were only in the three upstream tests that also fail onmain:TestHandleAuthUpdates_SameRevisionWaitDoesNotWaitForOtherAuthInBatchandTestEndToEndAuthFile{Replacement,Patch}_RestoresModelsInV1ModelsWithoutRestart.Review
Round 1:
/pstack:interrogateon 470363a. Three reviewers: Opus 5.5 at max (8 findings), gpt-5.6-sol at max (4) and Grok 4.7 at xhigh (2).HttpRequestbreaks its contract and splits eligibility. Raised by Opus. Fixed with the executor capability.Acceptprovenance and override. Raised by Opus and Grok.compressencoding can't be decoded (Sol). The fork's profile fetch has the same issue, so it predates this PR.Executor(auth.Provider)andexecutorKeyFromAuthdiffer only forcompat_name/provider_keycredentials (fix lane). The Codex and Claude OAuth credentials have neither.🤖 Generated with Claude Code