Skip to content

feat(auth): add Kimi Code OAuth login - #851

Open
euxaristia wants to merge 26 commits into
Gitlawb:mainfrom
euxaristia:feat/kimi-oauth-login
Open

feat(auth): add Kimi Code OAuth login#851
euxaristia wants to merge 26 commits into
Gitlawb:mainfrom
euxaristia:feat/kimi-oauth-login

Conversation

@euxaristia

@euxaristia euxaristia commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Adds Kimi Code as an OAuth login option alongside ChatGPT, using the device-code flow with Kimi's required X-Msh-* vendor-identity headers
  • Introduces a dedicated internal/kimiidentity package to manage the per-device Kimi identity file, including lazy creation, abandoned/empty-file repair with an ownership-safe lock, and stripping the identity headers when a request retargets off the Kimi catalog endpoint
  • Provider catalog, CLI (zero auth kimi), and TUI onboarding all route through the shared device-code flow, with the desktop Enter key routed to device code and polling cancelled on every quit path

Test plan

  • go test ./internal/kimiidentity/... ./internal/oauth/... ./internal/providercatalog/... ./internal/tui/... ./internal/cli/...

Summary by CodeRabbit

  • New Features

    • Added Kimi Code as an OAuth provider with device-code authentication, managed coding endpoints, aliases, and required identity handling.
    • Added a dedicated zero auth kimi login command.
    • Expanded OAuth setup guidance and provider documentation.
  • Bug Fixes

    • OAuth device login now cancels cleanly when exiting, navigating, or timing out.
    • Prevented stale login results and persisted provider headers from overwriting current settings.
    • Improved support for configured headers during OAuth requests.
    • Preserved custom headers when provider endpoints are overridden.

euxaristia and others added 21 commits July 30, 2026 22:21
Add a first-class Kimi Code OAuth provider modeled on the generic
device-code preset path (like xAI), not the bespoke ChatGPT/Codex path.
Kimi's returned access token works directly as a Bearer on its managed
coding endpoint (api.kimi.com/coding/v1), so no claim extraction is
needed.

- internal/providercatalog/catalog.go: new `kimi` catalog entry,
  OpenAI-compatible at https://api.kimi.com/coding/v1, flagged OAuth +
  OAuthDeviceFlow, RequiresAuth with no API-key env var. Listed directly
  below the ChatGPT entry so it appears below ChatGPT in the OAuth options.
- internal/oauth/presets.go: kimi device-code preset (auth.kimi.com
  endpoints, public kimi-code client_id); overridable via ZERO_OAUTH_KIMI_*.
- internal/cli/auth.go: add `zero auth kimi` sugar routing to the
  generic device-code login; list kimi below chatgpt in help text.
- internal/tui/provider_wizard.go: mention Kimi in the OAuth method
  subtitle and include it in the OAuth provider list.
- docs/oauth-subscriptions.md: document the Kimi Code OAuth path.
- Tests: presets, catalog, and oauth coverage updated.
One extra leading space made it the only misaligned entry in the
provider list.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…s model default

Get() matches an exact descriptor ID before it ever reaches another
descriptor's aliases. moonshot already aliases "kimi" to itself (its
API-key path at api.moonshot.ai), so giving the new Kimi Code OAuth
descriptor the same canonical ID silently stole that alias: any
existing profile with catalogID "kimi" would resolve to the new
OAuth-only descriptor instead of moonshot, changing its endpoint,
default model, and auth method without the user asking for it.
Renamed the descriptor (and preset key, CLI routing, TUI references,
docs) to "kimi-code" — "zero auth kimi" still works as a shortcut, it
just forwards to the non-colliding ID.

Also fixed the default model: "kimi-k2.7-code-highspeed" does not
exist on the managed endpoint. The real models are "kimi-for-coding"
(standard tier, all members) and "kimi-for-coding-highspeed" (requires
a higher subscription tier). Defaulted to the standard tier so a fresh
login doesn't select a model the user's plan may not include.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Kimi Code's OAuth/API backend rejects device-authorization, poll, and
refresh requests with 401 unless a handful of vendor-identity headers
are present; the generic RFC 8628/OAuth2 form bodies this package
builds had no way to carry them, so every real login attempt would
have failed. Added Config.ExtraHeaders (mirroring the existing
ExtraAuthParams pattern), applied at every request-building site
(RequestDeviceCode, pollDeviceOnce, PostToken — shared by code exchange
and refresh), and a new providerExtraHeaders hook in ResolveConfig that
supplies Kimi's headers regardless of ZERO_OAUTH_ALLOW_PRESETS (this is
a protocol requirement of Kimi's own backend, not tied to which
client_id is in use).

Header names, general shape, and the client_id were reverse-engineered
from the open-source kimi-cli client
(github.com/MoonshotAI/kimi-cli, src/kimi_cli/auth/oauth.py) since Kimi
has no public API documentation for this; values are a best-effort
match, not a verified spec — confirm against a real login before this
ships. Device-Id is generated fresh per process rather than persisted
to disk (kimi-cli persists it across runs); the header just needs to be
present and ASCII-safe, not stable long-term, so this is a disclosed
simplification, not a functional gap.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…device code

zero auth kimi discarded every argument after "kimi", so `zero auth
kimi --help` started a real device authorization instead of showing
help, --scope was silently dropped, and typos went unvalidated. Now
forwards args[1:] through the same parser `zero auth login` uses.

Kimi Code has no browser/loopback OAuth endpoint at all, but the
provider wizard's plain Enter on an OAuth-capable provider defaults to
the loopback flow unless the environment already prefers device code
(headless/SSH) — on a normal desktop session, pressing Enter on Kimi
in /provider or first-run onboarding would attempt a flow that has no
endpoint to hit. Added Descriptor.OAuthDeviceOnly and gated Enter to
also start device login when it's set, alongside the existing "d"
shortcut. Adjusted the footer hint and the "Sign in with OAuth"
subtitle, which advertised Kimi under "one-click browser login"
alongside providers that actually have one.

Updated docs/oauth-subscriptions.md, which had the same "Kimi has
browser login" claim in two places, directly contradicting its own
"Kimi is device-code only" section further down.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Three review findings:

- The X-Msh-* vendor-identity headers existed only in the OAuth request
  path, so after a successful login the OpenAI-compatible runtime sent
  /coding/v1 completions with just the bearer and Kimi's backend could
  reject every call. The header set now lives in a shared kimiidentity
  package used by both the OAuth flows and the kimi-code descriptor's
  CustomHeaders (populated lazily at catalog access so package import
  does no filesystem IO), and the device ID is persisted under the user
  config dir — mirroring kimi-cli's ~/.kimi/device_id — so login,
  refresh, and completions present one stable device identity.

- First-run onboarding routed a desktop Enter on a device-only provider
  through the generic browser-login command, which discarded the device
  flow's verification URL and code and left the spinner to time out.
  The OAuthDeviceOnly check the /provider wizard already gained now
  applies to onboarding too, with a desktop regression test.

- docs/oauth-subscriptions.md documented ZERO_OAUTH_KIMI_* override
  names that nothing reads; the provider resolves as kimi-code, so the
  documented names now carry the KIMI_CODE prefix.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Avoid a race where concurrent first-run processes could both miss the
device-identity file and overwrite each other's ID, by using an
exclusive create and adopting the winner's ID on conflict. Remove a
redundant header map copy in provider catalog cloning. Route the
mouse double-click activation path through the same OAuthDeviceOnly
check the keyboard path already used, so it no longer attempts a
browser OAuth flow that doesn't exist for Kimi.
Only attach kimi-code X-Msh-* headers (and mint the on-disk device id) on
Get/Require, not All/OAuthProviders listing. Add tests for moonshot "kimi"
alias resolution, runtime-header laziness, exclusive device-id creation, and
mouse advance on device-only providers.
… + testable device-id loader

- Add Descriptor.RuntimeHeaders so cloneDescriptor no longer special-cases
  descriptor.ID == "kimi-code"; the kimi-code entry supplies
  kimiidentity.Headers, keeping the generic clone helper provider-agnostic and
  avoiding the redundant map copy on lazy header population.
- Parameterize DeviceID loading behind loadOrCreateDeviceIDAt(path) so tests
  exercise the production exclusive-create + winner-adopt logic directly
  instead of re-implementing it, and add a read-existing test.
main added this test with PostToken's old 6-arg signature after this
branch forked; PostToken here already takes extraHeaders, so the merge
with main compiled fine textually but failed go vet.
Retry-read after exclusive create so concurrent first-run processes
adopt the winner UUID even if they observe an empty file mid-write.
Sync the written id before closing. Document device-code for xAI and
Hugging Face in the OAuth chooser summary, and state that Kimi X-Msh-*
headers apply across all OAuth and managed API calls.
Use kimi_code_cli for X-Msh-Platform, re-resolve RuntimeHeaders when the
provider wizard builds a profile, and cancel in-flight device-code polls
on Esc so abandoned logins do not silently persist credentials.

Refs Gitlawb#708
If an exclusive create leaves an empty or invalid file (winner dies
before writing the UUID), remove it once after the adopt retry window
and exclusive-create again so callers converge on a persisted identity
instead of permanently diverging.

Refs Gitlawb#708
…alog endpoint

A profile that started on api.kimi.com and persisted the descriptor's
X-Msh-* headers only had those headers stripped on retargeting if the
descriptor was aimlapi. Generalize the existing non-canonical-endpoint
cleanup to any descriptor with catalog-owned headers, so a Kimi profile
whose baseURL is later pointed at a proxy or staging host no longer
sends its device identity there. User-supplied headers are still
preserved.
When the persisted device-id file was invalid or empty (a previous
process died mid-publish), every racing process would retry-read, then
unconditionally remove and recreate it. A process could remove another
process's just-published winner between that process's failed retry
read and its own remove call, so the original process kept an id that
no longer matched what was on disk.

Repair is now serialized through an exclusive lock file: only the lock
holder removes and recreates the id file, and everyone else waits to
adopt whatever it publishes instead of attempting their own repair.
Added a concurrent test that repairs the same abandoned file from many
goroutines and checks they all converge on one persisted id.
…ttempts

Ctrl+C during a first-run device-code poll, and the provider wizard's
own quit path (model.quit, which only reset the aimlapi sub-flow),
both returned tea.Quit without canceling the in-flight poll. Since the
TUI runs on context.Background(), a login the user backed out of by
quitting could still complete in the background and get persisted.
Both quit paths now cancel any active setup/provider-wizard device
login first.

Separately, first-run setup's device-code messages only carried a
providerID, so abandoning a Kimi login with Esc and immediately
restarting it let a late phase-one result from the first attempt
overwrite the second attempt's displayed code and start polling an
authorization already backed out of. Added an attempt generation
(mirroring the provider wizard's existing oauthAttemptID) that bumps on
every restart and is required for a phase-one or poll result to apply.

Added regression tests for Ctrl+C canceling the poll in both setup and
the provider wizard, and for a stale device-code attempt being rejected.
The guide told users to set ZERO_OAUTH_ALLOW_PRESETS=1 for Kimi, but
zero auth kimi and zero auth login kimi-code both go through the auth
login engine, which enables presets unconditionally. The documented
commands already work without that variable.
…air lock

Restrict X-Msh-* device headers to canonical Kimi hosts on endpoint overrides, make device-id repair locks ownership-safe with token checking, honor cancellation in CompleteDeviceLogin before storing tokens, and strip runtime identity headers from persisted config profiles.

Refs Gitlawb#708
@coderabbitai

coderabbitai Bot commented Jul 31, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@euxaristia, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 4 minutes

Your organization has reached its usage spending cap. Adjust your spending cap in the billing tab.

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: d42dba7b-f9e4-40da-98b2-bddf233863ef

📥 Commits

Reviewing files that changed from the base of the PR and between 910eb37 and d41e06f.

📒 Files selected for processing (9)
  • internal/config/resolver_test.go
  • internal/kimiidentity/kimiidentity_test.go
  • internal/oauth/providers_test.go
  • internal/providercatalog/catalog_test.go
  • internal/providercatalog/export_test.go
  • internal/tui/onboarding.go
  • internal/tui/onboarding_test.go
  • internal/tui/provider_wizard.go
  • internal/tui/provider_wizard_oauth_test.go

Walkthrough

Kimi Code is added as a device-only OAuth provider. The change adds persistent identity headers, propagates provider headers through OAuth requests, supports cancellation and stale-attempt rejection in TUI flows, adds a CLI alias, and updates profile resolution and documentation.

Changes

Kimi Code OAuth

Layer / File(s) Summary
Persistent Kimi identity
internal/kimiidentity/*
Generates sanitized identity headers and persists a device UUID with concurrent creation and repair handling.
OAuth headers and preset
internal/oauth/*
Adds ExtraHeaders to OAuth requests and defines the Kimi Code device-flow preset with approved endpoint handling.
Provider catalog and runtime descriptors
internal/providercatalog/*
Adds Kimi Code as a device-only OAuth provider and generates runtime headers only during direct lookup.
Cancellable device login in TUI
internal/tui/*
Tracks OAuth attempts, cancels polling, rejects stale results, and starts device login automatically for Kimi Code.
Profile header resolution
internal/config/resolver.go, internal/config/resolver_test.go, internal/tui/provider_wizard.go, internal/tui/provider_wizard_oauth_test.go
Preserves runtime headers for canonical endpoints and removes X-Msh-* headers from retargeted profiles.
CLI and OAuth documentation
internal/cli/auth.go, docs/oauth-subscriptions.md
Adds the kimi auth alias and documents Kimi Code OAuth behavior and configuration.

Estimated code review effort: 5 (Critical) | ~90 minutes

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant TUI
  participant OAuthManager
  participant KimiCode
  participant ConfigStore
  User->>TUI: select Kimi Code and start login
  TUI->>OAuthManager: request device code
  OAuthManager->>KimiCode: send device request with identity headers
  KimiCode-->>TUI: return device code
  TUI->>OAuthManager: poll with cancellable context
  OAuthManager->>KimiCode: request token
  KimiCode-->>OAuthManager: return token
  OAuthManager->>ConfigStore: persist credentials
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 53.52% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: adding Kimi Code OAuth login support.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 9

🧹 Nitpick comments (7)
internal/kimiidentity/kimiidentity_test.go (1)

270-274: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Fix the staleness threshold in this comment.

The comment says > 1s. The production threshold in repairAbandonedDeviceID is 2*time.Second. The 5-second backdate still works, but the wrong number here will mislead the next person who tunes the threshold.

♻️ Proposed comment fix
-	// Backdate lock file mtime to make it stale (> 1s)
+	// Backdate lock file mtime to make it stale (> 2s, the repair threshold)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/kimiidentity/kimiidentity_test.go` around lines 270 - 274, Update
the comment above the lock-file timestamp backdating in the test to state that
the mtime is made stale by exceeding the 2-second threshold used by
repairAbandonedDeviceID; leave the 5-second backdate and test logic unchanged.
internal/oauth/presets.go (2)

174-181: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Tighten and clarify the host allowlist.

Two points.

First, the allowlist is partly redundant. strings.HasSuffix(host, ".kimi.com") already covers auth.kimi.com and api.kimi.com, so only the bare kimi.com case needs a separate test. The suffix tests are correct as written; the leading dot prevents a notkimi.com match.

Second, the domain set looks inconsistent. .moonshot.cn is approved, but bare moonshot.cn is not, and .moonshot.ai is absent even though the existing moonshot descriptor uses api.moonshot.ai. State the intent in a comment, or narrow the list to the hosts Kimi Code actually serves.

♻️ Proposed simplification
 	host := strings.ToLower(u.Hostname())
-	return host == "auth.kimi.com" || host == "api.kimi.com" || host == "kimi.com" || strings.HasSuffix(host, ".kimi.com") || strings.HasSuffix(host, ".moonshot.cn")
+	for _, domain := range []string{"kimi.com", "moonshot.cn"} {
+		if host == domain || strings.HasSuffix(host, "."+domain) {
+			return true
+		}
+	}
+	return false
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/oauth/presets.go` around lines 174 - 181, Update isCanonicalKimiHost
to remove the redundant auth.kimi.com and api.kimi.com checks, retaining the
bare kimi.com and dotted suffix checks. Clarify the intended allowlist with a
comment or narrow it to hosts Kimi Code actually serves; ensure moonshot.cn
handling is consistent with .moonshot.cn and include or exclude .moonshot.ai
based on the existing api.moonshot.ai usage.

162-172: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

One non-canonical override silently strips the headers from the canonical requests too.

providerExtraHeaders returns nil if any single supplied endpoint is non-canonical. Consider an operator who overrides only ZERO_OAUTH_KIMI_CODE_TOKEN_URL to a proxy and leaves the device endpoint at auth.kimi.com. The device-authorization request then goes to the canonical Kimi host without the X-Msh-* headers. Per the comment at Lines 183-190, Kimi answers 401. The user sees an authorization failure with no indication that an override caused it.

The fail-closed choice is right; do not send identity headers to an unapproved host. The problem is that the decision is all-or-nothing and invisible.

Two options, in order of preference:

  1. Decide per endpoint. Attach the headers to a request only when that request's own host is canonical. This keeps login working when only the token URL is proxied.
  2. If you keep the all-or-nothing rule, surface it. Emit a warning at login time that names the offending environment variable and states that vendor-identity headers are disabled.

Also document the behavior in docs/oauth-subscriptions.md so the override is not a trap.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/oauth/presets.go` around lines 162 - 172, Update
providerExtraHeaders to decide Kimi vendor-identity headers per endpoint rather
than disabling them globally when any override is non-canonical: return headers
only for requests whose own host is canonical, while preserving the fail-closed
behavior for proxied hosts. Ensure the endpoint-to-header decision is available
to each request, and document the mixed canonical/non-canonical override
behavior in docs/oauth-subscriptions.md.
internal/oauth/flow_test.go (1)

156-156: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add a test that asserts the extra headers reach the wire.

Both updated call sites pass nil for the new extraHeaders parameter, which is correct. No test in this cohort verifies the positive case, though. TestResolveConfigKimiCodePreset checks only that Config.ExtraHeaders is populated; nothing proves applyExtraHeaders puts those headers on the outgoing request.

Add a test with an httptest server that records r.Header and assert the X-Msh-* values arrive. Cover PostToken at minimum, and ideally RequestDeviceCode and pollDeviceOnce too, since Kimi requires the headers on all three.

The repository guidelines require a regression test for behavior changes.

Also applies to: 260-260

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/oauth/flow_test.go` at line 156, Add regression coverage for
extra-header propagation using an httptest server that records incoming request
headers. Extend the PostToken test around its extraHeaders argument to pass
representative X-Msh-* values and assert they arrive on the server request; also
cover RequestDeviceCode and pollDeviceOnce if their test fixtures permit,
ensuring all Kimi-required flows verify the headers reach the wire.

Source: Coding guidelines

internal/oauth/providers.go (1)

80-80: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Pass the endpoint overrides as a slice for readability.

The call takes four same-typed positional arguments after name. A reader cannot tell the order without opening providerExtraHeaders. A single []string argument, or reuse of the already-computed cfg endpoint values, would read better.

Note that this line resolves the endpoint values a second time. Lines 75-78 already compute the same environment lookups.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/oauth/providers.go` at line 80, Update the ExtraHeaders construction
in the provider configuration to reuse the endpoint values already computed in
cfg on lines 75-78, passing them as a single ordered []string to
providerExtraHeaders instead of repeating positional envValue lookups. Adjust
providerExtraHeaders as needed to accept the slice while preserving endpoint
order.
internal/tui/onboarding.go (1)

848-857: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Extract the device-only bypass rule into one shared helper. Both wizards independently implement the same condition to decide when to skip the browser OAuth flow for a device-only provider like Kimi Code, and each file's comment notes the other must be kept in sync manually.

  • internal/tui/onboarding.go#L848-L857: replace the inline descriptor.OAuthDeviceFlow && (descriptor.OAuthDeviceOnly || oauthPreferDeviceFlow()) check with a call to a shared helper, e.g. useDeviceLogin(descriptor).
  • internal/tui/provider_wizard_discovery.go#L46-L51: replace the identical inline check on provider with the same shared helper, so both wizards can never diverge on this rule.
♻️ Proposed shared helper
// useDeviceLogin reports whether the OAuth flow for this provider should go
// straight to device-code login instead of the browser flow: either the
// provider has no browser/loopback endpoint at all (OAuthDeviceOnly), or the
// environment prefers device flow (headless/SSH).
func useDeviceLogin(d providercatalog.Descriptor) bool {
	return d.OAuthDeviceFlow && (d.OAuthDeviceOnly || oauthPreferDeviceFlow())
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/tui/onboarding.go` around lines 848 - 857, Extract the shared
device-login condition into a helper such as useDeviceLogin(d
providercatalog.Descriptor), preserving the OAuthDeviceFlow, OAuthDeviceOnly,
and oauthPreferDeviceFlow logic. In internal/tui/onboarding.go lines 848-857,
replace the inline check with this helper; make the same replacement for
provider in internal/tui/provider_wizard_discovery.go lines 46-51 so both
wizards use the centralized rule.
internal/providercatalog/catalog.go (1)

452-460: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

cloneDescriptor drops runtime headers for any descriptor that also has static CustomHeaders.

The else if makes the two header sources mutually exclusive. A descriptor with both static CustomHeaders and RuntimeHeaders silently loses its runtime headers. No current descriptor has both, so this is not a live bug. The doc comment on RuntimeHeaders presents the mechanism as provider-agnostic, so merge instead of choosing.

♻️ Proposed merge of both header sources
-	if descriptor.CustomHeaders != nil {
-		descriptor.CustomHeaders = copyStringMap(descriptor.CustomHeaders)
-	} else if withRuntimeHeaders && descriptor.RuntimeHeaders != nil {
-		descriptor.CustomHeaders = descriptor.RuntimeHeaders()
-	}
+	static := copyStringMap(descriptor.CustomHeaders)
+	if withRuntimeHeaders && descriptor.RuntimeHeaders != nil {
+		merged := descriptor.RuntimeHeaders()
+		for key, value := range static {
+			merged[key] = value
+		}
+		descriptor.CustomHeaders = merged
+	} else {
+		descriptor.CustomHeaders = static
+	}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/providercatalog/catalog.go` around lines 452 - 460, Update
cloneDescriptor so withRuntimeHeaders merges RuntimeHeaders into any copied
static CustomHeaders instead of making the sources mutually exclusive. Preserve
static headers, add runtime headers when available, and retain nil behavior when
neither source exists.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@internal/config/resolver.go`:
- Around line 1095-1110: Update the conditional guarding the custom-header
cleanup in the descriptor resolution flow to use the presence of
descriptor.RuntimeHeaders rather than len(descriptor.CustomHeaders). Preserve
removal of matching catalog headers and all X-Msh-* headers for non-canonical
endpoints, including when RuntimeHeaders produces an empty CustomHeaders map.

In `@internal/kimiidentity/kimiidentity.go`:
- Around line 71-77: Remove the exported ResetDeviceIDForTest seam from the
production file to satisfy unreachable-function checks, and replace it with a
lint-compatible test seam such as an unexported deviceIDPathOverride used by the
device-ID loading path. Update tests to drive isolation through that seam while
preserving production behavior and synchronization.
- Around line 194-216: Update createOrAdoptDeviceID’s successful os.Rename path
in repairAbandonedDeviceID to re-read path with readValidDeviceID (or the
established retry helper) and return the persisted device ID when available,
falling back to id only if necessary. Preserve the existing rename and
error-handling behavior; do not add lock-mtime refresh unless separately
required.
- Around line 53-56: Run gofmt on internal/kimiidentity/kimiidentity.go to
normalize the var-block alignment and remove the consecutive blank lines after
ResetDeviceIDForTest, ensuring the file passes make fmt-check.

In `@internal/oauth/providers_test.go`:
- Around line 90-101: Prevent device-ID test state from leaking across platforms
and test order by exporting a shared user-config redirect helper from
internal/kimiidentity, based on setUserConfigRoot, that redirects APPDATA, HOME,
and XDG_CONFIG_HOME to t.TempDir() and resets the cached ID via t.Cleanup. Use
this helper in internal/oauth/providers_test.go at lines 90-101 and
internal/oauth/presets_test.go at lines 191-197; the latter must no longer omit
HOME. Also apply the same helper to internal/providercatalog/catalog_test.go’s
TestKimiRuntimeHeadersOnlyOnGet.
- Around line 104-107: Run gofmt on the map literal passed to ResolveConfig in
the relevant test, correcting the spacing between the ZERO_OAUTH_ALLOW_PRESETS
key and its value to match the longest key. Then run gofmt -l ./... and ensure
no formatting drift remains.

In `@internal/providercatalog/catalog_test.go`:
- Around line 494-499: Real Kimi device IDs are written outside the test sandbox
because config-root isolation is incomplete. Add a shared helper modeled on
setKimiUserConfigRoot that sets XDG_CONFIG_HOME, APPDATA, and HOME to
t.TempDir() and resets the device ID before and after; use it at
internal/providercatalog/catalog_test.go lines 494-499, 458-489 before Get
calls, and at internal/tui/provider_wizard_oauth_test.go lines 624-642 before
mouseTestModel().

In `@internal/providercatalog/catalog.go`:
- Around line 263-290: Remove the unused ListByTransport, ValidTransport, and
ValidAPIFormat helpers from the change. Do not add replacement abstractions or
broaden the scope; only retain these helpers if a production caller is added and
uses them.

In `@internal/tui/provider_wizard.go`:
- Around line 2144-2155: Update the Kimi profile construction around
providercatalog.Get and the profile.CustomHeaders assignment to preserve the
re-resolved X-Msh-* identity headers expected by
TestProviderWizardProfileAppliesKimiRuntimeHeaders. Remove the provider.ID ==
"kimi-code" strip loop, leaving the cloned runtime descriptor headers in
profile.CustomHeaders.

---

Nitpick comments:
In `@internal/kimiidentity/kimiidentity_test.go`:
- Around line 270-274: Update the comment above the lock-file timestamp
backdating in the test to state that the mtime is made stale by exceeding the
2-second threshold used by repairAbandonedDeviceID; leave the 5-second backdate
and test logic unchanged.

In `@internal/oauth/flow_test.go`:
- Line 156: Add regression coverage for extra-header propagation using an
httptest server that records incoming request headers. Extend the PostToken test
around its extraHeaders argument to pass representative X-Msh-* values and
assert they arrive on the server request; also cover RequestDeviceCode and
pollDeviceOnce if their test fixtures permit, ensuring all Kimi-required flows
verify the headers reach the wire.

In `@internal/oauth/presets.go`:
- Around line 174-181: Update isCanonicalKimiHost to remove the redundant
auth.kimi.com and api.kimi.com checks, retaining the bare kimi.com and dotted
suffix checks. Clarify the intended allowlist with a comment or narrow it to
hosts Kimi Code actually serves; ensure moonshot.cn handling is consistent with
.moonshot.cn and include or exclude .moonshot.ai based on the existing
api.moonshot.ai usage.
- Around line 162-172: Update providerExtraHeaders to decide Kimi
vendor-identity headers per endpoint rather than disabling them globally when
any override is non-canonical: return headers only for requests whose own host
is canonical, while preserving the fail-closed behavior for proxied hosts.
Ensure the endpoint-to-header decision is available to each request, and
document the mixed canonical/non-canonical override behavior in
docs/oauth-subscriptions.md.

In `@internal/oauth/providers.go`:
- Line 80: Update the ExtraHeaders construction in the provider configuration to
reuse the endpoint values already computed in cfg on lines 75-78, passing them
as a single ordered []string to providerExtraHeaders instead of repeating
positional envValue lookups. Adjust providerExtraHeaders as needed to accept the
slice while preserving endpoint order.

In `@internal/providercatalog/catalog.go`:
- Around line 452-460: Update cloneDescriptor so withRuntimeHeaders merges
RuntimeHeaders into any copied static CustomHeaders instead of making the
sources mutually exclusive. Preserve static headers, add runtime headers when
available, and retain nil behavior when neither source exists.

In `@internal/tui/onboarding.go`:
- Around line 848-857: Extract the shared device-login condition into a helper
such as useDeviceLogin(d providercatalog.Descriptor), preserving the
OAuthDeviceFlow, OAuthDeviceOnly, and oauthPreferDeviceFlow logic. In
internal/tui/onboarding.go lines 848-857, replace the inline check with this
helper; make the same replacement for provider in
internal/tui/provider_wizard_discovery.go lines 46-51 so both wizards use the
centralized rule.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 6fe85d3e-89da-4094-adb6-1e5d3b99e47c

📥 Commits

Reviewing files that changed from the base of the PR and between 8e26679 and ba6ed17.

📒 Files selected for processing (26)
  • docs/oauth-subscriptions.md
  • internal/cli/auth.go
  • internal/config/resolver.go
  • internal/config/resolver_test.go
  • internal/kimiidentity/kimiidentity.go
  • internal/kimiidentity/kimiidentity_test.go
  • internal/oauth/device.go
  • internal/oauth/flow.go
  • internal/oauth/flow_test.go
  • internal/oauth/manager.go
  • internal/oauth/oauth.go
  • internal/oauth/presets.go
  • internal/oauth/presets_test.go
  • internal/oauth/providers.go
  • internal/oauth/providers_test.go
  • internal/providercatalog/catalog.go
  • internal/providercatalog/catalog_test.go
  • internal/providercatalog/export_test.go
  • internal/providercatalog/oauth_test.go
  • internal/tui/model.go
  • internal/tui/oauth_device.go
  • internal/tui/onboarding.go
  • internal/tui/onboarding_test.go
  • internal/tui/provider_wizard.go
  • internal/tui/provider_wizard_discovery.go
  • internal/tui/provider_wizard_oauth_test.go
💤 Files with no reviewable changes (1)
  • internal/providercatalog/export_test.go

Comment thread internal/config/resolver.go Outdated
Comment thread internal/kimiidentity/kimiidentity.go
Comment thread internal/kimiidentity/kimiidentity.go Outdated
Comment thread internal/kimiidentity/kimiidentity.go
Comment thread internal/oauth/providers_test.go
Comment thread internal/oauth/providers_test.go
Comment thread internal/providercatalog/catalog_test.go Outdated
Comment thread internal/providercatalog/catalog.go Outdated
Comment thread internal/tui/provider_wizard.go Outdated
providerWizardProfile re-resolves through providercatalog.Get to carry
Kimi's X-Msh-* RuntimeHeaders into the wizard's post-login profile, but
an unconditional strip added right after undid that inside the same
sameProviderBaseURL(default) branch -- so the headers it just fetched
never survived. That block wasn't backed by a test or review comment,
and it broke TestProviderWizardProfileAppliesKimiRuntimeHeaders, the
regression test written earlier in this PR specifically to guard this
path. The real canonical-host restriction already lives in
internal/oauth/presets.go's isCanonicalKimiHost, which this code path
doesn't need to duplicate since it only runs when the base URL is
already the default.
Gate X-Msh identity stripping on RuntimeHeaders, re-read the device id
after repair rename, isolate config roots in tests, move test-only reset
into export_test.go, and drop unused providercatalog helpers.
@euxaristia

Copy link
Copy Markdown
Contributor Author

Addressed CodeRabbit findings in 910eb37:

  1. Gate X-Msh-* stripping on RuntimeHeaders (not only non-empty CustomHeaders).
  2. Dropped exported ResetDeviceIDForTest (path-keyed cache + config-root isolation instead).
  3. Re-read device id after repair rename / WriteFile fallback.
  4. Isolate XDG_CONFIG_HOME/APPDATA/HOME in all tests that touch Kimi device id.
  5. Removed unused ListByTransport / ValidTransport / ValidAPIFormat.

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Aug 1, 2026

Copy link
Copy Markdown

@euxaristia: I will perform a full review of the changes, including the fixes in 910eb37.

✅ Action performed

Full review finished.


Your included review limit is currently reached under our Fair Usage Limits Policy. This review may still proceed through usage-based billing if eligible. Your next included review will be available in 59 minutes.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
internal/providercatalog/catalog_test.go (1)

482-503: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Assert that listing does not create a device ID.

The test only checks that listing descriptors have no CustomHeaders. A regression could invoke RuntimeHeaders, discard the returned headers, and still persist the Kimi device ID.

After All() and OAuthProviders(), assert that the isolated config root has no Kimi device-ID file before calling Get("kimi-code").

As per coding guidelines, **/*_test.go requires a regression test for behavior changes.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/providercatalog/catalog_test.go` around lines 482 - 503, Extend
TestKimiRuntimeHeadersOnlyOnGet after the All() and OAuthProviders() listing
checks to assert that the isolated config root contains no persisted Kimi
device-ID file. Use the test’s existing isolation and device-ID storage symbols,
and keep the assertion before Get("kimi-code") so listing cannot create or
persist the device ID.

Source: Coding guidelines

🧹 Nitpick comments (1)
internal/kimiidentity/kimiidentity.go (1)

65-79: 🩺 Stability & Availability | 🔵 Trivial

Run identity handling under the race detector.

This package uses goroutines and shared identity-file repair paths, so run internal/kimiidentity with -race in an environment where Go can enable the race detector.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/kimiidentity/kimiidentity.go` around lines 65 - 79, Run the
internal/kimiidentity package tests with Go’s race detector enabled, using an
environment/toolchain where race detection is supported; verify that DeviceID
and the shared identity-file repair paths complete without reported data races.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@internal/kimiidentity/kimiidentity_test.go`:
- Line 13: Add a regression test in the test flow around IsolateDeviceIDStorage
that uses two temporary config roots sequentially, calls DeviceID after
switching roots, and verifies each root receives its own device ID rather than
reusing the cached value from the first root.

---

Outside diff comments:
In `@internal/providercatalog/catalog_test.go`:
- Around line 482-503: Extend TestKimiRuntimeHeadersOnlyOnGet after the All()
and OAuthProviders() listing checks to assert that the isolated config root
contains no persisted Kimi device-ID file. Use the test’s existing isolation and
device-ID storage symbols, and keep the assertion before Get("kimi-code") so
listing cannot create or persist the device ID.

---

Nitpick comments:
In `@internal/kimiidentity/kimiidentity.go`:
- Around line 65-79: Run the internal/kimiidentity package tests with Go’s race
detector enabled, using an environment/toolchain where race detection is
supported; verify that DeviceID and the shared identity-file repair paths
complete without reported data races.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 5df72e9e-020b-4def-a02f-2272ed0acc15

📥 Commits

Reviewing files that changed from the base of the PR and between ba6ed17 and 910eb37.

📒 Files selected for processing (11)
  • internal/config/resolver.go
  • internal/config/resolver_test.go
  • internal/kimiidentity/export_test.go
  • internal/kimiidentity/kimiidentity.go
  • internal/kimiidentity/kimiidentity_test.go
  • internal/oauth/presets_test.go
  • internal/oauth/providers_test.go
  • internal/providercatalog/catalog.go
  • internal/providercatalog/catalog_test.go
  • internal/tui/provider_wizard.go
  • internal/tui/provider_wizard_oauth_test.go
💤 Files with no reviewable changes (1)
  • internal/tui/provider_wizard.go
🚧 Files skipped from review as they are similar to previous changes (5)
  • internal/config/resolver.go
  • internal/config/resolver_test.go
  • internal/tui/provider_wizard_oauth_test.go
  • internal/oauth/providers_test.go
  • internal/oauth/presets_test.go

Comment thread internal/kimiidentity/kimiidentity_test.go
Add a path-keyed DeviceID regression across two config roots, and assert
All/OAuthProviders do not persist kimi-device-id before Get mints it.
@euxaristia

Copy link
Copy Markdown
Contributor Author

Addressed the two new CodeRabbit findings in the follow-up commit:

  1. Path-keyed cache regressionTestDeviceIDReloadsWhenConfigRootChanges mints under two sequential config roots and checks each root gets its own id (and file).
  2. Listing must not create device idTestKimiRuntimeHeadersOnlyOnGet now asserts the isolated kimi-device-id path is absent after All()/OAuthProviders() and present only after Get("kimi-code").

Also ran go test ./internal/kimiidentity/ -race.

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Aug 1, 2026

Copy link
Copy Markdown

@euxaristia: I will perform a full review of pull request #851. I will verify the device-ID cache isolation and that catalog listings do not create the Kimi device-ID file.

✅ Action performed

Full review finished.


Your included review limit is currently reached under our Fair Usage Limits Policy. This review may still proceed through usage-based billing if eligible. Your next included review will be available in 52 minutes.

coderabbitai[bot]
coderabbitai Bot previously approved these changes Aug 1, 2026

@Vasanthdev2004 Vasanthdev2004 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Thanks for the very thorough work here — the OAuth plumbing itself is in good shape and I checked the parts that usually go wrong on this path:

  • Redirects are refused on all four credential-bearing POSTs (RequestDeviceCode, pollDeviceOnce, PostToken, and Refresh via PostToken), so the new ExtraHeaders can't ride a 307 to another origin.
  • I drove a throwaway httptest TLS server through all four and confirmed the X-Msh-* values actually reach the wire — that part works.
  • providerExtraHeaders fails closed when an endpoint is overridden off a canonical Kimi host, and resolving a retargeted kimi-code profile through the real config.Resolve entry point does drop the identity headers while keeping user-supplied ones.
  • The kimi alias still resolves to moonshot, gofmt/go vet are clean, and ./internal/{kimiidentity,oauth,providercatalog,config,tui} all pass (the only internal/cli failure is the pre-existing symlink-privilege one).

That said, I'd like changes before this merges. The problems are all in the device-identity lifecycle, not the protocol work.

1. First-run onboarding mints the Kimi device id for every user. internal/tui/onboarding.go:117setupOAuthProviderOptions filters the provider list by calling providercatalog.Get(option.ID) on every option, and Get now runs RuntimeHeaderskimiidentity.DeviceID(). That function is reached from setupMethodOptions (onboarding.go:541), which is called from the render path setupMethodLines (onboarding.go:1665) as well as advanceSetup (onboarding.go:827). So the "How do you want to connect?" screen creates <UserConfigDir>/zero/kimi-device-id on first paint, before anyone has picked a provider.

I proved it with a temp config root: after All() and OAuthProviders() the file does not exist, and after one setupOAuthProviderOptions call it does. That's the exact invariant cloneDescriptor's own comment claims to protect ("merely enumerating providers never mints ~/.config/zero/kimi-device-id for users who never touch Kimi"). The filter only needs descriptor.OAuth, which the listing descriptors already carry — use OAuthProviders()/All() here, or add a headers-free lookup. Please add a test that walks the method + provider screens and asserts the file was never created; right now nothing would catch this coming back.

2. The wizard bakes the hostname and device UUID into config.json, and the resolver ignores them anyway. internal/tui/provider_wizard.go:2145 copies the runtime headers into profile.CustomHeaders, and applyProviderWizard persists that profile through config.UpsertProvider. I upserted a wizard-built profile into a temp config and read it back:

"customHeaders": {
  "X-Msh-Device-Id": "b5fd6ed8-50fb-4eec-8195-87571e6c8f60",
  "X-Msh-Device-Model": "windows amd64",
  "X-Msh-Device-Name": "Vasanth",
  ...
}

X-Msh-Device-Name is os.Hostname(). Two problems with persisting it. First, it's dead weight: applyCatalogDescriptor at internal/config/resolver.go:1074 deliberately skips stored x-msh-* values so the freshly minted ones win, so nothing ever reads the persisted copy. Second, it puts a machine hostname and a stable device UUID into a file people paste into bug reports, and the strip in resolver.go:1112 only fires while catalogId is present — resolving a profile with those headers, catalogId removed and baseURL: https://proxy.example.test/v1 forwards X-Msh-Device-Id and X-Msh-Device-Name to that host untouched. Note zero auth kimi (EnsureCatalogProvider) does not persist them, so the CLI and TUI already disagree. Simplest fix: keep the wizard's in-memory profile for the discovery call, but don't write RuntimeHeaders-derived headers to disk — the resolver re-attaches them at resolve time.

3. Both new guards in applyCatalogDescriptor are unproven. I removed the x-msh- prefix delete (resolver.go:1112-1116) and the stored-header override guard (resolver.go:1074-1076) and ran go test ./internal/config/ -count=1 — fully green. TestApplyCatalogDescriptorStripsKimiIdentityFromRetargetedProfile (resolver_test.go:1577) passes only because Require("kimi-code") returns a CustomHeaders map containing every X-Msh-* key, so the pre-existing EqualFold catalog-key loop already deletes both of the profile's headers. The only new code that test exercises is the widened else if condition. Please add: a case where the descriptor has RuntimeHeaders but empty CustomHeaders (the case the prefix branch exists for), and a case where a stale persisted X-Msh-Device-Id must lose to the freshly minted one on the canonical endpoint. Both should go red if the corresponding line is deleted.

4. Two existing tests were deleted rather than updated.

  • internal/providercatalog/catalog_test.goTestListByTransportPreservesCatalogOrder is gone (it was at line 364 on main), along with its export_test.go helper. That test pinned per-transport catalog ordering and the TransportOpenAICompatible/TransportAnthropicCompatible alias normalisation. Adding kimi-code into the openai-compatible run breaks its wantIDs list, so the fix was one line: add "kimi-code" after "chatgpt". Deleting the only ordering guard to make a new descriptor fit isn't a trade I want to make. (Swapping ValidTransport/ValidAPIFormat for inline maps in TestCatalogDescriptorsExposeRequiredDefaults is fine — that keeps the per-descriptor invariant.)
  • internal/oauth/providers_test.goTestEnvKey was replaced in place by TestResolveConfigKimiCodeStripsExtraHeadersOnEndpointOverride. envKey is still production code and is exactly what maps kimi-codeZERO_OAUTH_KIMI_CODE_*; put the old test back alongside the new one.

One thing that needs a decision, not a code change. internal/providercatalog/catalog.go:158 says the bearer is accepted directly "so no client spoofing is involved", while internal/kimiidentity/kimiidentity.go:30-37 says X-Msh-Platform must be kimi_code_cli because the coding/v1 endpoint runs a client whitelist. Those can't both be true, and the second one is the operative claim — we'd be presenting Zero as Moonshot's own CLI to get past an allowlist. That's an owner call and I'd like it made explicitly before this lands, same as we've done for other provider presets. Related: your own comments say the header names and values are reverse-engineered from kimi-cli and "should be confirmed against a real login before this ships" (and X-Msh-Version currently ships the literal string "unknown"). Has anyone run a real Kimi Code login end to end against this branch? If not, that's the last gate.

Smaller things, none blocking:

  • internal/oauth/presets.go:162-172: overriding one endpoint (say only ZERO_OAUTH_KIMI_CODE_TOKEN_URL) drops the identity headers from all requests, including the ones still going to auth.kimi.com, which by your own comment means a 401 the user can't diagnose. Either decide per endpoint, or surface which env var disabled them.
  • internal/oauth/presets.go:174-180: the allowlist takes .moonshot.cn but not bare moonshot.cn and not .moonshot.ai, while the existing moonshot descriptor uses api.moonshot.ai. Add a line saying that's deliberate or tighten the list.
  • No test asserts ExtraHeaders reach the wire. I verified by hand that they do, but given the entire feature hinges on it, an httptest server recording r.Header across RequestDeviceCode / pollDeviceOnce / PostToken / Refresh is worth having.
  • internal/kimiidentity/kimiidentity.go:168: the 2s lock-staleness threshold is never refreshed while the holder works, so a stalled holder can have its lock broken mid-publish and the two racers can walk away with different ids than what's on disk. Narrow, and it needs an already-corrupt file to reach — but consider touching the lock mtime while working.
  • docs/oauth-subscriptions.md documents the X-Msh-* requirement but not that one of those headers is the user's machine hostname. Worth saying out loud since it goes to a third party on every completion.

Happy to re-review as soon as 1-4 are addressed.

Everything in one pass as usual, nothing held back for a second round. The two deleted tests are the ones I'd push back on hardest — a guard removed to make a new descriptor fit is the kind of thing that's invisible in six months, and both look like one-line updates rather than deletions.

@euxaristia

Copy link
Copy Markdown
Contributor Author

Addressed Vasanth review items 1-4:

  1. Onboarding no longer mints kimi-device-id on paint. setupOAuthProviderOptions filters via OAuthProviders() listing clones instead of Get() / RuntimeHeaders. Regression: TestSetupMethodAndOAuthProviderScreensDoNotMintKimiDeviceID.

  2. Wizard does not persist X-Msh-* into config.json. In-memory / discovery profiles still carry runtime headers; stripRuntimeIdentityHeaders runs before UpsertProvider. Resolver re-attaches on canonical endpoints (matches CLI zero auth kimi).

  3. applyCatalogDescriptor guards covered. Prefix strip with empty CustomHeaders + set RuntimeHeaders; stale persisted device id loses to fresh runtime headers on the canonical endpoint.

  4. Restored deleted tests. TestListByTransportPreservesCatalogOrder (with kimi-code in openai-compat order) and TestEnvKey.

Owner decision still open: catalog comment says bearer is accepted with "no client spoofing" while X-Msh-Platform must be kimi_code_cli for the managed-endpoint allowlist. Real end-to-end Kimi Code login on this branch also still wanted as the last gate.

Stop first-run onboarding from minting kimi-device-id via Get on every OAuth option, strip RuntimeHeaders before wizard persist, pin applyCatalogDescriptor guards with real coverage, and restore ListByTransport/EnvKey tests.
@euxaristia
euxaristia force-pushed the feat/kimi-oauth-login branch from 8334dc0 to d41e06f Compare August 1, 2026 16:19

@Vasanthdev2004 Vasanthdev2004 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Approving. Re-checked on d41e06fd and all four are properly closed — I verified rather than read.

Onboarding no longer mints the device id. Filtering via OAuthProviders() listing clones instead of Get()/RuntimeHeaders is the right fix — it removes the call rather than guarding it, so there's no path left that reaches DeviceID() from a render.

The wizard no longer persists identity headers. stripRuntimeIdentityHeaders is genuinely pinned: I disabled it and TestStripRuntimeIdentityHeadersDropsXMshOnly goes red with the persist copy still carrying X-Msh-Device-Id, X-Msh-Device-Name and X-Msh-Platform — while correctly keeping X-User-Agent. That last part matters; a strip that took everything would have passed a weaker test.

Both deleted tests are back. TestListByTransportPreservesCatalogOrder returns with kimi-code added to its expected list, which was the one-line fix, and TestEnvKey sits alongside the new endpoint-override test rather than replacing it. Thanks for restoring them rather than arguing the point — the catalog-ordering guard is exactly the kind of thing nobody misses until it's needed.

internal/oauth, internal/providercatalog and internal/config all green here. The single internal/tui failure is TestAltScreenTranscriptScrollKeepsFooterFixed, which fails on origin/main for me too and passes in CI — a symlink/terminal quirk on my box, unrelated to this.

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.

2 participants