From a544ff9fdbf30dc98dfd50dc7e83787f2c518ccd Mon Sep 17 00:00:00 2001 From: liruifengv Date: Thu, 30 Jul 2026 01:38:44 +0800 Subject: [PATCH] feat(agent-core): thread the host identity into the managed auth facades The v1 managed auth facade constructed its OAuth toolkit without an identity, so token refreshes from inside the core went out without any X-Msh-* device headers. createManagedAuthFacade now takes an optional KimiHostIdentity and every call site supplies one: CoreProcessService._defaultOAuthTokenResolver forwards the core process's options.identity (the same source _defaultKimiRequestHeaders uses), and the DI-held services (oauth / auth summary / model catalog) read it from a new optional identity field on IEnvironmentService. The library-level "no identity, no device headers" contract is unchanged. --- .../src/services/auth/managedAuth.ts | 6 ++- .../authSummary/authSummaryService.ts | 2 +- .../coreProcess/coreProcessService.ts | 12 ++++- .../src/services/environment/environment.ts | 13 +++++- .../modelCatalog/modelCatalogService.ts | 2 +- .../src/services/oauth/oauthService.ts | 2 +- .../test/services/coreProcessService.test.ts | 45 ++++++++++++++++++- 7 files changed, 72 insertions(+), 10 deletions(-) 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,