mirror of
https://github.com/MoonshotAI/kimi-code.git
synced 2026-08-01 12:34:55 +00:00
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.
This commit is contained in:
parent
5e81a392fd
commit
a544ff9fdb
7 changed files with 72 additions and 10 deletions
|
|
@ -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<IEnvironmentService, 'homeDir' | 'configPath'>,
|
||||
identity?: KimiHostIdentity,
|
||||
) {
|
||||
this.toolkit = new KimiOAuthToolkit<ServicesManagedConfig>({
|
||||
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<IEnvironmentService, 'homeDir' | 'configPath'>,
|
||||
identity?: KimiHostIdentity,
|
||||
): ServicesAuthFacade {
|
||||
return new ServicesManagedAuthFacade(env);
|
||||
return new ServicesManagedAuthFacade(env, identity);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<AuthSummary> {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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<IEnvironmentService>(
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -83,7 +83,7 @@ export class OAuthService extends Disposable implements IOAuthService {
|
|||
constructor(@IEnvironmentService private readonly env: IEnvironmentService) {
|
||||
super();
|
||||
this._flows = this._register(new DisposableMap<string, FlowState>());
|
||||
this._authFacade = createManagedAuthFacade(env);
|
||||
this._authFacade = createManagedAuthFacade(env, env.identity);
|
||||
}
|
||||
|
||||
/** @internal Test-only factory that injects a mock facade. */
|
||||
|
|
|
|||
|
|
@ -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<string, string>[] = [];
|
||||
vi.stubGlobal('fetch', async (_input: unknown, init?: RequestInit) => {
|
||||
refreshHeaders.push((init?.headers ?? {}) as Record<string, string>);
|
||||
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,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue