-
Notifications
You must be signed in to change notification settings - Fork 911
fix: scope managed OAuth credentials #399
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,8 @@ | ||
| --- | ||
| "@moonshot-ai/agent-core": patch | ||
| "@moonshot-ai/kimi-code-oauth": patch | ||
| "@moonshot-ai/kimi-code-sdk": patch | ||
| "@moonshot-ai/kimi-code": patch | ||
| --- | ||
|
|
||
| Keep managed OAuth credentials scoped to their configured authentication and API endpoints. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,96 @@ | ||
| import { | ||
| KIMI_CODE_PROVIDER_NAME, | ||
| resolveKimiCodeOAuthKey, | ||
| resolveKimiCodeOAuthRef, | ||
| } from '@moonshot-ai/kimi-code-oauth'; | ||
| import { afterEach, describe, expect, it, vi } from 'vitest'; | ||
|
|
||
| import { refreshAllProviderModels } from '../../../src/tui/utils/refresh-providers'; | ||
| import type { KimiConfig } from '@moonshot-ai/kimi-code-sdk'; | ||
|
|
||
| type FetchMock = ( | ||
| input: Parameters<typeof fetch>[0], | ||
| init?: Parameters<typeof fetch>[1], | ||
| ) => Promise<Response>; | ||
|
|
||
| function fetchInputUrl(input: Parameters<typeof fetch>[0]): string { | ||
| if (typeof input === 'string') return input; | ||
| if (input instanceof URL) return input.href; | ||
| return input.url; | ||
| } | ||
|
|
||
| describe('refreshAllProviderModels', () => { | ||
| afterEach(() => { | ||
| vi.unstubAllEnvs(); | ||
| vi.unstubAllGlobals(); | ||
| }); | ||
|
|
||
| it('refreshes managed Kimi Code against environment endpoints over persisted config', async () => { | ||
| const configuredBaseUrl = 'https://api.configured.example.test/coding/v1'; | ||
| const envBaseUrl = 'https://api.env.example.test/coding/v1'; | ||
| const envOauthHost = 'https://auth.env.example.test'; | ||
| const configuredOauthKey = resolveKimiCodeOAuthKey({ baseUrl: configuredBaseUrl }); | ||
| const envOauthRef = resolveKimiCodeOAuthRef({ | ||
| oauthHost: envOauthHost, | ||
| baseUrl: envBaseUrl, | ||
| }); | ||
| const config: KimiConfig = { | ||
| providers: { | ||
| [KIMI_CODE_PROVIDER_NAME]: { | ||
| type: 'kimi', | ||
| baseUrl: configuredBaseUrl, | ||
| apiKey: '', | ||
| oauth: { | ||
| storage: 'file', | ||
| key: configuredOauthKey, | ||
| oauthHost: 'https://auth.kimi.com', | ||
| }, | ||
| }, | ||
| }, | ||
| models: { | ||
| 'kimi-code/kimi-for-coding': { | ||
| provider: KIMI_CODE_PROVIDER_NAME, | ||
| model: 'kimi-for-coding', | ||
| maxContextSize: 262144, | ||
| }, | ||
| }, | ||
| defaultModel: 'kimi-code/kimi-for-coding', | ||
| telemetry: true, | ||
| }; | ||
| vi.stubEnv('KIMI_CODE_BASE_URL', envBaseUrl); | ||
| vi.stubEnv('KIMI_CODE_OAUTH_HOST', envOauthHost); | ||
| const resolveOAuthToken = vi.fn(async (_providerName, oauthRef) => { | ||
| expect(oauthRef).toEqual(envOauthRef); | ||
| return 'env-access-token'; | ||
| }); | ||
| const fetchMock = vi.fn<FetchMock>(async (input, init) => { | ||
| expect(fetchInputUrl(input)).toBe(`${envBaseUrl}/models`); | ||
| expect(new Headers(init?.headers).get('authorization')).toBe('Bearer env-access-token'); | ||
| return new Response( | ||
| JSON.stringify({ | ||
| data: [ | ||
| { | ||
| id: 'kimi-for-coding', | ||
| context_length: 262144, | ||
| supports_reasoning: true, | ||
| }, | ||
| ], | ||
| }), | ||
| { status: 200, headers: { 'Content-Type': 'application/json' } }, | ||
| ); | ||
| }); | ||
| vi.stubGlobal('fetch', fetchMock); | ||
|
|
||
| const result = await refreshAllProviderModels({ | ||
| getConfig: async () => config, | ||
| removeProvider: vi.fn(), | ||
| setConfig: vi.fn(), | ||
| resolveOAuthToken, | ||
| }); | ||
|
|
||
| expect(result.failed).toEqual([]); | ||
| expect(result.unchanged).toEqual([KIMI_CODE_PROVIDER_NAME]); | ||
| expect(fetchMock).toHaveBeenCalledTimes(1); | ||
| expect(resolveOAuthToken).toHaveBeenCalledWith(KIMI_CODE_PROVIDER_NAME, envOauthRef); | ||
| }); | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -4,6 +4,8 @@ import { | |
| applyManagedKimiCodeLogoutConfig, | ||
| KIMI_CODE_PROVIDER_NAME, | ||
| KimiOAuthToolkit, | ||
| resolveKimiCodeLoginAuth, | ||
| resolveKimiCodeRuntimeAuth, | ||
| type AuthManagedUsageResult, | ||
| type AuthStatus, | ||
| type BearerTokenProvider, | ||
|
|
@@ -68,14 +70,27 @@ export class KimiAuthFacade { | |
| } | ||
|
|
||
| async status(providerName?: string | undefined): Promise<AuthStatus> { | ||
| return this.toolkit.status(providerName); | ||
| return this.toolkit.status(providerName, this.resolveRuntimeManagedAuth(providerName).oauthRef); | ||
| } | ||
|
|
||
| async login( | ||
| providerName: string | undefined = KIMI_CODE_PROVIDER_NAME, | ||
| options: KimiAuthLoginOptions = {}, | ||
| ): Promise<KimiAuthLoginResult> { | ||
| const result = await this.toolkit.login(providerName, { ...options, provisionConfig: true }); | ||
| const auth = this.resolveManagedAuth(providerName); | ||
| const loginAuth = resolveKimiCodeLoginAuth({ | ||
| configuredBaseUrl: auth.baseUrl, | ||
| configuredOAuthRef: auth.oauthRef, | ||
| requestedBaseUrl: options.baseUrl, | ||
| requestedOAuthHost: options.oauthHost, | ||
| }); | ||
| const result = await this.toolkit.login(providerName, { | ||
| ...options, | ||
| baseUrl: loginAuth.baseUrl, | ||
| oauthHost: loginAuth.oauthHost, | ||
| oauthRef: options.oauthRef ?? loginAuth.oauthRef, | ||
| provisionConfig: true, | ||
| }); | ||
| if (result.provision === undefined) { | ||
| throw new Error('Kimi auth login did not provision model config.'); | ||
| } | ||
|
|
@@ -91,7 +106,10 @@ export class KimiAuthFacade { | |
| } | ||
|
|
||
| async logout(providerName?: string | undefined): Promise<KimiAuthLogoutResult> { | ||
| const result = await this.toolkit.logout(providerName); | ||
| const result = await this.toolkit.logout( | ||
| providerName, | ||
| this.resolveRuntimeManagedAuth(providerName).oauthRef, | ||
| ); | ||
|
Comment on lines
+109
to
+112
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
For an existing pre-scoping config whose Useful? React with 👍 / 👎. |
||
| const updated = readConfigFile(this.options.configPath); | ||
| this.options.onConfigUpdated?.(updated); | ||
| return { | ||
|
|
@@ -101,13 +119,18 @@ export class KimiAuthFacade { | |
| } | ||
|
|
||
| async getManagedUsage(providerName?: string | undefined): Promise<AuthManagedUsageResult> { | ||
| return this.toolkit.getManagedUsage(providerName); | ||
| const auth = this.resolveRuntimeManagedAuth(providerName); | ||
| return this.toolkit.getManagedUsage(providerName, { | ||
| oauthRef: auth.oauthRef, | ||
| baseUrl: auth.baseUrl, | ||
|
Comment on lines
+123
to
+125
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When Useful? React with 👍 / 👎. |
||
| }); | ||
| } | ||
|
|
||
| async submitFeedback( | ||
| input: KimiAuthSubmitFeedbackInput, | ||
| providerName?: string | undefined, | ||
| ): Promise<FetchSubmitFeedbackResult> { | ||
| const auth = this.resolveRuntimeManagedAuth(providerName); | ||
| return this.toolkit.submitFeedback( | ||
| { | ||
| session_id: input.sessionId, | ||
|
|
@@ -117,17 +140,63 @@ export class KimiAuthFacade { | |
| model: input.model, | ||
| }, | ||
| providerName, | ||
| { | ||
| oauthRef: auth.oauthRef, | ||
| baseUrl: auth.baseUrl, | ||
| }, | ||
| ); | ||
| } | ||
|
|
||
| async getCachedAccessToken(providerName?: string): Promise<string | undefined> { | ||
| return this.toolkit.getCachedAccessToken(providerName); | ||
| async getCachedAccessToken( | ||
| providerName?: string, | ||
| oauthRef?: OAuthRef | undefined, | ||
| ): Promise<string | undefined> { | ||
| return this.toolkit.getCachedAccessToken( | ||
| providerName, | ||
| this.runtimeOAuthRef(providerName, oauthRef), | ||
| ); | ||
| } | ||
|
|
||
| readonly resolveOAuthTokenProvider = ( | ||
| providerName: string, | ||
| oauthRef?: OAuthRef | undefined, | ||
| ): BearerTokenProvider => { | ||
| return this.toolkit.tokenProvider(providerName, oauthRef); | ||
| return this.toolkit.tokenProvider(providerName, this.runtimeOAuthRef(providerName, oauthRef)); | ||
| }; | ||
|
|
||
| private resolveManagedAuth(providerName?: string | undefined): { | ||
| readonly oauthRef?: OAuthRef | undefined; | ||
| readonly baseUrl?: string | undefined; | ||
| } { | ||
| const name = providerName ?? KIMI_CODE_PROVIDER_NAME; | ||
| const config = readConfigFile(this.options.configPath); | ||
| const provider = config.providers[name]; | ||
| return { | ||
| oauthRef: provider?.oauth, | ||
| baseUrl: provider?.baseUrl, | ||
| }; | ||
| } | ||
|
|
||
| private resolveRuntimeManagedAuth(providerName?: string | undefined): { | ||
| readonly oauthRef: OAuthRef; | ||
| readonly baseUrl?: string | undefined; | ||
| } { | ||
| const auth = this.resolveManagedAuth(providerName); | ||
| return resolveKimiCodeRuntimeAuth({ | ||
| configuredBaseUrl: auth.baseUrl, | ||
| configuredOAuthRef: auth.oauthRef, | ||
| }); | ||
| } | ||
|
|
||
| private runtimeOAuthRef( | ||
| providerName: string | undefined, | ||
| oauthRef?: OAuthRef | undefined, | ||
| ): OAuthRef | undefined { | ||
| if ((providerName ?? KIMI_CODE_PROVIDER_NAME) !== KIMI_CODE_PROVIDER_NAME) return oauthRef; | ||
| const auth = this.resolveManagedAuth(providerName); | ||
| return resolveKimiCodeRuntimeAuth({ | ||
| configuredBaseUrl: auth.baseUrl, | ||
| configuredOAuthRef: oauthRef ?? auth.oauthRef, | ||
| }).oauthRef; | ||
|
Comment on lines
+197
to
+200
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When a Useful? React with 👍 / 👎. |
||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When
KIMI_CODE_BASE_URL/KIMI_CODE_OAUTH_HOSTare set temporarily over an existing managed config,resolveRuntimeManagedAuth()returns the env-scoped OAuth ref, so this logs out the override token, butKimiOAuthToolkit.logout()still unconditionally runs the config adapter'sremove()formanaged:kimi-code. A user who only meant to clear the temporary environment credential therefore loses the persisted provider/models for the configured environment while that configured token remains untouched; use the persisted config ref for config removal, or skip config removal when the runtime ref was produced by env overrides.Useful? React with 👍 / 👎.