feat(auth): add Kimi Code OAuth login - #851
Conversation
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.
…ation, and help text
…ation, and help text
…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
WalkthroughKimi 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. ChangesKimi Code OAuth
Estimated code review effort: 4 (Complex) | ~60 minutes Possibly related PRs
Suggested reviewers: 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
Actionable comments posted: 9
🧹 Nitpick comments (7)
internal/kimiidentity/kimiidentity_test.go (1)
270-274: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winFix the staleness threshold in this comment.
The comment says
> 1s. The production threshold inrepairAbandonedDeviceIDis2*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 valueTighten and clarify the host allowlist.
Two points.
First, the allowlist is partly redundant.
strings.HasSuffix(host, ".kimi.com")already coversauth.kimi.comandapi.kimi.com, so only the barekimi.comcase needs a separate test. The suffix tests are correct as written; the leading dot prevents anotkimi.commatch.Second, the domain set looks inconsistent.
.moonshot.cnis approved, but baremoonshot.cnis not, and.moonshot.aiis absent even though the existingmoonshotdescriptor usesapi.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 winOne non-canonical override silently strips the headers from the canonical requests too.
providerExtraHeadersreturnsnilif any single supplied endpoint is non-canonical. Consider an operator who overrides onlyZERO_OAUTH_KIMI_CODE_TOKEN_URLto a proxy and leaves the device endpoint atauth.kimi.com. The device-authorization request then goes to the canonical Kimi host without theX-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:
- 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.
- 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.mdso 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 winAdd a test that asserts the extra headers reach the wire.
Both updated call sites pass
nilfor the newextraHeadersparameter, which is correct. No test in this cohort verifies the positive case, though.TestResolveConfigKimiCodePresetchecks only thatConfig.ExtraHeadersis populated; nothing provesapplyExtraHeadersputs those headers on the outgoing request.Add a test with an
httptestserver that recordsr.Headerand assert theX-Msh-*values arrive. CoverPostTokenat minimum, and ideallyRequestDeviceCodeandpollDeviceOncetoo, 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 valuePass 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 openingproviderExtraHeaders. A single[]stringargument, or reuse of the already-computedcfgendpoint 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 winExtract 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 inlinedescriptor.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 onproviderwith 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
cloneDescriptordrops runtime headers for any descriptor that also has staticCustomHeaders.The
else ifmakes the two header sources mutually exclusive. A descriptor with both staticCustomHeadersandRuntimeHeaderssilently loses its runtime headers. No current descriptor has both, so this is not a live bug. The doc comment onRuntimeHeaderspresents 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
📒 Files selected for processing (26)
docs/oauth-subscriptions.mdinternal/cli/auth.gointernal/config/resolver.gointernal/config/resolver_test.gointernal/kimiidentity/kimiidentity.gointernal/kimiidentity/kimiidentity_test.gointernal/oauth/device.gointernal/oauth/flow.gointernal/oauth/flow_test.gointernal/oauth/manager.gointernal/oauth/oauth.gointernal/oauth/presets.gointernal/oauth/presets_test.gointernal/oauth/providers.gointernal/oauth/providers_test.gointernal/providercatalog/catalog.gointernal/providercatalog/catalog_test.gointernal/providercatalog/export_test.gointernal/providercatalog/oauth_test.gointernal/tui/model.gointernal/tui/oauth_device.gointernal/tui/onboarding.gointernal/tui/onboarding_test.gointernal/tui/provider_wizard.gointernal/tui/provider_wizard_discovery.gointernal/tui/provider_wizard_oauth_test.go
💤 Files with no reviewable changes (1)
- internal/providercatalog/export_test.go
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.
|
Addressed CodeRabbit findings in
@coderabbitai full review |
|
✅ Action performedFull 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. |
There was a problem hiding this comment.
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 winAssert that listing does not create a device ID.
The test only checks that listing descriptors have no
CustomHeaders. A regression could invokeRuntimeHeaders, discard the returned headers, and still persist the Kimi device ID.After
All()andOAuthProviders(), assert that the isolated config root has no Kimi device-ID file before callingGet("kimi-code").As per coding guidelines,
**/*_test.gorequires 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 | 🔵 TrivialRun identity handling under the race detector.
This package uses goroutines and shared identity-file repair paths, so run
internal/kimiidentitywith-racein 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
📒 Files selected for processing (11)
internal/config/resolver.gointernal/config/resolver_test.gointernal/kimiidentity/export_test.gointernal/kimiidentity/kimiidentity.gointernal/kimiidentity/kimiidentity_test.gointernal/oauth/presets_test.gointernal/oauth/providers_test.gointernal/providercatalog/catalog.gointernal/providercatalog/catalog_test.gointernal/tui/provider_wizard.gointernal/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
Add a path-keyed DeviceID regression across two config roots, and assert All/OAuthProviders do not persist kimi-device-id before Get mints it.
|
✅ Action performedFull 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. |
Vasanthdev2004
left a comment
There was a problem hiding this comment.
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, andRefreshviaPostToken), so the newExtraHeaderscan't ride a 307 to another origin. - I drove a throwaway
httptestTLS server through all four and confirmed theX-Msh-*values actually reach the wire — that part works. providerExtraHeadersfails closed when an endpoint is overridden off a canonical Kimi host, and resolving a retargetedkimi-codeprofile through the realconfig.Resolveentry point does drop the identity headers while keeping user-supplied ones.- The
kimialias still resolves tomoonshot,gofmt/go vetare clean, and./internal/{kimiidentity,oauth,providercatalog,config,tui}all pass (the onlyinternal/clifailure 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:117 — setupOAuthProviderOptions filters the provider list by calling providercatalog.Get(option.ID) on every option, and Get now runs RuntimeHeaders → kimiidentity.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.go—TestListByTransportPreservesCatalogOrderis gone (it was at line 364 onmain), along with itsexport_test.gohelper. That test pinned per-transport catalog ordering and theTransportOpenAICompatible/TransportAnthropicCompatiblealias normalisation. Addingkimi-codeinto the openai-compatible run breaks itswantIDslist, 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. (SwappingValidTransport/ValidAPIFormatfor inline maps inTestCatalogDescriptorsExposeRequiredDefaultsis fine — that keeps the per-descriptor invariant.)internal/oauth/providers_test.go—TestEnvKeywas replaced in place byTestResolveConfigKimiCodeStripsExtraHeadersOnEndpointOverride.envKeyis still production code and is exactly what mapskimi-code→ZERO_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 onlyZERO_OAUTH_KIMI_CODE_TOKEN_URL) drops the identity headers from all requests, including the ones still going toauth.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.cnbut not baremoonshot.cnand not.moonshot.ai, while the existingmoonshotdescriptor usesapi.moonshot.ai. Add a line saying that's deliberate or tighten the list.- No test asserts
ExtraHeadersreach the wire. I verified by hand that they do, but given the entire feature hinges on it, anhttptestserver recordingr.HeaderacrossRequestDeviceCode/pollDeviceOnce/PostToken/Refreshis 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.mddocuments theX-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.
|
Addressed Vasanth review items 1-4:
Owner decision still open: catalog comment says bearer is accepted with "no client spoofing" while |
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.
8334dc0 to
d41e06f
Compare
Vasanthdev2004
left a comment
There was a problem hiding this comment.
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.
|
@coderabbitai full review |
✅ Action performedFull 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 28 minutes. |
There was a problem hiding this comment.
Actionable comments posted: 8
🤖 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 606-612: Inspect newAuthManager to verify the AllowPresets value
used by auth login, then update the conflicting provider descriptions to match
that shipped behavior. In internal/cli/auth.go lines 606-612, correct the stale
Kimi or xAI help text; in docs/oauth-subscriptions.md lines 84-109, align both
the xAI opt-in and Kimi opt-out statements with the same result.
In `@internal/kimiidentity/kimiidentity.go`:
- Around line 164-175: The stale-lock recovery in the lock acquisition flow must
not reclaim locks solely based on the two-second mtime threshold. Update the
logic around os.ReadFile, os.Stat, and os.Rename to use a renewable lease or
another ownership protocol that proves the holder is dead before reclamation,
failing closed when ownership or permission cannot be verified; preserve atomic
lock replacement only after that proof.
- Around line 116-138: Update the device-ID publication flow around
readValidDeviceIDWithRetry and repairAbandonedDeviceID to write the complete ID
to a temporary file, check WriteString, Sync, and Close errors, then atomically
replace the destination while holding the repair lock. Apply the same atomic
publication and failure propagation to the initial creation path, returning an
error or otherwise signaling failure instead of an ID that was not persisted;
preserve post-publication rereading for race convergence.
- Around line 186-193: Update the deferred cleanup in the lock-acquisition flow
around lock.WriteString, lock.Sync, and lock.Close so failures from lock.Close
or removing lockPath are propagated to the caller or otherwise prevent a
successful device ID result. Only remove lockPath when its contents still match
ownerToken, preserving pre-existing or concurrently replaced locks, and ensure
cleanup failure causes the repair operation to fail closed.
- Around line 87-111: Update loadOrCreateDeviceIDAt and the helpers it invokes,
including readValidDeviceID and createOrAdoptDeviceID, to perform all device-ID,
lock, temporary-file, and rename operations relative to an opened
configuration-root handle using traversal-resistant, no-follow operations and
platform reparse-point protections. Remove direct path-based os.ReadFile,
os.MkdirAll, os.OpenFile, os.WriteFile, and os.Rename usage for these
operations; do not rely on pre-open EvalSymlinks, and preserve the existing
race-safe create-or-adopt behavior.
In `@internal/oauth/manager.go`:
- Around line 201-205: Update the token persistence flow around ProviderKey and
m.store.Save to serialize context cancellation with the commit, using an attempt
generation or shared lock so cancellation and token persistence are mutually
exclusive. Ensure a device-login attempt that is canceled before commit cannot
overwrite stored authentication state.
In `@internal/oauth/oauth.go`:
- Around line 117-126: Add request-level httptest regression coverage for
applyExtraHeaders across device authorization, device polling, code exchange,
and refresh flows. Assert Kimi ExtraHeaders are received by each HTTP handler,
and verify a provider without ExtraHeaders sends none; keep the existing
configuration-resolution tests intact.
In `@internal/oauth/presets.go`:
- Around line 174-180: Restrict isCanonicalKimiHost to an explicit allowlist of
approved OAuth endpoint hosts instead of accepting arbitrary *.kimi.com or
*.moonshot.cn subdomains. Preserve URL parsing and case normalization, then add
tests covering every approved host and representative unapproved subdomains to
verify identity headers are only applied to verified endpoints.
🪄 Autofix
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: 341e6d92-2d2c-42df-9aca-d2d7586569b4
📒 Files selected for processing (27)
docs/oauth-subscriptions.mdinternal/cli/auth.gointernal/config/resolver.gointernal/config/resolver_test.gointernal/kimiidentity/export_test.gointernal/kimiidentity/kimiidentity.gointernal/kimiidentity/kimiidentity_test.gointernal/oauth/device.gointernal/oauth/flow.gointernal/oauth/flow_test.gointernal/oauth/manager.gointernal/oauth/oauth.gointernal/oauth/presets.gointernal/oauth/presets_test.gointernal/oauth/providers.gointernal/oauth/providers_test.gointernal/providercatalog/catalog.gointernal/providercatalog/catalog_test.gointernal/providercatalog/export_test.gointernal/providercatalog/oauth_test.gointernal/tui/model.gointernal/tui/oauth_device.gointernal/tui/onboarding.gointernal/tui/onboarding_test.gointernal/tui/provider_wizard.gointernal/tui/provider_wizard_discovery.gointernal/tui/provider_wizard_oauth_test.go
Bind device-id storage to an opened config root, publish via complete temp files, reclaim repair locks only for dead holders, restrict Kimi identity headers to approved hosts, add ExtraHeaders httptest coverage, and document that zero auth login enables presets for xai and kimi alike. Refs Gitlawb#708
CodeRabbit follow-up (c038f96)Addressed open review findings on Kimi OAuth identity / presets: Fixed
Skipped
Verify@coderabbitai full review |
Rate Limit Exceeded
|
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 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/process_alive_windows.go`:
- Around line 10-26: Make process liveness checks fail closed. In
internal/kimiidentity/process_alive_windows.go, update processAlive so pid <= 0
remains false, but OpenProcess failures and GetExitCodeProcess failures return
true; only a successfully queried process with a non-STILL_ACTIVE exit code
should return false. In internal/kimiidentity/process_alive_posix.go, update
processAlive to return false only for syscall.ESRCH, while treating nil and
syscall.EPERM—and other inconclusive errors—as alive.
🪄 Autofix
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: da252ea1-5e47-464a-9ce7-6d81bbbdbe70
📒 Files selected for processing (11)
docs/oauth-subscriptions.mdinternal/cli/auth.gointernal/kimiidentity/kimiidentity.gointernal/kimiidentity/kimiidentity_test.gointernal/kimiidentity/process_alive_posix.gointernal/kimiidentity/process_alive_windows.gointernal/oauth/device_test.gointernal/oauth/flow_test.gointernal/oauth/manager.gointernal/oauth/presets.gointernal/oauth/providers_test.go
🚧 Files skipped from review as they are similar to previous changes (3)
- internal/cli/auth.go
- internal/kimiidentity/kimiidentity.go
- docs/oauth-subscriptions.md
Only reclaim a repair lock when the holder is proven dead (Windows ERROR_INVALID_PARAMETER / POSIX ESRCH). Treat access-denied and other inconclusive processAlive results as alive so a live holder cannot lose its lease. Refs Gitlawb#851
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 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/process_alive_windows.go`:
- Line 24: Update the process liveness logic around OpenProcess to request
SYNCHRONIZE and query termination with windows.WaitForSingleObject(handle, 0)
instead of interpreting the exit code 259. Return dead for WAIT_OBJECT_0, live
for WAIT_TIMEOUT and query errors, while preserving handle cleanup. Add a
Windows regression test covering a child process that exits with code 259.
🪄 Autofix
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: 10a3cd12-b5ef-4090-9a65-9571343e2da3
📒 Files selected for processing (2)
internal/kimiidentity/process_alive_posix.gointernal/kimiidentity/process_alive_windows.go
Exit code 259 is STILL_ACTIVE but can also be a real exit code, so GetExitCodeProcess alone can treat a dead lock holder as live and block repair-lease reclamation. Open with SYNCHRONIZE and use a zero-timeout wait instead; keep fail-closed behavior for access-denied and unknown errors.
|
Addressed the Windows |
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 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/process_alive_windows_test.go`:
- Around line 12-33: Update TestProcessAliveExitCode259IsDead to open and retain
a SYNCHRONIZE handle for the child process before calling cmd.Wait. Wait on that
handle to confirm termination, keep it open while asserting processAlive(pid) is
false, then close the handle and call cmd.Wait afterward while preserving the
ExitError code-259 validation.
🪄 Autofix
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: db2cb8dc-abe2-46e8-bf07-23fb48db4cf1
📒 Files selected for processing (2)
internal/kimiidentity/process_alive_windows.gointernal/kimiidentity/process_alive_windows_test.go
| // TestProcessAliveExitCode259IsDead is the regression for treating STILL_ACTIVE | ||
| // (259) as a process exit code rather than a liveness flag. A dead child that | ||
| // exits with 259 must be reported as not alive so a stale repair lease can be | ||
| // reclaimed. | ||
| func TestProcessAliveExitCode259IsDead(t *testing.T) { | ||
| cmd := exec.Command("cmd", "/C", "exit /b 259") | ||
| if err := cmd.Start(); err != nil { | ||
| t.Fatalf("Start: %v", err) | ||
| } | ||
| pid := cmd.Process.Pid | ||
| err := cmd.Wait() | ||
| var ee *exec.ExitError | ||
| if !errors.As(err, &ee) || ee.ExitCode() != 259 { | ||
| t.Fatalf("Wait: %v (want ExitError with code 259)", err) | ||
| } | ||
| // Keep a live reference so the process object is still openable by PID | ||
| // while we probe (Go retains a handle until Process is released). | ||
| _ = cmd.Process | ||
| if processAlive(pid) { | ||
| t.Fatalf("processAlive(%d) = true after exit code 259; want false (dead)", pid) | ||
| } | ||
| } |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== locate relevant files =="
fd -a 'process_alive_windows(test)?\.go$' . || true
echo "== file outline =="
for f in $(fd 'process_alive_windows(test)?\.go$' .); do
echo "--- $f"
wc -l "$f"
done
echo "== process_alive_windows_test.go =="
test -f internal/kimiidentity/process_alive_windows_test.go && cat -n internal/kimiidentity/process_alive_windows_test.go
echo "== process_alive_windows.go =="
test -f internal/kimiidentity/process_alive_windows.go && cat -n internal/kimiidentity/process_alive_windows.go
echo "== all processAlive usages =="
rg -n "processAlive|ProcessAlive|STILL_ACTIVE|WAIT_OBJECT_0|ERROR_INVALID_PARAMETER" .Repository: Gitlawb/zero
Length of output: 8745
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Behavioral probe: demonstrate the Go exec test-process lifetime semantics without repository changes.
# We cannot execute repository code, but Go's "cmd.Wait closes handles" behavior is documented and
# can be observed by checking whether a fresh "go tool" invocation can open the child PID immediately
# after cmd.Wait() returns and cmd.Process has not been assigned elsewhere.
#
# This small standalone probe must fail fast on non-Windows; the repository implementation uses
# Windows APIs, so do not treat the probe result as a platform-independent proof.
if [ "$(go env GOOS)" = "windows" ]; then
cat > /tmp/probe_wait_pid_lifetime.go <<'GO'
package main
import (
"fmt"
"os"
"os/exec"
"syscall"
)
func main() {
cmd := exec.Command("cmd", "/C", "exit 259")
if err := cmd.Start(); err != nil {
fmt.Fprintf(os.Stderr, "Start: %v\n", err)
os.Exit(1)
}
pid := cmd.Process.Pid
if err := cmd.Wait(); err == nil {
fmt.Fprintln(os.Stderr, "Wait unexpectedly succeeded")
os.Exit(1)
}
proc, err := os.FindProcess(pid)
if err == nil {
handle := proc.Phandle
fmt.Printf("find: err=%v pid=%v handle=%v\n", err, pid, handle)
} else {
fmt.Printf("find: err=%v pid=%v\n", err, pid)
h, err := syscall.OpenProcess(syscall.SYNCHRONIZE, false, uint32(pid))
fmt.Printf("syscall: err=%v pid=%v handle=%v\n", err, pid, h)
}
os.Exit(0)
}
GO
set +e
go run /tmp/probe_wait_pid_lifetime.go
status=$?
set -e
echo "probe exit status=$status"
else
echo "probe skipped: GOOS=$(go env GOOS); repository implementation is Windows-only"
fiRepository: Gitlawb/zero
Length of output: 219
🌐 Web query:
Go exec.Cmd Wait Windows process handle documentation
💡 Result:
In Go's os/exec package, Cmd.Wait is the standard method for waiting for an external process to exit and cleaning up its associated resources [1][2]. When running on Windows, this process involves both waiting for the process to terminate and ensuring that I/O operations (stdin, stdout, and stderr) are fully closed [1][2]. Core behavior on Windows: 1. Process Termination: Cmd.Wait internally calls os.Process.Wait, which uses the Windows API WaitForSingleObject (specifically with the process handle) to block until the process exits [3]. Once the process terminates, it also retrieves the exit code using GetExitCodeProcess and process timing information via GetProcessTimes [3]. 2. Resource Cleanup: Cmd.Wait is responsible for releasing resources associated with the Cmd struct, such as closing file descriptors (pipes) used for standard I/O [1][2]. Even if the process itself has terminated, Cmd.Wait may continue to block until all copying operations to stdin or from stdout/stderr have finished [2]. If these I/O operations are not handled or if they hang, Wait will not return, which is a known point of divergence from os.Process.Wait (which only cares about process exit) [4]. 3. Handle Management: On Windows, every os.Process contains a handle to the process, which is managed internally [3]. The os/exec package ensures this handle is acquired and released correctly during the waiting process [3]. The os/exec implementation on Windows is specifically designed to handle these handles via handleTransientAcquire and handleTransientRelease to maintain correct system resource state [3]. 4. Error Handling: Cmd.Wait returns an *ExitError if the process exits with a non-zero status code [1][2]. It also handles specific Windows error codes related to pipe I/O (such as ERROR_BROKEN_PIPE or ERROR_NO_DATA), ensuring that operations like stdin writes do not cause spurious errors if the process exits unexpectedly [5]. In summary, while os.Process.Wait is a low-level synchronization primitive targeting the process handle, exec.Cmd.Wait is a higher-level wrapper that manages the lifecycle of the process and its associated I/O streams [2][4]. It is required to be called after Start to prevent resource leaks [1].
Citations:
- 1: https://pkg.go.dev/os/exec?GOOS=windows
- 2: https://go.dev/src/os/exec/exec.go?m=text
- 3: https://github.com/golang/go/blob/master/src/os/exec_windows.go
- 4: os/exec: Inconsistent behaviour in
exec.Cmd.Waitgolang/go#20730 - 5: windows: exec.Cmd.Wait returns an error when the process exits without reading stdin golang/go#20445
Keep a liveness handle open before asserting the terminated-process branch.
cmd.Wait() closes the process handle resource held by cmd.Process, so processAlive(pid) can take the ERROR_INVALID_PARAMETER return path instead of the intended WAIT_OBJECT_0 signal path. Create the SYNCHRONIZE handle before waiting, wait on that handle to signal, keep it open during processAlive(pid), then call cmd.Wait() after the assertion.
🤖 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/process_alive_windows_test.go` around lines 12 - 33,
Update TestProcessAliveExitCode259IsDead to open and retain a SYNCHRONIZE handle
for the child process before calling cmd.Wait. Wait on that handle to confirm
termination, keep it open while asserting processAlive(pid) is false, then close
the handle and call cmd.Wait afterward while preserving the ExitError code-259
validation.
Source: Coding guidelines
|
@coderabbitai full review |
✅ Action performedFull review finished. |
There was a problem hiding this comment.
Actionable comments posted: 3
♻️ Duplicate comments (1)
internal/kimiidentity/process_alive_windows_test.go (1)
16-33: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick winThe test does not exercise the
WAIT_OBJECT_0branch it claims to cover.This was raised in a previous review and is still present.
cmd.Wait()releases the process handle thatcmd.Processholds. The_ = cmd.Processstatement at Line 29 is a no-op; it retains nothing. AfterWaitreturns, the PID can be freed, soprocessAlive(pid)returnsfalsethrough theERROR_INVALID_PARAMETERpath inOpenProcess, not throughWaitForSingleObjectreturningWAIT_OBJECT_0. The test therefore passes without proving that exit code 259 is handled correctly, and it can flake if the operating system recycles the PID onto a live process before the probe.Open a
SYNCHRONIZEhandle beforecmd.Wait(), keep it open across the assertion, and callcmd.Wait()after the assertion. The retained handle keeps the PID reserved and forces the wait path.💚 Proposed fix: retain a SYNCHRONIZE handle across the probe
pid := cmd.Process.Pid - err := cmd.Wait() + // Hold an independent handle so the PID cannot be freed or recycled while + // we probe; this forces the WaitForSingleObject path in processAlive. + handle, err := windows.OpenProcess(windows.SYNCHRONIZE, false, uint32(pid)) + if err != nil { + t.Fatalf("OpenProcess: %v", err) + } + defer windows.CloseHandle(handle) + + err = cmd.Wait() var ee *exec.ExitError if !errors.As(err, &ee) || ee.ExitCode() != 259 { t.Fatalf("Wait: %v (want ExitError with code 259)", err) } - // Keep a live reference so the process object is still openable by PID - // while we probe (Go retains a handle until Process is released). - _ = cmd.Process if processAlive(pid) { t.Fatalf("processAlive(%d) = true after exit code 259; want false (dead)", pid) }Add the import:
import ( "errors" "os" "os/exec" "testing" + + "golang.org/x/sys/windows" )🤖 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/process_alive_windows_test.go` around lines 16 - 33, Update TestProcessAliveExitCode259IsDead to open and retain a Windows SYNCHRONIZE handle for the child process before calling cmd.Wait(), using the existing process-aliveness implementation’s expected handle type. Probe processAlive while that handle remains open so the check reaches the WAIT_OBJECT_0 path, then call cmd.Wait() afterward and close the retained handle reliably; remove the ineffective _ = cmd.Process statement.Source: Coding guidelines
🧹 Nitpick comments (6)
internal/providercatalog/catalog.go (1)
416-430: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick winRemove
RuntimeHeadersfrom listing copies.
cloneDescriptor(descriptor, false)leavesRuntimeHeaderscallable. A catalog or TUI consumer can invoke the callback and create Kimi's persistent device identity during listing. Clear the callback whenwithRuntimeHeadersis false. Keep it only onGetandRequireresults.Proposed fix
func cloneDescriptor(descriptor Descriptor, withRuntimeHeaders bool) Descriptor { descriptor.AuthEnvVars = append([]string{}, descriptor.AuthEnvVars...) descriptor.SupportedAPIFormats = append([]APIFormat{}, descriptor.SupportedAPIFormats...) descriptor.Aliases = append([]string{}, descriptor.Aliases...) + if !withRuntimeHeaders { + descriptor.RuntimeHeaders = nil + } if descriptor.CustomHeaders != nil { descriptor.CustomHeaders = copyStringMap(descriptor.CustomHeaders) } 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 416 - 430, Update cloneDescriptor to clear RuntimeHeaders when withRuntimeHeaders is false, while preserving it for Get and Require results. Ensure listing copies returned by All and OAuthProviders cannot invoke the callback or create persistent device identity.internal/kimiidentity/kimiidentity_test.go (2)
157-196: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winThe adopt-the-winner test depends on a fixed timing margin.
The simulated winner sleeps 30ms before writing.
readValidDeviceIDWithRetrygives up after 200ms. That leaves 170ms of slack. Under-raceon a loaded CI runner, four goroutines plus scheduler jitter can consume that slack, and a worker then returns its own freshly minted id instead ofwinner, which fails the test.Signal the write instead of sleeping: have the workers start first, then write the winner from the main goroutine. Or reduce the sleep to a few milliseconds. Either removes the dependency on absolute wall-clock margins.
As per coding guidelines, "run affected concurrent code under the race detector."
♻️ Proposed fix: shrink the timing dependency
done := make(chan struct{}) + started := make(chan struct{}) go func() { defer close(done) - time.Sleep(30 * time.Millisecond) + <-started + time.Sleep(5 * time.Millisecond) _, _ = f.WriteString(winner + "\n") _ = f.Sync() _ = f.Close() }() const workers = 4 ids := make([]string, workers) var wg sync.WaitGroup wg.Add(workers) for i := range workers { go func(i int) { defer wg.Done() ids[i] = loadOrCreateDeviceIDAt(path) }(i) } + close(started) wg.Wait()🤖 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 157 - 196, Update TestLoadOrCreateDeviceIDAdoptsWinnerAfterEmptyCreate to remove the fixed 30ms timing dependency: start the worker goroutines, then signal or perform the winner write from the main goroutine so readValidDeviceIDWithRetry observes it deterministically. Keep all workers required to return winner, and run the affected concurrent test with the race detector.Source: Coding guidelines
371-383: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winAdd a non-ASCII case to the sanitizer test.
The realistic input for
asciiHeaderValueisos.Hostname()on a machine with a non-ASCII hostname. The current cases cover printable ASCII, C0 control characters, and the all-control fallback. They do not cover multi-byte runes, which is the main class this function strips.Add a case with mixed ASCII and non-ASCII, and a case that is entirely non-ASCII so the
"unknown"fallback is proven for that input class.💚 Proposed additional cases
if got := asciiHeaderValue("\x01\x02"); got != "unknown" { t.Fatalf("got %q, want unknown", got) } + if got := asciiHeaderValue("büro-laptop"); got != "bro-laptop" { + t.Fatalf("got %q, want bro-laptop", got) + } + if got := asciiHeaderValue("主机名"); got != "unknown" { + t.Fatalf("got %q, want unknown", got) + }🤖 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 371 - 383, Add non-ASCII coverage to TestAsciiHeaderValueStripsNonPrintable: verify mixed ASCII and multi-byte characters are sanitized to the retained ASCII content, and verify an entirely non-ASCII input returns "unknown". Keep the existing printable, control-character, and all-control cases unchanged.internal/tui/provider_wizard.go (2)
1403-1422: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winCentralize the
x-msh-prefix.The literal
"x-msh-"now appears here and twice ininternal/config/resolver.go(Lines 1075 and 1114), across two packages. A future vendor-header change must update all three sites. Export a constant and a predicate from the package that owns the identity headers (internal/kimiidentityorinternal/providercatalog) and call it from both.The clone-before-delete behavior itself is correct and well covered by
TestStripRuntimeIdentityHeadersDropsXMshOnly.🤖 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/provider_wizard.go` around lines 1403 - 1422, Centralize the X-Msh header prefix and detection logic in the owning identity-header package by exporting a prefix constant and predicate. Update stripRuntimeIdentityHeaders and both resolver locations to use that shared predicate instead of local "x-msh-" literals, while preserving the existing clone-before-delete behavior and case-insensitive matching.
389-389: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueThe subtitle lists device code for Kimi Code only.
docs/oauth-subscriptions.mdLine 40 lists device code for xAI, Kimi Code, and Hugging Face. Align the wizard subtitle so the two descriptions agree.✏️ Proposed fix
- subtitle: "No API key to copy — one-click browser login (OpenRouter, xAI, ChatGPT, Hugging Face) or device code (Kimi Code).", + subtitle: "No API key to copy — one-click browser login (OpenRouter, xAI, ChatGPT, Hugging Face) or device code (xAI, Kimi Code, Hugging Face).",As per coding guidelines: "Ensure PR descriptions, help text, and comments match shipped behavior".
🤖 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/provider_wizard.go` at line 389, Update the subtitle in the provider wizard’s no-API-key option to list device-code authentication for xAI, Kimi Code, and Hugging Face, matching the supported behavior documented in docs/oauth-subscriptions.md.Source: Coding guidelines
docs/oauth-subscriptions.md (1)
113-118: 📐 Maintainability & Code Quality | 🔵 TrivialThe documentation admits the
X-Msh-*header set is unverified.Lines 116-118 tell readers to verify the headers against a real login. The PR objectives list an end-to-end Kimi Code login test as an open item. Until that test exists, a header-name or ordering mistake reaches users as a login failure with no local signal.
A hermetic test against a stub authorization server would cover the contract without a real Kimi account: assert the exact
X-Msh-*header names and values on the device-authorization, poll, exchange, and refresh requests. Do you want me to draft that test, or open an issue to track it?🤖 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 `@docs/oauth-subscriptions.md` around lines 113 - 118, The documentation currently leaves the X-Msh-* header contract unverified; add a hermetic OAuth test using a stub authorization server that exercises device authorization, polling, code exchange, and refresh, asserting the exact header names and values on each request. Remove the unresolved verification warning only once this coverage is in place.
🤖 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 606-613: Update the provider help text in internal/cli/auth.go
around the zero auth login documentation to remove provider-specific preset
opt-in claims and describe the global AllowPresets behavior for all OAuth
presets. Also update docs/oauth-subscriptions.md lines 54-63 and the referenced
lines 124 and 137 to use consistent global opt-in wording; do not imply that
presets require selecting particular providers or commands.
In `@internal/kimiidentity/kimiidentity.go`:
- Around line 288-299: Update reclaimDeadRepairLock to keep all lock operations
rooted in the provided *os.Root instead of constructing a path with root.Name().
Replace the lockutil.ReclaimStaleLock usage or add a rooted wrapper so rename,
read, restore, and removal use root.Rename, root.ReadFile, and root.Remove,
while preserving the existing lockHolderAlive and fail-closed behavior.
In `@internal/tui/onboarding_test.go`:
- Around line 2157-2160: Update the type-assertion diagnostics at
internal/tui/onboarding_test.go:2157-2160,
internal/tui/provider_wizard_oauth_test.go:252-255, and
internal/tui/provider_wizard_oauth_test.go:295-298: assign cmd() to an untyped
raw variable, assert raw against the expected setupOAuthMsg or
providerWizardOAuthMsg type, and format raw with %T on failure so diagnostics
report the actual command type.
---
Duplicate comments:
In `@internal/kimiidentity/process_alive_windows_test.go`:
- Around line 16-33: Update TestProcessAliveExitCode259IsDead to open and retain
a Windows SYNCHRONIZE handle for the child process before calling cmd.Wait(),
using the existing process-aliveness implementation’s expected handle type.
Probe processAlive while that handle remains open so the check reaches the
WAIT_OBJECT_0 path, then call cmd.Wait() afterward and close the retained handle
reliably; remove the ineffective _ = cmd.Process statement.
---
Nitpick comments:
In `@docs/oauth-subscriptions.md`:
- Around line 113-118: The documentation currently leaves the X-Msh-* header
contract unverified; add a hermetic OAuth test using a stub authorization server
that exercises device authorization, polling, code exchange, and refresh,
asserting the exact header names and values on each request. Remove the
unresolved verification warning only once this coverage is in place.
In `@internal/kimiidentity/kimiidentity_test.go`:
- Around line 157-196: Update
TestLoadOrCreateDeviceIDAdoptsWinnerAfterEmptyCreate to remove the fixed 30ms
timing dependency: start the worker goroutines, then signal or perform the
winner write from the main goroutine so readValidDeviceIDWithRetry observes it
deterministically. Keep all workers required to return winner, and run the
affected concurrent test with the race detector.
- Around line 371-383: Add non-ASCII coverage to
TestAsciiHeaderValueStripsNonPrintable: verify mixed ASCII and multi-byte
characters are sanitized to the retained ASCII content, and verify an entirely
non-ASCII input returns "unknown". Keep the existing printable,
control-character, and all-control cases unchanged.
In `@internal/providercatalog/catalog.go`:
- Around line 416-430: Update cloneDescriptor to clear RuntimeHeaders when
withRuntimeHeaders is false, while preserving it for Get and Require results.
Ensure listing copies returned by All and OAuthProviders cannot invoke the
callback or create persistent device identity.
In `@internal/tui/provider_wizard.go`:
- Around line 1403-1422: Centralize the X-Msh header prefix and detection logic
in the owning identity-header package by exporting a prefix constant and
predicate. Update stripRuntimeIdentityHeaders and both resolver locations to use
that shared predicate instead of local "x-msh-" literals, while preserving the
existing clone-before-delete behavior and case-insensitive matching.
- Line 389: Update the subtitle in the provider wizard’s no-API-key option to
list device-code authentication for xAI, Kimi Code, and Hugging Face, matching
the supported behavior documented in docs/oauth-subscriptions.md.
🪄 Autofix
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: f7f6e70d-177e-4d1b-9757-381d2554214c
📒 Files selected for processing (31)
docs/oauth-subscriptions.mdinternal/cli/auth.gointernal/config/resolver.gointernal/config/resolver_test.gointernal/kimiidentity/export_test.gointernal/kimiidentity/kimiidentity.gointernal/kimiidentity/kimiidentity_test.gointernal/kimiidentity/process_alive_posix.gointernal/kimiidentity/process_alive_windows.gointernal/kimiidentity/process_alive_windows_test.gointernal/oauth/device.gointernal/oauth/device_test.gointernal/oauth/flow.gointernal/oauth/flow_test.gointernal/oauth/manager.gointernal/oauth/oauth.gointernal/oauth/presets.gointernal/oauth/presets_test.gointernal/oauth/providers.gointernal/oauth/providers_test.gointernal/providercatalog/catalog.gointernal/providercatalog/catalog_test.gointernal/providercatalog/export_test.gointernal/providercatalog/oauth_test.gointernal/tui/model.gointernal/tui/oauth_device.gointernal/tui/onboarding.gointernal/tui/onboarding_test.gointernal/tui/provider_wizard.gointernal/tui/provider_wizard_discovery.gointernal/tui/provider_wizard_oauth_test.go
| func reclaimDeadRepairLock(root *os.Root, lockName string) (bool, error) { | ||
| lockPath := filepath.Join(root.Name(), lockName) | ||
| suffix := fmt.Sprintf("%d.%d", os.Getpid(), time.Now().UnixNano()) | ||
| return lockutil.ReclaimStaleLock(lockPath, suffix, func(reclaimedPath string) bool { | ||
| raw, err := os.ReadFile(reclaimedPath) | ||
| if err != nil { | ||
| // Cannot inspect: fail closed and treat as live so we restore. | ||
| return true | ||
| } | ||
| return lockHolderAlive(raw) | ||
| }) | ||
| } |
There was a problem hiding this comment.
🔒 Security & Privacy | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "--- lockutil implementation ---"
fd -t f . internal/lockutil --exec cat -n {} \;
echo "--- other ReclaimStaleLock callers ---"
rg -n -C5 'ReclaimStaleLock' --glob '*.go'
echo "--- os.Root.Name usages ---"
rg -n -C3 '\.Name\(\)' internal/kimiidentityRepository: Gitlawb/zero
Length of output: 30966
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "--- kimyidentity relevant sections ---"
fd -t f 'kimiidentity.go$' . --exec sh -c 'wc -l "$1"; ast-grep outline "$1" --match "openDeviceIDDir" --view expanded; sed -n "1,360p" "$1"' sh {} \;
echo "--- lockutil package files ---"
fd -t f . internal/lockutil | sed 's#^\./##' | sortRepository: Gitlawb/zero
Length of output: 14103
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "--- Go os.Root docs if available ---"
go doc os.Root 2>/dev/null || true
echo "--- Go stdlib source doc snippets if available ---"
go env GOROOT
grep -n "func .*Root.*Name\\|type Root struct\\|func \\(\\*Root\\) Rename\\|func \\(\\*Root\\) ReadFile\\|func \\(\\*Root\\) Remove" "$(go env GOROOT)/src/os/root.go" 2>/dev/null || true
sed -n '1,220p' "$(go env GOROOT)/src/os/root.go" 2>/dev/null || true
echo "--- read-only behavioral text analysis of lockutil ---"
python3 - <<'PY'
from pathlib import Path
for p in ["internal/lockutil/reclaim.go", "internal/lockutil/reclaim_other_test.go", "internal/lockutil/reclaim_windows_test.go"]:
text = Path(p).read_text()
print(p)
for needle in ["ReclaimStaleLock", "os.Rename", "RemoveLockFile", "isLive(reclaimed)", "restoreLockFile", "os.ReadFile"]:
if needle in text:
print(" contains", needle)
# simple parser: print ReclaimStaleLock and surrounding implementation
lines = text.splitlines()
for i,l in enumerate(lines):
if "func ReclaimStaleLock" in l:
print(" ReclaimStaleLock:")
for j in range(max(0,i-5), min(len(lines), i+35)):
print(f" {j+1}: {lines[j]}")
PYRepository: Gitlawb/zero
Length of output: 12518
Keep lock reclamation inside *os.Root.
ReclaimStaleLock receives a plain path, uses os.Rename, then passes the moved name into os.ReadFile before restoring/removing it. root.Name() is the path string from when the root was opened, so rebuilding root.Name() + lockName lets the reclaim operations pass through symlink/reparse-point components again. Perform the reclaim with root.Rename, root.ReadFile, and root.Remove or add a rooted Lockutil wrapper; do not rely on filepath.Join(root.Name(), lockName).
🤖 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 288 - 299, Update
reclaimDeadRepairLock to keep all lock operations rooted in the provided
*os.Root instead of constructing a path with root.Name(). Replace the
lockutil.ReclaimStaleLock usage or add a rooted wrapper so rename, read,
restore, and removal use root.Rename, root.ReadFile, and root.Remove, while
preserving the existing lockHolderAlive and fail-closed behavior.
Source: Coding guidelines
…I test diagnostics Refs Gitlawb#851
|
@coderabbitai full review |
✅ Action performedFull review finished. |
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 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/process_alive_windows_test.go`:
- Around line 65-68: Update the test around the process termination assertion to
open a SYNCHRONIZE process handle before cmd.Process.Kill, wait on that handle,
and invoke processAlive(pid) while the handle remains open. Close the handle
only after the liveness assertion, then call cmd.Wait; follow the existing
handle-management pattern used by TestProcessAliveExitCode259IsDead.
🪄 Autofix
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: dd40f6ff-11c1-4979-8d67-29550113a8fa
📒 Files selected for processing (4)
internal/kimiidentity/kimiidentity.gointernal/kimiidentity/process_alive_windows_test.gointernal/tui/onboarding_test.gointernal/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/tui/onboarding_test.go
- internal/kimiidentity/kimiidentity.go
| _ = cmd.Process.Kill() | ||
| _ = cmd.Wait() | ||
|
|
||
| if processAlive(pid) { |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Keep the liveness handle open before the post-wait assertion.
Line 66 calls cmd.Wait() before Line 68 calls processAlive(pid). On Windows, cmd.Wait() can close the process handle held by cmd.Process, so processAlive(pid) can receive ERROR_INVALID_PARAMETER and pass through the invalid-PID branch. The test can then pass without exercising WAIT_OBJECT_0 for a terminated process.
Open a SYNCHRONIZE handle before Kill, wait on that handle, call processAlive(pid) while the handle remains open, then close it and call cmd.Wait(). This is the same unresolved handle-lifetime issue previously reported for TestProcessAliveExitCode259IsDead.
#!/bin/bash
set -euo pipefail
sed -n '49,71p' internal/kimiidentity/process_alive_windows_test.go
rg -n -C 3 \
'OpenProcess|WaitForSingleObject|processAlive\(pid\)|cmd\.Wait\(\)' \
internal/kimiidentity/process_alive_windows_test.go \
internal/kimiidentity/process_alive_windows.go🤖 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/process_alive_windows_test.go` around lines 65 - 68,
Update the test around the process termination assertion to open a SYNCHRONIZE
process handle before cmd.Process.Kill, wait on that handle, and invoke
processAlive(pid) while the handle remains open. Close the handle only after the
liveness assertion, then call cmd.Wait; follow the existing handle-management
pattern used by TestProcessAliveExitCode259IsDead.
Source: Coding guidelines
There was a problem hiding this comment.
Actionable comments posted: 3
♻️ Duplicate comments (2)
internal/kimiidentity/kimiidentity.go (1)
288-302: 🔒 Security & Privacy | 🟠 Major | 🏗️ Heavy liftKeep repair-lock reclamation inside the
*os.Roothandle.Line 289 rebuilds a path from
root.Name().root.Name()is the path string captured when the root was opened, so it is not traversal-resistant.lockutil.ReclaimStaleLockthen performsos.Rename, and the callback at Line 295 performsos.ReadFileon that reconstructed path. A symlink or reparse point on a component can redirect the rename, the read, and the restore outside the configuration root. That defeats the rooted design used by every other operation in this file.Perform the reclaim with
root.Rename,root.ReadFile, androot.Remove, or add a rooted variant oflockutil.ReclaimStaleLockthat accepts an*os.Rootplus a name.As per coding guidelines, "Do not rely on pre-open
EvalSymlinksfollowed by open for containment; bind containment at open/use time with rooted or handle-relative traversal-resistant APIs."🤖 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 288 - 302, The reclaimDeadRepairLock flow must remain rooted in the provided *os.Root instead of reconstructing paths from root.Name(). Replace or extend lockutil.ReclaimStaleLock so reclamation uses root.Rename, root.ReadFile, and root.Remove for every lock operation, including the lockHolderAlive callback, while preserving the existing stale-lock and fail-closed behavior.Source: Coding guidelines
internal/kimiidentity/process_alive_windows_test.go (1)
22-32: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick winBoth Windows liveness assertions probe a PID whose handle is already released.
cmd.Wait()releases the process handle Go holds, sowindows.OpenProcesscan returnERROR_INVALID_PARAMETERandprocessAlivereturnsfalsefrom the invalid-PID branch instead of theWAIT_OBJECT_0branch. Both tests then pass without exercising the terminated-process path they exist to cover.
internal/kimiidentity/process_alive_windows_test.go#L22-L32: open aSYNCHRONIZEhandle beforecmd.Wait(), keep it open across theprocessAlive(pid)assertion, then close it; remove the no-op_ = cmd.Processstatement.internal/kimiidentity/process_alive_windows_test.go#L65-L70: open aSYNCHRONIZEhandle beforecmd.Process.Kill(), wait on that handle, assertprocessAlive(pid)while it stays open, then close it and callcmd.Wait().🤖 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/process_alive_windows_test.go` around lines 22 - 32, Update both liveness tests in internal/kimiidentity/process_alive_windows_test.go: lines 22-32 should open and retain a SYNCHRONIZE handle before cmd.Wait(), use it during processAlive(pid), then close it and remove the no-op cmd.Process reference; lines 65-70 should open the handle before cmd.Process.Kill(), wait on it, assert processAlive(pid) while retained, then close it before calling cmd.Wait().Source: Coding guidelines
🧹 Nitpick comments (3)
internal/tui/provider_wizard_oauth_test.go (1)
224-225: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winTwo cancellation tests redirect only
XDG_CONFIG_HOME.os.UserConfigDirreadsAPPDATAon Windows and$HOME/Library/Application Supporton macOS, so these tests are hermetic on Linux only. The file already definesisolateKimiDeviceIDStorage, which sets all three variables.
internal/tui/provider_wizard_oauth_test.go#L224-L225: replace the baret.Setenv("XDG_CONFIG_HOME", t.TempDir())inTestProviderWizardEscCancelsDeviceLoginPollwithisolateKimiDeviceIDStorage(t)and keep the explicitZERO_OAUTH_TOKENS_PATH.internal/tui/provider_wizard_oauth_test.go#L270-L271: apply the same replacement inTestModelQuitCancelsProviderWizardDeviceLoginPoll.As per coding guidelines: "Code and tests must pass on Linux, macOS, and Windows".
🤖 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/provider_wizard_oauth_test.go` around lines 224 - 225, Update TestProviderWizardEscCancelsDeviceLoginPoll at internal/tui/provider_wizard_oauth_test.go:224-225 and TestModelQuitCancelsProviderWizardDeviceLoginPoll at internal/tui/provider_wizard_oauth_test.go:270-271 to call isolateKimiDeviceIDStorage(t) instead of setting only XDG_CONFIG_HOME, while retaining the explicit ZERO_OAUTH_TOKENS_PATH setup in both tests.Source: Coding guidelines
internal/providercatalog/catalog.go (1)
416-433: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value
RuntimeHeadersis silently ignored when a descriptor also declaresCustomHeaders.The
else ifmakes the two sources mutually exclusive. Today onlykimi-codesetsRuntimeHeadersand it has no staticCustomHeaders, so behavior is correct. A future descriptor that needs both (for example static attribution plus device identity) would silently drop the runtime headers. Either merge the two maps or state the exclusion in theRuntimeHeadersdoc comment on theDescriptorfield.♻️ Optional: merge instead of exclude
- if descriptor.CustomHeaders != nil { - descriptor.CustomHeaders = copyStringMap(descriptor.CustomHeaders) - } else if withRuntimeHeaders && descriptor.RuntimeHeaders != nil { - descriptor.CustomHeaders = descriptor.RuntimeHeaders() - } + merged := map[string]string{} + if withRuntimeHeaders && descriptor.RuntimeHeaders != nil { + merged = descriptor.RuntimeHeaders() + } + for key, value := range descriptor.CustomHeaders { + merged[key] = value + } + if len(merged) == 0 { + merged = nil + } + descriptor.CustomHeaders = merged🤖 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 416 - 433, Update cloneDescriptor so withRuntimeHeaders applies descriptor.RuntimeHeaders even when CustomHeaders is already present, merging both maps while preserving the descriptor’s static headers and allowing runtime values to be included. Keep the existing deep-copy behavior and avoid mutating the original descriptor.internal/tui/provider_wizard.go (1)
143-149: 🩺 Stability & Availability | 🔵 Trivial | 💤 Low valueCancel any stored poll context before you overwrite
deviceLoginCancel.
applyProviderWizardDeviceCodeassigns a new cancel func without releasing a previous one.oauthResultMatchesstays true for the sameattemptID, so a duplicateproviderWizardDeviceCodeMsgfor one attempt would drop the earlier cancel func and leak that context until the parentm.ctxends. One defensive call keeps the invariant local.♻️ Optional guard
+ m.providerWizard.cancelDeviceLogin() ctx, cancel := context.WithCancel(m.ctx) m.providerWizard.deviceLoginCancel = cancel🤖 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/provider_wizard.go` around lines 143 - 149, In applyProviderWizardDeviceCode, invoke the existing deviceLoginCancel function before assigning the newly created cancel function, guarding against nil when no poll is active. Preserve the current context creation and assignment flow so duplicate providerWizardDeviceCodeMsg events cannot orphan the previous poll context.
🤖 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-92: Add regression tests for the auth “kimi” case covering
forwarding to runAuthLogin with the kimi-code preset, --help producing parser
help without starting authorization, and unknown flags or extra positional
arguments being rejected consistently with auth login kimi-code. Reuse existing
CLI auth test helpers and assert both outcomes and relevant output or errors.
In `@internal/oauth/presets.go`:
- Around line 162-189: Update isCanonicalKimiHost to require an HTTPS scheme
before accepting auth.kimi.com or api.kimi.com; continue rejecting malformed
URLs and all other hosts, while preserving case-insensitive scheme and hostname
handling.
In `@internal/tui/model.go`:
- Around line 1077-1080: add a regression test for model.quit() with
setup.deviceLoginCancel active, invoke the direct quit path, and assert that the
device-login poll command returns context.Canceled. Keep the existing Ctrl+C
test unchanged and ensure the new test specifically covers direct exit
cancellation.
---
Duplicate comments:
In `@internal/kimiidentity/kimiidentity.go`:
- Around line 288-302: The reclaimDeadRepairLock flow must remain rooted in the
provided *os.Root instead of reconstructing paths from root.Name(). Replace or
extend lockutil.ReclaimStaleLock so reclamation uses root.Rename, root.ReadFile,
and root.Remove for every lock operation, including the lockHolderAlive
callback, while preserving the existing stale-lock and fail-closed behavior.
In `@internal/kimiidentity/process_alive_windows_test.go`:
- Around line 22-32: Update both liveness tests in
internal/kimiidentity/process_alive_windows_test.go: lines 22-32 should open and
retain a SYNCHRONIZE handle before cmd.Wait(), use it during processAlive(pid),
then close it and remove the no-op cmd.Process reference; lines 65-70 should
open the handle before cmd.Process.Kill(), wait on it, assert processAlive(pid)
while retained, then close it before calling cmd.Wait().
---
Nitpick comments:
In `@internal/providercatalog/catalog.go`:
- Around line 416-433: Update cloneDescriptor so withRuntimeHeaders applies
descriptor.RuntimeHeaders even when CustomHeaders is already present, merging
both maps while preserving the descriptor’s static headers and allowing runtime
values to be included. Keep the existing deep-copy behavior and avoid mutating
the original descriptor.
In `@internal/tui/provider_wizard_oauth_test.go`:
- Around line 224-225: Update TestProviderWizardEscCancelsDeviceLoginPoll at
internal/tui/provider_wizard_oauth_test.go:224-225 and
TestModelQuitCancelsProviderWizardDeviceLoginPoll at
internal/tui/provider_wizard_oauth_test.go:270-271 to call
isolateKimiDeviceIDStorage(t) instead of setting only XDG_CONFIG_HOME, while
retaining the explicit ZERO_OAUTH_TOKENS_PATH setup in both tests.
In `@internal/tui/provider_wizard.go`:
- Around line 143-149: In applyProviderWizardDeviceCode, invoke the existing
deviceLoginCancel function before assigning the newly created cancel function,
guarding against nil when no poll is active. Preserve the current context
creation and assignment flow so duplicate providerWizardDeviceCodeMsg events
cannot orphan the previous poll context.
🪄 Autofix
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: e85910e7-4876-484c-b83e-40b5347d9356
📒 Files selected for processing (31)
docs/oauth-subscriptions.mdinternal/cli/auth.gointernal/config/resolver.gointernal/config/resolver_test.gointernal/kimiidentity/export_test.gointernal/kimiidentity/kimiidentity.gointernal/kimiidentity/kimiidentity_test.gointernal/kimiidentity/process_alive_posix.gointernal/kimiidentity/process_alive_windows.gointernal/kimiidentity/process_alive_windows_test.gointernal/oauth/device.gointernal/oauth/device_test.gointernal/oauth/flow.gointernal/oauth/flow_test.gointernal/oauth/manager.gointernal/oauth/oauth.gointernal/oauth/presets.gointernal/oauth/presets_test.gointernal/oauth/providers.gointernal/oauth/providers_test.gointernal/providercatalog/catalog.gointernal/providercatalog/catalog_test.gointernal/providercatalog/export_test.gointernal/providercatalog/oauth_test.gointernal/tui/model.gointernal/tui/oauth_device.gointernal/tui/onboarding.gointernal/tui/onboarding_test.gointernal/tui/provider_wizard.gointernal/tui/provider_wizard_discovery.gointernal/tui/provider_wizard_oauth_test.go
| case "kimi": | ||
| // Kimi Code is a standard device-code OAuth preset (no bespoke client | ||
| // like ChatGPT's Codex flow), so it reuses the generic `auth login` | ||
| // engine — which already opts into presets and resolves the baked-in | ||
| // kimi-code client_id/endpoints. `zero auth kimi` is sugar for | ||
| // `zero auth login kimi-code`, forwarding whatever flags the caller | ||
| // passed after "kimi" (--device, --scope, --help) through the real | ||
| // parser instead of discarding them: `zero auth kimi --help` must show | ||
| // help, not silently start a real device authorization, and an unknown | ||
| // flag or extra positional must be rejected the same way `zero auth | ||
| // login` rejects one. | ||
| return runAuthLogin(append([]string{"kimi-code"}, args[1:]...), stdout, stderr, deps) |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
Add regression tests for zero auth kimi.
Cover alias forwarding, zero auth kimi --help, and invalid flags. These cases protect the promised parity with zero auth login kimi-code.
As per coding guidelines: “Every behavior or security-boundary change requires a regression test, including failure paths.”
🤖 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/cli/auth.go` around lines 81 - 92, Add regression tests for the auth
“kimi” case covering forwarding to runAuthLogin with the kimi-code preset,
--help producing parser help without starting authorization, and unknown flags
or extra positional arguments being rejected consistently with auth login
kimi-code. Reuse existing CLI auth test helpers and assert both outcomes and
relevant output or errors.
Source: Coding guidelines
| func providerExtraHeaders(name string, overriddenEndpoints ...string) map[string]string { | ||
| if strings.ToLower(strings.TrimSpace(name)) == "kimi-code" { | ||
| for _, ep := range overriddenEndpoints { | ||
| if ep = strings.TrimSpace(ep); ep != "" && !isCanonicalKimiHost(ep) { | ||
| return nil | ||
| } | ||
| } | ||
| return kimiExtraHeaders() | ||
| } | ||
| return nil | ||
| } | ||
|
|
||
| // isCanonicalKimiHost reports whether urlStr's host is an explicitly approved | ||
| // Kimi OAuth/API endpoint. Only the hosts this package's preset and managed | ||
| // coding path talk to are allowed; arbitrary *.kimi.com / *.moonshot.cn | ||
| // subdomains must not receive the persistent device identity. | ||
| func isCanonicalKimiHost(urlStr string) bool { | ||
| u, err := url.Parse(urlStr) | ||
| if err != nil || u.Hostname() == "" { | ||
| return false | ||
| } | ||
| switch strings.ToLower(u.Hostname()) { | ||
| case "auth.kimi.com", "api.kimi.com": | ||
| return true | ||
| default: | ||
| return false | ||
| } | ||
| } |
There was a problem hiding this comment.
🔒 Security & Privacy | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# List every providerExtraHeaders call site and its arguments.
rg -nP -C 6 '\bproviderExtraHeaders\s*\(' --type=go
# Confirm the allowlist tests cover scheme variants.
rg -nP -C 3 'isCanonicalKimiHost|http://auth\.kimi\.com' --type=goRepository: Gitlawb/zero
Length of output: 150
🏁 Script executed:
#!/bin/bash
set -u
echo "== locate relevant files =="
fd -a 'presets\.go$|kimi|oauth' . | sed 's#^\./##' | head -200
echo
echo "== search providerExtraHeaders usages =="
rg -n -C 7 'providerExtraHeaders\s*\(' . || true
echo
echo "== search isCanonicalKimiHost usages =="
rg -n -C 4 'isCanonicalKimiHost\s*\(' . || true
echo
echo "== direct git ls-files relevant =="
git ls-files | rg '(^|/)internal/oauth/presets\.go$|oauth|kimi|moonshot|presets' || trueRepository: Gitlawb/zero
Length of output: 8700
🏁 Script executed:
#!/bin/bash
set -u
echo "== internal/oauth/presets.go relevant section =="
sed -n '18,200p' internal/oauth/presets.go
echo
echo "== internal/oauth/providers.go relevant section =="
sed -n '60,95p' internal/oauth/providers.go
echo
echo "== internal/oauth/providers_test.go relevant tests =="
sed -n '120,175p' internal/oauth/providers_test.go
echo
echo "== read-only verifier: isCanonicalKimiHost behavior for scheme/host variants =="
python3 - <<'PY'
from urllib.parse import urlparse
host_allowlist = {"auth.kimi.com", "api.kimi.com"}
def proposed(urlStr):
u = urlparse(urlStr)
if not u.scheme or not u.netloc:
return False
if u.scheme.lower() != "https":
return False
return u.hostname and u.hostname.lower() in host_allowlist
def current(urlStr):
u = urlparse(urlStr)
if not u.netloc:
return False
return bool(u.hostname and u.hostname.lower() in host_allowlist)
cases = [
"https://auth.kimi.com/oauth",
"http://auth.kimi.com/oauth",
"FTP://AUTH.KIMI.COM/oauth",
"https://evil.kimi.com/oauth",
"auth.kimi.com/oauth",
"",
]
for case in cases:
print(f"{case!r}: current={current(case)} proposed={proposed(case)}")
PYRepository: Gitlawb/zero
Length of output: 13589
Reject non-HTTPS Kimi URLs in isCanonicalKimiHost
providerExtraHeaders passes four overridden endpoints, so the call-site issue does not apply. However, isCanonicalKimiHost only checks the hostname, so http://auth.kimi.com/..., FTP://AUTH.KIMI.COM/..., and other non-HTTPS URLs currently receive X-Msh-* device identity headers. Add !strings.EqualFold(u.Scheme, "https") before returning true.
🤖 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 - 189, Update isCanonicalKimiHost
to require an HTTPS scheme before accepting auth.kimi.com or api.kimi.com;
continue rejecting malformed URLs and all other hosts, while preserving
case-insensitive scheme and hostname handling.
Source: Coding guidelines
| m.providerWizard.cancelDeviceLogin() | ||
| m.providerWizard.resetAimlapiOnboard() | ||
| } | ||
| m.setup.cancelDeviceLogin() |
There was a problem hiding this comment.
🔒 Security & Privacy | 🟠 Major | ⚡ Quick win
Add a direct quit-cancellation regression test.
Test model.quit() with an active setup.deviceLoginCancel. Assert that the poll command returns context.Canceled. The current Ctrl+C test does not exercise this direct exit path.
As per coding guidelines: “Every behavior or security-boundary change requires a regression test, including failure paths.”
🤖 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/model.go` around lines 1077 - 1080, add a regression test for
model.quit() with setup.deviceLoginCancel active, invoke the direct quit path,
and assert that the device-login poll command returns context.Canceled. Keep the
existing Ctrl+C test unchanged and ensure the new test specifically covers
direct exit cancellation.
Source: Coding guidelines
Summary
X-Msh-*vendor-identity headersinternal/kimiidentitypackage 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 endpointzero 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 pathTest plan
go test ./internal/kimiidentity/... ./internal/oauth/... ./internal/providercatalog/... ./internal/tui/... ./internal/cli/...Summary by CodeRabbit
New Features
zero auth kimilogin command.Bug Fixes