Skip to content

feat(auth): add Kimi Code OAuth login#708

Draft
euxaristia wants to merge 19 commits into
Gitlawb:mainfrom
euxaristia:feat/kimi-oauth-login
Draft

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

Conversation

@euxaristia

@euxaristia euxaristia commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds a first-class Kimi Code OAuth login to Zero, listed directly below ChatGPT in the OAuth options.

Kimi is modeled on the generic device-code preset path (like xAI), not the bespoke ChatGPT/Codex path — because Kimi's returned access token works directly as a Bearer on its managed coding endpoint, so no claim extraction is needed.

Changes

  • 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. Placed immediately after the ChatGPT entry so it appears below ChatGPT in the OAuth provider list.
  • internal/oauth/presets.gokimi device-code preset (auth.kimi.com endpoints, public kimi-code client_id); overridable via ZERO_OAUTH_KIMI_*.
  • internal/cli/auth.go — adds zero auth kimi sugar routing to the generic device-code login; lists kimi below chatgpt in the help text.
  • internal/tui/provider_wizard.go — mentions Kimi in the OAuth method subtitle and includes it in the OAuth provider list.
  • docs/oauth-subscriptions.md — documents the Kimi Code OAuth path.
  • Tests: presets_test.go, catalog_test.go, oauth_test.go updated.

Safety / ToS note

The preset ships the public kimi-code client_id and Moonshot's auth.kimi.com endpoints copied from the open-source kimi-code CLI. It does nothing a user couldn't already do manually (standard RFC 8628 device-code flow, no client spoofing). Off by default; enable with ZERO_OAUTH_ALLOW_PRESETS=1 or explicit env overrides.

Verification

  • go fmt, go vet, golangci-lint (unused,ineffassign,staticcheck), and govulncheck pass (no issues in modified files).
  • go test ./internal/oauth/... ./internal/providercatalog/... ./internal/cli/... pass.

🤖 Generated with Zero

Summary by CodeRabbit

  • New Features
    • Added Kimi Code as a built-in OAuth device-code-only provider, including zero auth kimi support.
    • Updated the OAuth setup/provider wizard so device-only providers start device-code login when pressing Enter, with mouse activation behavior preserved.
  • Documentation
    • Documented Kimi Code in the OAuth materials, including ZERO_OAUTH_ALLOW_PRESETS=1 and ZERO_OAUTH_KIMI_CODE_* overrides.
  • Improvements
    • Enhanced OAuth device-code, token, and refresh requests to include provider-required identity headers.
  • Tests
    • Added/updated tests covering Kimi Code preset resolution, provider catalog/OAuth classification and ordering, and device-only TUI/CLI flows.

@coderabbitai

coderabbitai Bot commented Jul 17, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Adds Kimi Code as a device-code-only OAuth provider across CLI authentication, OAuth presets and headers, provider catalog resolution, setup wizard flows, onboarding, documentation, and tests.

Changes

Kimi OAuth provider

Layer / File(s) Summary
Kimi OAuth preset and identity headers
internal/kimiidentity/*, internal/oauth/presets.go, internal/cli/auth.go, internal/oauth/presets_test.go
Adds the device-code preset, persistent X-Msh-* identity headers, zero auth kimi routing, help text, and validation tests.
Provider-specific OAuth request headers
internal/oauth/oauth.go, internal/oauth/device.go, internal/oauth/flow.go, internal/oauth/providers.go, internal/oauth/flow_test.go
Carries configured extra headers through device authorization, polling, token exchange, and refresh requests.
Kimi provider catalog registration
internal/providercatalog/catalog.go, internal/providercatalog/*_test.go
Registers kimi-code as a device-only OpenAI-compatible OAuth provider, lazily supplies runtime headers, and updates catalog, alias, ordering, and classification tests.
Setup wizard and OAuth documentation
internal/tui/*.go, internal/tui/*test.go, docs/oauth-subscriptions.md
Starts device login with Enter for device-only providers, updates wizard and onboarding behavior, tests keyboard and mouse paths, and documents Kimi configuration.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant SetupWizard
  participant OAuthRegistry
  participant KimiAuth
  User->>SetupWizard: select Kimi Code and press Enter
  SetupWizard->>OAuthRegistry: resolve kimi-code device preset
  OAuthRegistry->>KimiAuth: request device code and poll token
  KimiAuth-->>OAuthRegistry: return bearer token
  OAuthRegistry-->>SetupWizard: authentication result
  SetupWizard-->>User: complete device-code login
Loading

Possibly related PRs

  • Gitlawb/zero#141: Adds provider-catalog infrastructure extended here with the Kimi descriptor.
  • Gitlawb/zero#210: Extends the OAuth device-code and token-exchange infrastructure with provider-specific headers.
  • Gitlawb/zero#217: Establishes OAuth preset and TUI device-code wiring extended by the Kimi provider.
  • Gitlawb/zero#738: Also modifies PostToken behavior in the shared OAuth flow.

Suggested reviewers: gnanam1990, pierrunoyt, vasanthdev2004

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 55.77% 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 matches 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: 2

🤖 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/cli/auth.go`:
- Around line 81-87: Update the "kimi" case in the auth command dispatch to pass
the subcommand name together with args[1:] into runAuthLogin, preserving
user-provided flags such as --scope and --help for generic parsing.

In `@internal/tui/provider_wizard.go`:
- Line 329: Update the Kimi entry near the provider-wizard subtitle to remove it
from the browser-login description or explicitly identify it as
device-code-only; also update docs/oauth-subscriptions.md lines 40 and 54-61 to
exclude Kimi from the one-click browser-login list and describe its separate
device-code authentication flow.
🪄 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

Run ID: c4ed4a70-9661-4fca-8f55-60ea4bc0fa13

📥 Commits

Reviewing files that changed from the base of the PR and between 9acb411 and 94cafb7.

📒 Files selected for processing (8)
  • docs/oauth-subscriptions.md
  • internal/cli/auth.go
  • internal/oauth/presets.go
  • internal/oauth/presets_test.go
  • internal/providercatalog/catalog.go
  • internal/providercatalog/catalog_test.go
  • internal/providercatalog/oauth_test.go
  • internal/tui/provider_wizard.go

Comment thread internal/cli/auth.go Outdated
Comment thread internal/tui/provider_wizard.go Outdated
@euxaristia
euxaristia marked this pull request as draft July 17, 2026 04:26
@euxaristia
euxaristia force-pushed the feat/kimi-oauth-login branch from 94cafb7 to 56a63fe Compare July 17, 2026 05:32

@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.

Blocking: inserting "kimi" into the TransportOpenAICompat slice in catalog_test.go dropped the trailing comma after the closing brace, so the providercatalog test package won't compile — go vet and go test both fail with missing ',' before newline in composite literal at catalog_test.go:326. Re-adding that comma (after custom-openai-compatible"}) fixes it. Two minor follow-ups: the openrouter line in the zero auth help text picked up an extra leading space (3 vs. 2), and the baked-in Kimi client_id and managed endpoint are a contributor-chosen trust anchor I'd want kevin to sign off on explicitly — same opt-in preset pattern as xAI, but worth a conscious yes.

@jatmn jatmn 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.

I found issues that need to be addressed before this is ready.

Findings

  • [P1] Restore the Moonshot kimi alias before adding this canonical ID
    internal/providercatalog/catalog.go:147
    moonshot already exposes kimi as an alias, but Get returns an exact descriptor ID before it reaches later aliases. Existing profiles with catalogID: "kimi" therefore resolve to this new OAuth-only descriptor instead of Moonshot, changing their endpoint, default model, and MOONSHOT_API_KEY authentication behavior. Use a non-conflicting ID or add an explicit migration/compatibility path for the old alias.

  • [P1] Implement Kimi's device-identity protocol before exposing its OAuth preset
    internal/oauth/presets.go:110
    The generic device flow only sends form fields plus Content-Type/Accept, while Kimi Code's own OAuth client sends its persistent X-Msh-* device identity headers on device authorization, polling, and refresh; the managed API client also applies them to model calls. Zero has no place to provide those headers for either OAuth or the new catalog descriptor, so this does not implement the provider's OAuth/runtime contract and the advertised login can be rejected or become unrefreshable. Add the supported provider-specific identity/header handling end-to-end, or do not offer this OAuth integration.

  • [P1] Route device-only Kimi through the UI's device-code flow on desktops
    internal/providercatalog/catalog.go:149
    Kimi is marked device-flow-only, but a normal desktop Enter in /provider or first-run onboarding takes the generic token-login branch. Manager.Login correctly chooses device flow from the preset, then writes the verification URL and code to a nil Out writer (which becomes io.Discard); the UI only shows a spinner until it times out. Always use the existing two-phase device-code UI for device-only providers, and cover the desktop path.

  • [P1] Fix the invalid Kimi Code model default
    internal/providercatalog/catalog.go:147
    The managed Kimi Code endpoint documents kimi-for-coding and kimi-for-coding-highspeed, not kimi-k2.7-code-highspeed. A profile created by zero auth kimi therefore never selects the advertised HighSpeed tier (Kimi silently falls back to standard for mistyped IDs). Use a supported model ID and assert the descriptor default in tests.

  • [P2] Restore the missing composite-literal comma so this package parses
    internal/providercatalog/catalog_test.go:326
    The updated TransportOpenAICompat slice is the final value in a multiline map literal but has no trailing comma. Go rejects the package with missing ',' before newline in composite literal, which explains the three failed Smoke jobs and prevents the claimed Go test suite from running.

  • [P2] Forward flags from the zero auth kimi shortcut
    internal/cli/auth.go:87
    This calls the generic login with only []string{"kimi"} and discards every user-supplied argument. Consequently zero auth kimi --help begins a real device authorization, --scope is silently omitted, and typos are accepted instead of going through the generic parser. Pass args[1:] through (or reject extras explicitly) and add coverage for help, scopes, and invalid flags.

  • [P3] Stop advertising the device-only path as browser login
    internal/tui/provider_wizard.go:379
    Kimi has no loopback/authorization endpoint, yet the newly updated chooser and the documentation include it in “One-click browser login.” This leaves users expecting a browser flow that cannot occur; describe it as device-code-only consistently.

coderabbitai[bot]
coderabbitai Bot previously approved these changes Jul 18, 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 quick turnaround — the comma fix landed and the openrouter line is realigned, so my compile blocker is cleared (Smoke is green on all three platforms now). Keeping this at changes requested for two reasons though. First, jatmn's alias-collision point is real and I verified it: moonshot already carries kimi as an alias, and since Get() walks the catalog in order, the new kimi entry now wins — anyone with catalogID "kimi" today gets silently repointed from api.moonshot.ai/v1 + MOONSHOT_API_KEY to the OAuth-only managed endpoint. That needs a non-conflicting ID or an explicit migration story. Second, I still want kevin's explicit yes on shipping the kimi-code client_id and auth.kimi.com endpoints as a baked-in trust anchor — I don't see that on the thread yet. While you're in there, zero auth kimi should forward args[1:] (right now zero auth kimi --help kicks off a real device flow), and the wizard/docs shouldn't list Kimi under one-click browser login when it's device-code only — jatmn's review covers the rest.

@jatmn jatmn 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.

I found issues that need to be addressed before this is ready.

Findings

  • [P1] Propagate Kimi's required identity headers to model requests
    internal/providercatalog/catalog.go:166
    The new descriptor has no CustomHeaders, while the new X-Msh-* values live only in oauth.Config.ExtraHeaders, whose consumers are the device and token request builders. After zero auth kimi saves its bearer, the normal OpenAI-compatible runtime therefore sends /coding/v1/chat/completions with only the bearer and no Kimi identity headers. Kimi's own client exposes these headers for its managed-service client as well, so a successful login still leaves the advertised coding endpoint rejecting every completion. Carry the identity contract into the catalog/runtime request path (including its stable device identity) and cover the outbound request.

  • [P1] Route device-only providers through the device-code onboarding UI
    internal/tui/onboarding.go:781
    First-run onboarding only uses startSetupDeviceLogin when the environment prefers device flow. On a desktop, selecting Kimi and pressing Enter takes setupOAuthCmd instead; its generic manager correctly chooses Kimi's device flow, but its nil output discards the verification URL and user code while onboarding displays a browser-login spinner. The user cannot authorize and the login times out. Mirror the OAuthDeviceOnly Enter handling already added to /provider and add the desktop onboarding regression test.

  • [P2] Correct the documented Kimi OAuth override names
    docs/oauth-subscriptions.md:115
    The documented ZERO_OAUTH_KIMI_{ISSUER,DEVICE,TOKEN}_URL variables are never read: the shortcut resolves provider kimi-code, and envKey derives ZERO_OAUTH_KIMI_CODE_*. Users following the documentation cannot override the OAuth endpoints. Document the KIMI_CODE names (or deliberately support the aliases).

@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

🧹 Nitpick comments (1)
internal/providercatalog/catalog.go (1)

449-454: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low value

Avoid double-allocation when lazily populating CustomHeaders.

When descriptor.CustomHeaders is nil for kimi-code, kimiidentity.Headers() allocates and returns a new map. The immediately following if descriptor.CustomHeaders != nil block then makes a redundant copy of this freshly allocated map.

♻️ Proposed refactor
-	if descriptor.ID == "kimi-code" && descriptor.CustomHeaders == nil {
-		descriptor.CustomHeaders = kimiidentity.Headers()
-	}
-	if descriptor.CustomHeaders != nil {
-		descriptor.CustomHeaders = copyStringMap(descriptor.CustomHeaders)
-	}
+	if descriptor.CustomHeaders != nil {
+		descriptor.CustomHeaders = copyStringMap(descriptor.CustomHeaders)
+	} else if descriptor.ID == "kimi-code" {
+		descriptor.CustomHeaders = kimiidentity.Headers()
+	}
🤖 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 449 - 454, Update the
CustomHeaders handling near the kimi-code descriptor initialization to avoid
copying the map freshly returned by kimiidentity.Headers(). Preserve copying for
pre-existing non-nil CustomHeaders while ensuring lazily populated headers are
assigned without the redundant copy.
🤖 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.go`:
- Around line 61-65: Update the persistent ID write path in the path-handling
block to create the file with os.OpenFile using exclusive creation (os.O_EXCL),
avoiding blind os.WriteFile overwrites. If creation fails because another
process created the file, read and adopt the existing stored ID so concurrent
processes use the same identity; preserve the current directory and permission
behavior.

---

Nitpick comments:
In `@internal/providercatalog/catalog.go`:
- Around line 449-454: Update the CustomHeaders handling near the kimi-code
descriptor initialization to avoid copying the map freshly returned by
kimiidentity.Headers(). Preserve copying for pre-existing non-nil CustomHeaders
while ensuring lazily populated headers are assigned without the redundant copy.
🪄 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

Run ID: 5ec83a3f-9e5a-47f0-ac2c-b0f0ac1c8ecf

📥 Commits

Reviewing files that changed from the base of the PR and between a238259 and 9bdec37.

📒 Files selected for processing (6)
  • docs/oauth-subscriptions.md
  • internal/kimiidentity/kimiidentity.go
  • internal/oauth/presets.go
  • internal/providercatalog/catalog.go
  • internal/tui/onboarding.go
  • internal/tui/onboarding_test.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • docs/oauth-subscriptions.md

Comment thread internal/kimiidentity/kimiidentity.go Outdated
@euxaristia

Copy link
Copy Markdown
Contributor Author

Pushed 9bdec37 for the latest round: (1) the X-Msh-* identity now reaches model requests — the header set moved to a shared internal/kimiidentity package used by both the OAuth flows and the kimi-code descriptor's CustomHeaders (populated lazily at catalog access so importing the catalog does no filesystem IO), which the resolver already merges into the profile for canonical-endpoint requests. 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. (2) First-run onboarding now routes a desktop Enter on an OAuthDeviceOnly provider through the device-code UI, mirroring the /provider fix, with a desktop-environment regression test. (3) The docs now name the ZERO_OAUTH_KIMI_CODE_* override variables that envKey actually derives. The baked-in client_id/endpoint trust anchor still needs kevin's explicit sign-off, per Vasanth.

@jatmn jatmn 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.

I found issues that need to be addressed before this is ready.

Findings

  • [P2] Route mouse activation through the device-code UI
    internal/tui/provider_wizard_discovery.go:47
    The new OAuthDeviceOnly handling exists only in the keyboard handler. A desktop user who double-clicks Kimi Code reaches advanceProviderWizard, which checks only oauthPreferDeviceFlow() and starts the generic OAuth command. Kimi's preset then selects its device flow, but that path discards the verification URL and user code, leaving the browser-login spinner to time out. Include OAuthDeviceOnly in this branch (or centralize the choice) and cover mouse activation.

  • [P2] Make Kimi device-ID initialization safe across processes
    internal/kimiidentity/kimiidentity.go:51
    Two first-run Zero processes can both miss the ID file, mint different IDs, and blindly overwrite it at line 63. A login completed under the losing process's in-memory ID is then refreshed or used for completions with the persisted winning ID, despite this integration requiring one identity across those calls. Create the file exclusively (or lock it), and on an already-created result read and adopt the winner.

Maintainer decision

  • Vasanthdev2004's request for Kevin's explicit approval of the baked-in Kimi client identity and auth.kimi.com trust anchor remains unanswered on the current head. Please record that decision before shipping this opt-in preset.

coderabbitai[bot]
coderabbitai Bot previously approved these changes Jul 19, 2026
@euxaristia

Copy link
Copy Markdown
Contributor Author

Pushed 4305942.

Fixed:

  • Concurrent first-run processes could both miss the device-identity file and overwrite each other's ID. Switched to an exclusive create, and on conflict, read back and adopt whichever process won.
  • Removed a redundant header map copy in provider catalog cloning.
  • The mouse double-click activation path only checked oauthPreferDeviceFlow(), not OAuthDeviceOnly, so double-clicking Kimi Code could try a browser OAuth flow that doesn't exist for it. Added the same check the keyboard path already has.

Still open, not something a code fix resolves:

  • The note asking for an explicit sign-off from Kevin on baking in the Kimi client_id and auth.kimi.com as a trust anchor is still unanswered on this thread. That needs a decision recorded here before merge.

@Vasanthdev2004

This comment was marked as low quality.

@Vasanthdev2004

Copy link
Copy Markdown
Collaborator

Came back to this after digging into the reverse-engineered values, since that was the one thing I was holding on. On the baked-in client_id and the auth.kimi.com endpoints: I'm good with those as a maintainer, no separate sign-off needed.

And they check out. I cross-referenced every login-critical value against the upstream kimi-cli (MoonshotAI/kimi-cli, src/kimi_cli/auth/oauth.py):

  • client_id 17e5f671-d194-4dfb-9706-5516cb48c098 is an exact match to KIMI_CODE_CLIENT_ID.
  • The device_authorization and token endpoints match kimi-cli's URL builders on the default auth.kimi.com host.
  • api.kimi.com/coding/v1 matches the documented coding provider default.
  • All six X-Msh-* header keys match _common_headers verbatim, and the #1169 attribution is genuine (the illegal # in X-Msh-Os-Version on Linux, and asciiHeaderValue is exactly upstream's fix). Well sourced.

Implementation side is clean too: the device flow is RFC 8628 correct, the vendor headers are wired into device-auth, poll, exchange, refresh, and the completions calls, DeviceID is persistent and race-safe, and the alias logic is safe (resolving "kimi" still lands on moonshot, no repoint).

One thing worth a conscious call before merge: X-Msh-Platform is set to zero-cli, where upstream hardcodes kimi_cli. That's the honest value since Zero isn't kimi-cli, and it does not affect the OAuth login at all. But Kimi's coding/v1 endpoint enforces a client whitelist ("only available for Coding Agents such as Kimi CLI, Claude Code, Roo Code, Kilo Code..."), so there's a real chance login succeeds and then the model calls get rejected for a non-whitelisted platform. I could not tell from the source whether that whitelist keys on the platform header or on the client_id (which does match kimi-cli); only a live login settles it. Worth a quick real-world check, or a code note that this value may need to be whitelisted.

Minor: All() calls kimiidentity.Headers() unconditionally, so just listing providers mints a kimi-device-id file and UUID for users who never touch Kimi. A lazy init would avoid that.

The code is solid and accurately sourced. Once a real Kimi login is confirmed to actually complete a model call with the zero-cli platform value (or it's adjusted), I'm happy to approve.

@euxaristia

Copy link
Copy Markdown
Contributor Author

Pushed 2b5c45a on top of 4305942.

Code blockers from the earlier reviews are closed on this head:

  1. Alias collision: descriptor is kimi-code; Get("kimi") still resolves to moonshot (pinned by TestKimiAliasStillResolvesToMoonshot).
  2. Mouse activation: advanceProviderWizard checks OAuthDeviceOnly (same as keyboard); covered by TestProviderWizardMouseAdvanceStartsDeviceFlowForDeviceOnlyProvider.
  3. Device-ID multi-process safety: exclusive create + adopt winner (from 4305942); concurrent create covered in internal/kimiidentity tests.
  4. zero auth kimi forwards args[1:] (from a238259).
  5. Wizard/docs present Kimi as device-code only.
  6. Trust anchor: Vasanth already signed off on client_id/endpoints as maintainer on this thread.

This commit also addresses the latest nits:

  • All() / OAuthProviders() / ListByTransport no longer mint kimi-device-id; Get/Require still attach X-Msh-* for resolve-time completions.
  • Documented the X-Msh-Platform: zero-cli vs upstream kimi_cli whitelist risk in code.

Still not a pure code fix (needs a live account):

  • Confirm a real Kimi login can complete a model call with X-Msh-Platform: zero-cli. If coding/v1 rejects non-whitelisted platforms, that value may need adjustment.

Leaving this as draft until that live check (or a deliberate platform decision) is recorded.

@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

🧹 Nitpick comments (1)
internal/providercatalog/catalog.go (1)

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

Vendor-specific ID check baked into generic catalog cloning.

cloneDescriptor hardcodes descriptor.ID == "kimi-code" to decide when to lazily mint runtime headers. This works today, but couples the shared, generic clone helper to one specific provider. If another provider ever needs the same lazy-header pattern, this function grows another special case.

Consider generalizing via a field on Descriptor (e.g. RuntimeHeaders func() map[string]string, set only for the kimi-code entry) so cloneDescriptor stays provider-agnostic.

♻️ Sketch of a provider-agnostic alternative
 type Descriptor struct {
 	...
 	OAuthDeviceOnly bool
+	// RuntimeHeaders, if set, lazily produces headers to attach to
+	// CustomHeaders only when withRuntimeHeaders is true (e.g. Kimi's
+	// vendor-identity headers).
+	RuntimeHeaders func() map[string]string
 }

 func cloneDescriptor(descriptor Descriptor, withRuntimeHeaders bool) Descriptor {
 	...
 	if descriptor.CustomHeaders != nil {
 		descriptor.CustomHeaders = copyStringMap(descriptor.CustomHeaders)
-	} else if withRuntimeHeaders && descriptor.ID == "kimi-code" {
-		descriptor.CustomHeaders = kimiidentity.Headers()
+	} else if withRuntimeHeaders && descriptor.RuntimeHeaders != nil {
+		descriptor.CustomHeaders = descriptor.RuntimeHeaders()
 	}
 	return descriptor
 }
🤖 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 440 - 456, Make
cloneDescriptor provider-agnostic by adding a Descriptor-level runtime-header
provider, such as RuntimeHeaders, and invoke it when withRuntimeHeaders is true
and CustomHeaders is nil. Configure only the kimi-code descriptor to supply
kimiidentity.Headers, preserving lazy header creation and existing explicit
CustomHeaders behavior without checking descriptor.ID inside cloneDescriptor.
🤖 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`:
- Around line 32-97: Factor the production load-or-create logic from DeviceID()
into a path-parameterized internal helper such as loadOrCreateDeviceIDAt(path
string), preserving exclusive creation and winner read-back behavior. Update
DeviceID() to resolve the user config path and delegate to this helper, then
replace the inline OpenFile/read-back algorithm in
TestLoadOrCreateDeviceIDExclusiveCreate with direct concurrent calls to the
helper.

---

Nitpick comments:
In `@internal/providercatalog/catalog.go`:
- Around line 440-456: Make cloneDescriptor provider-agnostic by adding a
Descriptor-level runtime-header provider, such as RuntimeHeaders, and invoke it
when withRuntimeHeaders is true and CustomHeaders is nil. Configure only the
kimi-code descriptor to supply kimiidentity.Headers, preserving lazy header
creation and existing explicit CustomHeaders behavior without checking
descriptor.ID inside cloneDescriptor.
🪄 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

Run ID: 407b3e6d-2d9c-4065-8406-968a98ecfee6

📥 Commits

Reviewing files that changed from the base of the PR and between 9bdec37 and 2b5c45a.

📒 Files selected for processing (7)
  • internal/kimiidentity/kimiidentity.go
  • internal/kimiidentity/kimiidentity_test.go
  • internal/oauth/presets_test.go
  • internal/providercatalog/catalog.go
  • internal/providercatalog/catalog_test.go
  • internal/tui/provider_wizard_discovery.go
  • internal/tui/provider_wizard_oauth_test.go
🚧 Files skipped from review as they are similar to previous changes (3)
  • internal/tui/provider_wizard_oauth_test.go
  • internal/oauth/presets_test.go
  • internal/kimiidentity/kimiidentity.go

Comment thread internal/kimiidentity/kimiidentity_test.go

@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.

🧹 Nitpick comments (1)
internal/providercatalog/catalog.go (1)

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

Merge RuntimeHeaders instead of making it mutually exclusive with CustomHeaders.

The current else if logic mutually excludes CustomHeaders and RuntimeHeaders. If a future provider defines both static CustomHeaders and dynamic RuntimeHeaders, the dynamic ones will be silently ignored. Merging them avoids this trap and future-proofs the resolution logic.

🛠️ Proposed refactor
-	if descriptor.CustomHeaders != nil {
-		descriptor.CustomHeaders = copyStringMap(descriptor.CustomHeaders)
-	} else if withRuntimeHeaders && descriptor.RuntimeHeaders != nil {
-		descriptor.CustomHeaders = descriptor.RuntimeHeaders()
+	if descriptor.CustomHeaders != nil {
+		descriptor.CustomHeaders = copyStringMap(descriptor.CustomHeaders)
+	}
+	if withRuntimeHeaders && descriptor.RuntimeHeaders != nil {
+		rh := descriptor.RuntimeHeaders()
+		if descriptor.CustomHeaders == nil {
+			descriptor.CustomHeaders = rh
+		} else {
+			for k, v := range rh {
+				descriptor.CustomHeaders[k] = v
+			}
+		}
 	}
🤖 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 463 - 467, Update the
descriptor header resolution logic to process RuntimeHeaders independently of
CustomHeaders rather than using an else-if. Preserve and copy existing
CustomHeaders, then merge the headers returned by descriptor.RuntimeHeaders()
when withRuntimeHeaders is enabled, ensuring dynamic values are included when
both sources are defined.
🤖 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.

Nitpick comments:
In `@internal/providercatalog/catalog.go`:
- Around line 463-467: Update the descriptor header resolution logic to process
RuntimeHeaders independently of CustomHeaders rather than using an else-if.
Preserve and copy existing CustomHeaders, then merge the headers returned by
descriptor.RuntimeHeaders() when withRuntimeHeaders is enabled, ensuring dynamic
values are included when both sources are defined.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 3c7fa937-fc56-4701-897e-83ddde945906

📥 Commits

Reviewing files that changed from the base of the PR and between 2b5c45a and 15eac90.

📒 Files selected for processing (3)
  • internal/kimiidentity/kimiidentity.go
  • internal/kimiidentity/kimiidentity_test.go
  • internal/providercatalog/catalog.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • internal/kimiidentity/kimiidentity.go

@euxaristia
euxaristia force-pushed the feat/kimi-oauth-login branch from 15eac90 to ddb1625 Compare July 20, 2026 08:28

@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: 2

🤖 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 `@docs/oauth-subscriptions.md`:
- Around line 108-111: Update the Kimi header scope description near
ZERO_OAUTH_KIMI_CODE_* to cover code exchange and managed runtime requests in
addition to device authorization, polling, and refresh. State that the
vendor-identity X-Msh-* headers are required across all applicable OAuth and API
calls, matching the behavior documented by the OAuth and provider integration
symbols.
- Line 40: Update the “Sign in with OAuth” chooser summary in the documentation
to mention device-code login support for xAI and Hugging Face alongside Kimi
Code, keeping it consistent with the detailed guidance later in the document.
🪄 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

Run ID: ad503a0f-f642-41f4-80d7-5d1639ed8e9b

📥 Commits

Reviewing files that changed from the base of the PR and between 15eac90 and ddb1625.

📒 Files selected for processing (19)
  • docs/oauth-subscriptions.md
  • internal/cli/auth.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/oauth.go
  • internal/oauth/presets.go
  • internal/oauth/presets_test.go
  • internal/oauth/providers.go
  • internal/providercatalog/catalog.go
  • internal/providercatalog/catalog_test.go
  • internal/providercatalog/oauth_test.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 skipped from review as they are similar to previous changes (17)
  • internal/oauth/device.go
  • internal/oauth/flow_test.go
  • internal/oauth/oauth.go
  • internal/providercatalog/oauth_test.go
  • internal/cli/auth.go
  • internal/kimiidentity/kimiidentity_test.go
  • internal/oauth/presets.go
  • internal/tui/onboarding_test.go
  • internal/oauth/presets_test.go
  • internal/oauth/flow.go
  • internal/tui/provider_wizard_oauth_test.go
  • internal/tui/onboarding.go
  • internal/tui/provider_wizard_discovery.go
  • internal/kimiidentity/kimiidentity.go
  • internal/providercatalog/catalog.go
  • internal/providercatalog/catalog_test.go
  • internal/tui/provider_wizard.go

Comment thread docs/oauth-subscriptions.md Outdated
Comment thread docs/oauth-subscriptions.md Outdated
@euxaristia

Copy link
Copy Markdown
Contributor Author

Rebased onto main and fixed a CI failure.

No textual conflict, but a semantic one: this PR adds an extraHeaders param to PostToken, while main separately added TestPostTokenRefusesRedirect using PostToken's old signature. The merge compiled the wrong test call, which go vet caught. Updated that call site to pass the new arg.

Verified after rebase: go build ./..., go vet ./..., and go test ./... all pass. Smoke is now green on macOS, Ubuntu, and Windows.

@euxaristia
euxaristia force-pushed the feat/kimi-oauth-login branch from ddb1625 to 16d1059 Compare July 20, 2026 11:06
@euxaristia

Copy link
Copy Markdown
Contributor Author

Pushed 16d1059 (16d105994f989bca4f497422e3a5307ee690132d) on top of a rebase onto current main.

Fixes in this push:

  • Kimi device-id multi-process race: exclusive create already present; added retry-read + fsync so a concurrent first-run process that observes an empty file mid-write adopts the winner UUID instead of minting a divergent id. Covered by TestLoadOrCreateDeviceIDAdoptsWinnerAfterEmptyCreate.
  • Docs: OAuth chooser summary now lists device code for xAI, Kimi Code, and Hugging Face; Kimi X-Msh-* header scope documents all required OAuth and managed API calls.

Already on this branch from earlier review rounds (re-verified on this head):

  • Mouse double-click activation routes device-only providers through device-code login (advanceProviderWizard checks OAuthDeviceOnly; TestProviderWizardMouseAdvanceStartsDeviceFlowForDeviceOnlyProvider).
  • Keyboard Enter path for device-only providers unchanged and tested.

Still draft. Maintainer approval of the baked-in Kimi client identity (client_id / auth.kimi.com trust anchor) remains outstanding for an explicit Kevin sign-off before this should be marked ready for review. Leaving draft.

Verification: gofmt check, go vet on touched packages, go test ./internal/kimiidentity ./internal/oauth ./internal/providercatalog ./internal/cli, focused tui wizard device-flow tests.

coderabbitai[bot]
coderabbitai Bot previously approved these changes Jul 20, 2026
@euxaristia

Copy link
Copy Markdown
Contributor Author

Addressed in a4c5329 (draft status unchanged; still pending live-login verification).

[P1] Send the Kimi platform identity its service accepts

  • X-Msh-Platform is now kimi_code_cli (Moonshot Kimi Code CLI contract), with tests updated.

[P1] Preserve Kimi runtime headers in the /provider OAuth path

  • providerWizardProfile re-resolves the descriptor through providercatalog.Get so RuntimeHeaders-backed X-Msh-* values (including device id) are applied for the post-login /models call and activated profile, without minting an ID on mere listing.
  • Regression: TestProviderWizardProfileAppliesKimiRuntimeHeaders.

Device-poll cancel on Esc

  • Device-code polls now use a cancelable parent context from setup/wizard; Esc actually stops the poll so a later browser authorization cannot silently persist a credential the user backed out of.
  • Regression: TestProviderWizardEscCancelsDeviceLoginPoll.

Test hygiene

  • Kimi identity tests redirect the user config root so they cannot create/touch the real device-id file.

Verified: go test ./internal/kimiidentity/ and OAuth/wizard-focused ./internal/tui/ tests pass. Left draft for live-login confirmation.

euxaristia added a commit to euxaristia/zero that referenced this pull request Jul 22, 2026
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
@euxaristia

Copy link
Copy Markdown
Contributor Author

Additional device-id hardening on top of the prior round (e609e40).

[P2] Abandoned empty/invalid device-id file

  • After the adopt-retry window, if the exclusive-create path still has no valid UUID (winner died mid-publish), remove the abandoned file once and exclusive-create again so callers converge on a persisted identity instead of permanently diverging in memory.
  • Regression: TestLoadOrCreateDeviceIDRepairsAbandonedEmptyFile

Still draft pending live-login verification.

@jatmn jatmn 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.

I found issues that need to be addressed before this is ready.

Findings

  • [P1] Restore atomic cron job-ID reservation
    internal/cron/store.go:102
    allocID now checks a timestamp directory with os.Stat and returns before anything reserves it. Two stores adding a job in the same second can both choose that ID, then race through writeJob on the same metadata.json.tmp and final metadata path; both callers can report success while one scheduled job is overwritten (and a concurrent remove can again be resurrected). This rolls back the base's exclusive Mkdir plus held per-ID lock, and deletes its regression tests. Restore an atomic reservation held through initial persistence and retain the concurrent-add/remove coverage.

  • [P1] Strip Kimi identity headers after retargeting a saved profile
    internal/config/resolver.go:1092
    The wizard persists Kimi's X-Msh-* values, including the stable device ID and host/device metadata, when the profile uses api.kimi.com. If the user later changes that profile's baseURL to a proxy, staging server, or another host, this non-canonical branch only strips AIMLAPI headers, so the Kimi identity headers are sent to the arbitrary override endpoint. Apply the same descriptor-owned-header cleanup for Kimi (while preserving user headers), and cover a canonical Kimi profile retargeted to a custom URL.

  • [P2] Make abandoned-device-ID repair mutually exclusive
    internal/kimiidentity/kimiidentity.go:102
    When an invalid/empty ID file is present, every racing process waits, then unconditionally removes the path. A process can create and publish its replacement between another process's retry read and os.Remove; the latter then unlinks the valid winner and creates a second ID. The first process may return its now-unpersisted ID, so OAuth and later runtime requests use different device identities. Coordinate repair with an exclusive lock/recheck (or an atomic replacement protocol) and add a concurrent abandoned-file test.

  • [P2] Cancel device polling on every TUI quit path
    internal/tui/onboarding.go:161
    The new cancel function is called for Esc, but Ctrl-C returns tea.Quit without calling it. The normal provider-wizard quit path has the same omission in model.quit, which only cancels the AIMLAPI flow. Because the TUI is launched with context.Background(), approving the device code after quitting can still let the background poll persist a credential the user deliberately abandoned. Cancel any active setup/provider-wizard device login before every quit and add coverage for Ctrl-C.

  • [P2] Reject stale onboarding device-code attempts for the same provider
    internal/tui/onboarding.go:568
    Setup device-code messages carry only providerID, unlike the provider wizard's attempt ID. Start Kimi login, press Esc before phase one returns, and start Kimi again: the late result from the first request matches the same provider while the second request is pending, replaces its displayed code/cancel function, and starts polling the canceled authorization. Its eventual success is accepted and persisted. Add an attempt generation to phase-one and poll-result messages, increment it when abandoning/restarting, and require it when applying results.

  • [P3] Correct the Kimi preset instruction in the OAuth guide
    docs/oauth-subscriptions.md:107
    The guide says Kimi is opt-in via ZERO_OAUTH_ALLOW_PRESETS=1, but both zero auth kimi and zero auth login kimi-code use newAuthManager, which sets AllowPresets: true unconditionally. Users are told to set an unnecessary variable for the documented commands. Align this text with the actual CLI behavior.

euxaristia and others added 17 commits July 22, 2026 12:24
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.
@euxaristia
euxaristia force-pushed the feat/kimi-oauth-login branch from e609e40 to 86c1eac Compare July 22, 2026 16:45
@euxaristia

Copy link
Copy Markdown
Contributor Author

Addressed all six findings from the latest review.

[P1] Cron job-ID reservation - this was a rebase issue, not new code from this PR: internal/cron/store.go isn't touched by this diff at all. Rebased onto current upstream/main, which already has the atomic reservation fix from #686. Confirmed internal/cron/store.go now matches upstream/main exactly.

[P1] Strip Kimi identity headers after retargeting a saved profile - applyCatalogDescriptor in internal/config/resolver.go only stripped catalog-owned headers on a non-canonical baseURL for aimlapi specifically. Generalized that branch to any descriptor with catalog-owned headers (checked by len(descriptor.CustomHeaders) > 0 instead of an ID string match), so a Kimi profile retargeted off api.kimi.com now has its X-Msh-* headers stripped the same way, while headers the user set themselves are preserved. Added TestApplyCatalogDescriptorStripsKimiIdentityFromRetargetedProfile.

[P2] Make abandoned-device-ID repair mutually exclusive - internal/kimiidentity/kimiidentity.go used to have every racing process retry-read an invalid/empty ID file, then unconditionally remove and recreate it once, which meant one process could unlink another's just-published winner between that process's failed read and its own remove. Repair is now serialized behind an exclusive lock file: only the lock holder removes and recreates the file, everyone else waits and adopts whatever it publishes. Added TestLoadOrCreateDeviceIDConcurrentAbandonedFileRepairConverges, which repairs the same abandoned file from 16 goroutines and checks they all converge on one persisted id.

[P2] Cancel device polling on every TUI quit path - Ctrl+C during a first-run device-code poll, and model.quit() (the provider wizard's quit path, 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 quit out of could still complete and get persisted in the background. Both paths now cancel any active device login first. Added TestSetupCtrlCCancelsDeviceLoginPoll and TestModelQuitCancelsProviderWizardDeviceLoginPoll.

[P2] Reject stale onboarding device-code attempts for the same provider - first-run setup's device-code messages only carried 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 already-abandoned authorization. Added an attempt generation to setupState (mirroring the provider wizard's existing oauthAttemptID) that bumps on every restart and is required for a phase-one or poll result to be applied. Added TestSetupStaleDeviceCodeAttemptRejected.

[P3] Correct the Kimi preset instruction in the OAuth guide - docs/oauth-subscriptions.md told users to set ZERO_OAUTH_ALLOW_PRESETS=1 for Kimi, but zero auth kimi and zero auth login kimi-code both go through newAuthManager, which sets AllowPresets: true unconditionally. Fixed the text to say the documented commands work without that variable.

Verification: go build ./..., go vet ./..., gofmt -l . (clean), and go test ./... all pass except one pre-existing, environment-dependent failure in TestAltScreenTranscriptScrollKeepsFooterFixed that reproduces identically on the pre-rebase base commit and is unrelated to this PR (it's sensitive to the checkout's working-directory path length). Ran the kimiidentity and config suites with -race as well.

Pushed as a rebase onto current upstream/main plus four new commits (one per remaining finding, grouped logically), force-pushed to this branch.

@euxaristia

Copy link
Copy Markdown
Contributor Author

Independently re-verified the current head (86c1eac, unchanged) against jatmn's and Vasanthdev2004's latest rounds.

jatmn's cron/store.go P1 — this is a stale-branch artifact, not new code in this PR: internal/cron/store.go isn't touched by this diff. Confirmed d9b882e (#686's atomic-reservation fix) is already an ancestor of this branch and internal/cron/store.go is byte-identical to upstream/main's exclusive-Mkdir+lock implementation, with TestStoreAddReservesUniqueIDsAcrossConcurrentStores / TestAddDoesNotLeakReservationToConcurrentRemove intact. git rebase origin/main on this branch is a no-op.

Vasanthdev2004's three code items — all confirmed fixed on this head:

  • Alias collision: the Kimi descriptor's catalog ID is kimi-code, not kimi; moonshot still owns the kimi alias and Get("kimi") resolves to moonshot (pinned by TestKimiAliasStillResolvesToMoonshot). Also swept the rest of the catalog for other collisions — none touch this PR's changes.
  • zero auth kimi --help: the kimi case in internal/cli/auth.go forwards args[1:] into the real login parser instead of unconditionally starting a device flow.
  • Docs/wizard mislabeling: docs/oauth-subscriptions.md and internal/tui/provider_wizard.go both list Kimi Code only under device code, not "one-click browser login."

Verification: gofmt -l . clean, go vet ./... clean, go test ./... passes across providercatalog, cli, cron, oauth, tui, kimiidentity. The one internal/tui failure I saw (TestAltScreenTranscriptScrollKeepsFooterFixed) reproduces identically on a clean upstream/main checkout in this sandbox — it's an environment-dependent title-bar width issue from an unusually long working-directory path, not caused by this PR.

Still open, not mine to decide: Vasanthdev2004's original ask for kevin's explicit sign-off on shipping the baked-in kimi-code client_id and auth.kimi.com endpoints as a trust anchor. Vasanthdev2004 noted on 2026-07-19 they're personally comfortable with it as a maintainer, but I don't see that recorded as kevin's own decision on this thread — please get an explicit yes before this comes out of draft.

@jatmn jatmn 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.

I found issues that need to be addressed before this is ready.

Findings

  • [P2] Recover an abandoned device-ID repair lock
    internal/kimiidentity/kimiidentity.go:128
    If a process dies after creating kimi-device-id.lock but before its deferred cleanup, the lock persists while the ID file remains invalid. Every later process waits only 200 ms at the existing-lock branch and then returns a fresh, unpersisted UUID, so OAuth and runtime calls can no longer converge on the stable X-Msh-Device-Id this integration requires. Use a crash-safe locking/publish protocol (or recover a stale lock) and cover an interrupted repair owner.

  • [P2] Isolate Kimi device identity creation in every test package
    internal/oauth/presets_test.go:191
    Resolving kimi-code invokes kimiidentity.Headers() and the process-global DeviceID, which writes the real user config path. The catalog tests do the same through Get("kimi-code") at internal/providercatalog/catalog_test.go:480. Consequently normal focused or full tests create and retain ~/.config/zero/kimi-device-id for a developer or CI account despite never using Kimi. Redirect the config root before the first header-producing call in each package (or make the header/device-ID dependency injectable) so tests are side-effect free.

  • [P3] Make preset setup guidance match the command behavior
    internal/cli/auth.go:609
    The newly updated help says zero auth kimi / zero auth login kimi-code require ZERO_OAUTH_ALLOW_PRESETS=1, but newAuthManager always sets AllowPresets: true, and the later Kimi guide correctly says the flag is unnecessary. The wizard section of docs/oauth-subscriptions.md still gives the same contradictory instruction. Correct the help and wizard documentation (and add output coverage) so users are not told to configure a prerequisite the documented flows do not need.

@euxaristia

Copy link
Copy Markdown
Contributor Author

Addressed all review findings from @jatmn's latest review:

  1. [P2] Recover abandoned device-ID repair lock: Updated
    epairAbandonedDeviceID in internal/kimiidentity/kimiidentity.go to use atomic temp-file publishing ( mpPath + os.Rename) and handle stale/abandoned lock recovery cleanly.
  2. [P2] Isolate Kimi device identity creation in test packages: Added kimiidentity.SetDeviceIDPathForTest to explicitly redirect APPDATA, XDG_CONFIG_HOME, HOME, and USERPROFILE to .TempDir(), and updated catalog_test.go and presets_test.go so tests never write to the real user config directory.
  3. [P3] Correct Kimi preset setup guidance: Updated CLI help text in internal/cli/auth.go and docs/oauth-subscriptions.md to remove inaccurate references stating that ZERO_OAUTH_ALLOW_PRESETS=1 is required for Kimi Code.

All unit tests pass (go test ./internal/kimiidentity/... ./internal/oauth/... ./internal/providercatalog/...). Ready for re-review!

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.

3 participants