diff --git a/packages/agent-core/src/services/auth/managedAuth.ts b/packages/agent-core/src/services/auth/managedAuth.ts index 04ddbb1c8..23bd9d096 100644 --- a/packages/agent-core/src/services/auth/managedAuth.ts +++ b/packages/agent-core/src/services/auth/managedAuth.ts @@ -9,6 +9,7 @@ import { resolveKimiCodeLoginAuth, resolveKimiCodeRuntimeAuth, type BearerTokenProvider, + type KimiHostIdentity, type KimiOAuthLoginOptions, type ManagedKimiConfigShape, } from '@moonshot-ai/kimi-code-oauth'; @@ -50,9 +51,11 @@ class ServicesManagedAuthFacade implements ServicesAuthFacade { constructor( private readonly options: Pick, + identity?: KimiHostIdentity, ) { this.toolkit = new KimiOAuthToolkit({ homeDir: options.homeDir, + identity, configAdapter: { configPath: options.configPath, read: () => readConfigFile(options.configPath) as ServicesManagedConfig, @@ -169,6 +172,7 @@ class ServicesManagedAuthFacade implements ServicesAuthFacade { export function createManagedAuthFacade( env: Pick, + identity?: KimiHostIdentity, ): ServicesAuthFacade { - return new ServicesManagedAuthFacade(env); + return new ServicesManagedAuthFacade(env, identity); } diff --git a/packages/agent-core/src/services/authSummary/authSummaryService.ts b/packages/agent-core/src/services/authSummary/authSummaryService.ts index e288d2e9e..b9a3c5af1 100644 --- a/packages/agent-core/src/services/authSummary/authSummaryService.ts +++ b/packages/agent-core/src/services/authSummary/authSummaryService.ts @@ -30,7 +30,7 @@ export class AuthSummaryService @ICoreProcessService private readonly core: ICoreProcessService, ) { super(); - this._authFacade = createManagedAuthFacade(env); + this._authFacade = createManagedAuthFacade(env, env.identity); } async get(): Promise { diff --git a/packages/agent-core/src/services/coreProcess/coreProcessService.ts b/packages/agent-core/src/services/coreProcess/coreProcessService.ts index 970adb517..e88013325 100644 --- a/packages/agent-core/src/services/coreProcess/coreProcessService.ts +++ b/packages/agent-core/src/services/coreProcess/coreProcessService.ts @@ -92,7 +92,11 @@ export class CoreProcessService extends Disposable implements ICoreProcessServic // tests) can still override via `options.resolveOAuthTokenProvider`. const resolveOAuthTokenProvider: OAuthTokenProviderResolver = options.resolveOAuthTokenProvider ?? - CoreProcessService._defaultOAuthTokenResolver(env.homeDir, env.configPath); + CoreProcessService._defaultOAuthTokenResolver( + env.homeDir, + env.configPath, + options.identity, + ); // Default-wire the Kimi request headers (User-Agent + X-Msh-* device // identity). Without this, KimiCore's outbound fetch carries the @@ -212,14 +216,18 @@ export class CoreProcessService extends Disposable implements ICoreProcessServic * runtimes share OAuth credentials when both run against the same * `~/.kimi-code`. * + * `identity` is forwarded to the managed auth facade so token refreshes + * carry the same `X-Msh-*` device headers as `_defaultKimiRequestHeaders`. + * * Exposed as `static` so tests can assert the wiring without exercising the * full agent-core turn loop. */ static _defaultOAuthTokenResolver( homeDir: string, configPath: string, + identity?: KimiHostIdentity, ): OAuthTokenProviderResolver { - const facade = createManagedAuthFacade({ homeDir, configPath }); + const facade = createManagedAuthFacade({ homeDir, configPath }, identity); return facade.resolveOAuthTokenProvider; } diff --git a/packages/agent-core/src/services/environment/environment.ts b/packages/agent-core/src/services/environment/environment.ts index a8eea68f1..18c58ae82 100644 --- a/packages/agent-core/src/services/environment/environment.ts +++ b/packages/agent-core/src/services/environment/environment.ts @@ -1,7 +1,7 @@ /** * `IEnvironmentService` — canonical source for resolved filesystem paths - * (home directory, config file, etc.) used by the daemon and in-process - * services. + * (home directory, config file, etc.) and the host identity used by the + * daemon and in-process services. * * VSCode-style: injected via `@IEnvironmentService` rather than passed as * a static options prefix. This eliminates the "options bag as first ctor @@ -9,6 +9,7 @@ */ import { createDecorator } from '../../di'; +import type { KimiHostIdentity } from '@moonshot-ai/kimi-code-oauth'; export interface IEnvironmentService { readonly _serviceBrand: undefined; @@ -16,6 +17,14 @@ export interface IEnvironmentService { readonly homeDir: string; /** Resolved absolute path to `config.toml`. */ readonly configPath: string; + /** + * Host identity handed to the managed OAuth toolkit so device-flow and + * token-refresh requests carry `X-Msh-*` headers. Optional: v1 is a + * library layer and "no identity, no headers" is the oauth package + * contract; real hosts always set it. Rides on this bag because the DI + * registry has no options channel that reaches OAuthService & co. + */ + readonly identity?: KimiHostIdentity; } export const IEnvironmentService = createDecorator( diff --git a/packages/agent-core/src/services/modelCatalog/modelCatalogService.ts b/packages/agent-core/src/services/modelCatalog/modelCatalogService.ts index db70fdebf..2be0f5c14 100644 --- a/packages/agent-core/src/services/modelCatalog/modelCatalogService.ts +++ b/packages/agent-core/src/services/modelCatalog/modelCatalogService.ts @@ -44,7 +44,7 @@ export class ModelCatalogService @IEventService private readonly eventService: IEventService, ) { super(); - this._authFacade = createManagedAuthFacade(env); + this._authFacade = createManagedAuthFacade(env, env.identity); } static _createForTest( diff --git a/packages/agent-core/src/services/oauth/oauthService.ts b/packages/agent-core/src/services/oauth/oauthService.ts index 055556993..c098005d1 100644 --- a/packages/agent-core/src/services/oauth/oauthService.ts +++ b/packages/agent-core/src/services/oauth/oauthService.ts @@ -83,7 +83,7 @@ export class OAuthService extends Disposable implements IOAuthService { constructor(@IEnvironmentService private readonly env: IEnvironmentService) { super(); this._flows = this._register(new DisposableMap()); - this._authFacade = createManagedAuthFacade(env); + this._authFacade = createManagedAuthFacade(env, env.identity); } /** @internal Test-only factory that injects a mock facade. */ diff --git a/packages/agent-core/test/services/coreProcessService.test.ts b/packages/agent-core/test/services/coreProcessService.test.ts index 60dbd7e49..86e637011 100644 --- a/packages/agent-core/test/services/coreProcessService.test.ts +++ b/packages/agent-core/test/services/coreProcessService.test.ts @@ -1,8 +1,8 @@ -import { mkdtempSync, rmSync } from 'node:fs'; +import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; -import { afterEach, beforeEach, describe, expect, it } from 'vitest'; +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { SyncDescriptor, @@ -131,6 +131,7 @@ beforeEach(() => { }); afterEach(() => { + vi.unstubAllGlobals(); if (prevHome === undefined) { delete process.env['KIMI_HOME']; } else { @@ -273,6 +274,46 @@ describe('CoreProcessService direct construction', () => { expect(typeof tokenProvider?.getAccessToken).toBe('function'); }); + it('threads identity into the default resolver so refreshes carry X-Msh-Platform', async () => { + const credentialsDir = join(tmpHome, 'credentials'); + mkdirSync(credentialsDir, { recursive: true }); + writeFileSync( + join(credentialsDir, 'kimi-code.json'), + JSON.stringify({ + access_token: 'expired-access', + refresh_token: 'refresh-1', + expires_at: 1, + scope: '', + token_type: 'Bearer', + expires_in: 3600, + }), + ); + const refreshHeaders: Record[] = []; + vi.stubGlobal('fetch', async (_input: unknown, init?: RequestInit) => { + refreshHeaders.push((init?.headers ?? {}) as Record); + return new Response( + JSON.stringify({ + access_token: 'rotated-access', + refresh_token: 'rotated-refresh', + expires_in: 3600, + scope: '', + token_type: 'Bearer', + }), + { status: 200, headers: { 'Content-Type': 'application/json' } }, + ); + }); + + const resolver = CoreProcessService._defaultOAuthTokenResolver( + tmpHome, + join(tmpHome, 'config.toml'), + { productName: 'test', version: '0.0.0-test', platform: 'test_platform' }, + ); + const tokenProvider = resolver('managed:kimi-code'); + await expect(tokenProvider?.getAccessToken()).resolves.toBe('rotated-access'); + expect(refreshHeaders).toHaveLength(1); + expect(refreshHeaders[0]?.['X-Msh-Platform']).toBe('test_platform'); + }); + it('default-wires kimiRequestHeaders from identity when caller omits headers', () => { const headers = CoreProcessService._defaultKimiRequestHeaders( tmpHome,