refactor(services,protocol): remove node-sdk dependency

- Replace `@moonshot-ai/kimi-code-sdk` with `@moonshot-ai/agent-core` in protocol\n- Remove `@moonshot-ai/kimi-code-sdk` from services dependencies\n- Introduce internal `managedAuth` facade in services to replace `KimiAuthFacade`\n- Add compile-time assertions that neither package references the node SDK
This commit is contained in:
haozhe.yang 2026-06-10 12:16:36 +08:00
parent 38d87b892f
commit be2eb046b7
15 changed files with 245 additions and 60 deletions

View file

@ -0,0 +1,7 @@
---
"@moonshot-ai/services": patch
"@moonshot-ai/protocol": patch
"@moonshot-ai/kimi-code": patch
---
Remove daemon service and protocol package dependencies on the node SDK.

View file

@ -35,7 +35,6 @@
"dependencies": {
"@moonshot-ai/agent-core": "workspace:^",
"@moonshot-ai/kimi-code-oauth": "workspace:^",
"@moonshot-ai/kimi-code-sdk": "workspace:^",
"@moonshot-ai/protocol": "workspace:^",
"chokidar": "^4.0.3",
"ignore": "^5.3.2",

View file

@ -0,0 +1,178 @@
import {
readConfigFile,
writeConfigFile,
type KimiConfig,
type OAuthRef,
type OAuthTokenProviderResolver,
} from '@moonshot-ai/agent-core';
import {
applyManagedKimiCodeConfig,
applyManagedKimiCodeLogoutConfig,
KIMI_CODE_PROVIDER_NAME,
KimiOAuthToolkit,
resolveKimiCodeLoginAuth,
resolveKimiCodeRuntimeAuth,
type BearerTokenProvider,
type KimiOAuthLoginOptions,
type ManagedKimiConfigShape,
} from '@moonshot-ai/kimi-code-oauth';
import type { IEnvironmentService } from '../environment/environment';
type ServicesManagedConfig = KimiConfig & ManagedKimiConfigShape;
type ServicesAuthLoginOptions = Omit<KimiOAuthLoginOptions, 'provisionConfig'>;
interface ServicesAuthLoginResult {
readonly providerName: string;
readonly ok: true;
readonly defaultModel: string;
readonly defaultThinking: boolean;
readonly configPath?: string | undefined;
}
interface ServicesAuthLogoutResult {
readonly providerName: string;
readonly ok: true;
}
export interface ServicesAuthFacade {
login(
providerName?: string | undefined,
options?: ServicesAuthLoginOptions,
): Promise<ServicesAuthLoginResult>;
logout(providerName?: string | undefined): Promise<ServicesAuthLogoutResult>;
getCachedAccessToken(
providerName?: string,
oauthRef?: OAuthRef | undefined,
): Promise<string | undefined>;
readonly resolveOAuthTokenProvider: OAuthTokenProviderResolver;
}
class ServicesManagedAuthFacade implements ServicesAuthFacade {
private readonly toolkit: KimiOAuthToolkit<ServicesManagedConfig>;
constructor(
private readonly options: Pick<IEnvironmentService, 'homeDir' | 'configPath'>,
) {
this.toolkit = new KimiOAuthToolkit<ServicesManagedConfig>({
homeDir: options.homeDir,
configAdapter: {
configPath: options.configPath,
read: () => readConfigFile(options.configPath) as ServicesManagedConfig,
write: async (config) => {
await writeConfigFile(options.configPath, config);
},
apply: applyManagedKimiCodeConfig,
remove: applyManagedKimiCodeLogoutConfig,
},
});
}
async login(
providerName: string | undefined = KIMI_CODE_PROVIDER_NAME,
options: ServicesAuthLoginOptions = {},
): Promise<ServicesAuthLoginResult> {
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.');
}
return {
providerName: result.providerName,
ok: true,
defaultModel: result.provision.defaultModel,
defaultThinking: result.provision.defaultThinking,
configPath: result.provision.configPath,
};
}
async logout(
providerName?: string | undefined,
): Promise<ServicesAuthLogoutResult> {
const result = await this.toolkit.logout(
providerName,
this.resolveRuntimeManagedAuth(providerName).oauthRef,
);
return {
providerName: result.providerName,
ok: result.ok,
};
}
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,
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;
}
}
export function createManagedAuthFacade(
env: Pick<IEnvironmentService, 'homeDir' | 'configPath'>,
): ServicesAuthFacade {
return new ServicesManagedAuthFacade(env);
}

View file

@ -22,14 +22,12 @@
*
* **Implementation** (`AuthSummaryService`): Reads the live config via
* `ICoreProcessService.rpc.getKimiConfig({})` and the managed-OAuth credential
* state via `KimiAuthFacade.status(...)`. Both are cheap (in-process RPC +
* state via a cached-token lookup. Both are cheap (in-process RPC +
* a token-file existence probe), so we run them on every call instead of
* caching keeps the staleness window at zero.
*/
import { createDecorator, Disposable } from '@moonshot-ai/agent-core';
import type { KimiConfig } from '@moonshot-ai/agent-core';
import { KimiAuthFacade } from '@moonshot-ai/kimi-code-sdk';
import { createDecorator } from '@moonshot-ai/agent-core';
import type { AuthSummary } from '@moonshot-ai/protocol';
export interface IAuthSummaryService {
@ -111,5 +109,3 @@ export class AuthModelNotResolvedError extends Error {
this.providerId = providerId;
}
}

View file

@ -4,8 +4,8 @@
import { Disposable, InstantiationType, registerSingleton } from '@moonshot-ai/agent-core';
import type { KimiConfig } from '@moonshot-ai/agent-core';
import { KimiAuthFacade } from '@moonshot-ai/kimi-code-sdk';
import type { AuthSummary } from '@moonshot-ai/protocol';
import { createManagedAuthFacade, type ServicesAuthFacade } from '../auth/managedAuth';
import { IEnvironmentService } from '../environment/environment';
import { ICoreProcessService } from '../coreProcess/coreProcess';
import {
@ -23,17 +23,14 @@ export class AuthSummaryService
implements IAuthSummaryService {
readonly _serviceBrand: undefined;
private readonly _authFacade: KimiAuthFacade;
private readonly _authFacade: ServicesAuthFacade;
constructor(
@IEnvironmentService private readonly env: IEnvironmentService,
@ICoreProcessService private readonly core: ICoreProcessService,
) {
super();
this._authFacade = new KimiAuthFacade({
homeDir: env.homeDir,
configPath: env.configPath,
});
this._authFacade = createManagedAuthFacade(env);
}
async get(): Promise<AuthSummary> {

View file

@ -13,8 +13,7 @@
* `PromptService`, ) dispatch on through the proxy below.
*
* The result is wrapped in a small `SDKRpcClient`-shaped proxy so that
* service impls get the same ergonomics as `@moonshot-ai/kimi-code-sdk`
* (`SDKRpcClientBase` subclass). The proxy is exposed as `rpc` for in-package
* service impls get SDK-style RPC ergonomics. The proxy is exposed as `rpc` for in-package
* consumers; the public package barrel does NOT re-export `SDKRpcClientBase`,
* so daemon-side code stays one abstraction layer away.
*

View file

@ -17,8 +17,8 @@ import {
createKimiDefaultHeaders,
type KimiHostIdentity,
} from '@moonshot-ai/kimi-code-oauth';
import { KimiAuthFacade } from '@moonshot-ai/kimi-code-sdk';
import { createManagedAuthFacade } from '../auth/managedAuth';
import { BridgeClientAPI } from './coreProcessClient';
import { IApprovalService } from '../approval/approval';
import { IEnvironmentService } from '../environment/environment';
@ -78,7 +78,7 @@ export class CoreProcessService extends Disposable implements ICoreProcessServic
// is a different code path (file existence on the credentials store) so
// it stays green; the failure only surfaces inside the prompt turn, as
// an `auth.login_required` error after `turn.step.started`. We bridge
// the gap by default-constructing a `KimiAuthFacade` against the same
// the gap by default-constructing a managed auth facade against the same
// home + config paths KimiCore will use, and handing its
// `resolveOAuthTokenProvider` into the core. Callers (e.g. node-sdk
// tests) can still override via `options.resolveOAuthTokenProvider`.
@ -197,7 +197,7 @@ export class CoreProcessService extends Disposable implements ICoreProcessServic
homeDir: string,
configPath: string,
): OAuthTokenProviderResolver {
const facade = new KimiAuthFacade({ homeDir, configPath });
const facade = createManagedAuthFacade({ homeDir, configPath });
return facade.resolveOAuthTokenProvider;
}

View file

@ -4,13 +4,13 @@ import {
registerSingleton,
} from '@moonshot-ai/agent-core';
import type { KimiConfig, ProviderConfig } from '@moonshot-ai/agent-core';
import { KimiAuthFacade } from '@moonshot-ai/kimi-code-sdk';
import type {
ModelCatalogItem,
ProviderCatalogItem,
SetDefaultModelResponse,
} from '@moonshot-ai/protocol';
import { createManagedAuthFacade, type ServicesAuthFacade } from '../auth/managedAuth';
import { ICoreProcessService } from '../coreProcess/coreProcess';
import { IEnvironmentService } from '../environment/environment';
import {
@ -26,17 +26,14 @@ export class ModelCatalogService
implements IModelCatalogService {
readonly _serviceBrand: undefined;
private readonly _authFacade: KimiAuthFacade;
private readonly _authFacade: ServicesAuthFacade;
constructor(
@IEnvironmentService env: IEnvironmentService,
@ICoreProcessService private readonly core: ICoreProcessService,
) {
super();
this._authFacade = new KimiAuthFacade({
homeDir: env.homeDir,
configPath: env.configPath,
});
this._authFacade = createManagedAuthFacade(env);
}
async listModels(): Promise<readonly ModelCatalogItem[]> {

View file

@ -29,7 +29,7 @@
*
*
* startLogin()
* KimiAuthFacade.login() runs in BACKGROUND
* managed auth facade login runs in BACKGROUND
*
* onDeviceCode(auth) (fires once)
*
@ -62,14 +62,7 @@
* `DeviceCodeTimeoutError`.
*/
import { createDecorator, Disposable } from '@moonshot-ai/agent-core';
import {
DeviceCodeTimeoutError,
KIMI_CODE_PROVIDER_NAME,
OAuthError,
type DeviceAuthorization,
} from '@moonshot-ai/kimi-code-oauth';
import { KimiAuthFacade } from '@moonshot-ai/kimi-code-sdk';
import { createDecorator } from '@moonshot-ai/agent-core';
import type {
OAuthFlowSnapshot,
OAuthFlowStart,
@ -77,7 +70,6 @@ import type {
OAuthLoginCancelResponse,
OAuthLogoutResponse,
} from '@moonshot-ai/protocol';
import { ulid } from 'ulid';
export interface IOAuthService {
readonly _serviceBrand: undefined;
@ -114,5 +106,3 @@ export interface IOAuthService {
// eslint-disable-next-line @typescript-eslint/no-redeclare
export const IOAuthService = createDecorator<IOAuthService>('oauthService');

View file

@ -9,7 +9,6 @@ import {
OAuthError,
type DeviceAuthorization,
} from '@moonshot-ai/kimi-code-oauth';
import { KimiAuthFacade } from '@moonshot-ai/kimi-code-sdk';
import type {
OAuthFlowSnapshot,
OAuthFlowStart,
@ -19,6 +18,7 @@ import type {
} from '@moonshot-ai/protocol';
import { ulid } from 'ulid';
import { createManagedAuthFacade, type ServicesAuthFacade } from '../auth/managedAuth';
import { IEnvironmentService } from '../environment/environment';
import { IOAuthService } from './oauth';
@ -76,20 +76,17 @@ const TERMINAL_RETENTION_MS = 5 * 60 * 1000;
export class OAuthService extends Disposable implements IOAuthService {
readonly _serviceBrand: undefined;
private readonly _authFacade: KimiAuthFacade;
private readonly _authFacade: ServicesAuthFacade;
private readonly _flows: DisposableMap<string, FlowState>;
constructor(@IEnvironmentService private readonly env: IEnvironmentService) {
super();
this._flows = this._register(new DisposableMap<string, FlowState>());
this._authFacade = new KimiAuthFacade({
homeDir: env.homeDir,
configPath: env.configPath,
});
this._authFacade = createManagedAuthFacade(env);
}
/** @internal Test-only factory that injects a mock facade. */
static _createForTest(env: IEnvironmentService, facade: KimiAuthFacade): OAuthService {
static _createForTest(env: IEnvironmentService, facade: ServicesAuthFacade): OAuthService {
const svc = new (OAuthService as any)(env) as OAuthService;
(svc as any)._authFacade = facade;
return svc;
@ -108,7 +105,7 @@ export class OAuthService extends Disposable implements IOAuthService {
const flowId = `oauth_${ulid()}`;
const controller = new AbortController();
// Capture the device authorization via a deferred. `KimiAuthFacade.login`
// Capture the device authorization via a deferred. The managed auth facade
// calls `onDeviceCode` exactly once, then starts polling. We resolve the
// deferred from inside the callback so this method can return as soon as
// the URLs are known — well before the polling completes.

View file

@ -1,3 +1,7 @@
import { readFileSync, readdirSync, statSync } from 'node:fs';
import { join, relative } from 'node:path';
import { fileURLToPath } from 'node:url';
import { describe, expect, it, vi } from 'vitest';
import {
@ -31,6 +35,35 @@ import {
type QuestionResult,
} from '../src';
const packageRoot = fileURLToPath(new URL('..', import.meta.url));
const sdkPackageName = ['@moonshot-ai', 'kimi-code-sdk'].join('/');
function readPackageFiles(): string {
const files = [
'package.json',
'tsdown.config.ts',
'vitest.config.ts',
...sourceFiles(join(packageRoot, 'src')),
];
return files
.map((file) => readFileSync(join(packageRoot, file), 'utf8'))
.join('\n');
}
function sourceFiles(dir: string): string[] {
const files: string[] = [];
for (const entry of readdirSync(dir)) {
const full = join(dir, entry);
const stat = statSync(full);
if (stat.isDirectory()) {
files.push(...sourceFiles(full));
} else if (entry.endsWith('.ts')) {
files.push(relative(packageRoot, full));
}
}
return files;
}
class FakeEventService implements IEventService {
readonly _serviceBrand: undefined;
@ -113,6 +146,10 @@ function makeFakeQuestion(): QuestionRequest & { sessionId: string; agentId: str
}
describe('@moonshot-ai/services · interfaces', () => {
it('does not depend on the node SDK package', () => {
expect(readPackageFiles()).not.toContain(sdkPackageName);
});
it('registers all three peer services in a test instantiation service', () => {
const events = new FakeEventService();
const approvals = new FakeApprovalService();

View file

@ -1,7 +1,7 @@
/**
* `OAuthService` (P2.7) unit tests.
*
* Hermetic: a mock `KimiAuthFacade` is injected so we don't need a real
* Hermetic: a mock managed auth facade is injected so we don't need a real
* OAuth host on the network. The mock's `login()` exposes a deferred device
* authorization + completion promise so tests can drive each transition
* independently:
@ -32,8 +32,8 @@ import {
OAuthError,
type DeviceAuthorization,
} from '@moonshot-ai/kimi-code-oauth';
import type { KimiAuthFacade } from '@moonshot-ai/kimi-code-sdk';
import type { ServicesAuthFacade } from '../src/auth/managedAuth';
import { IEnvironmentService } from '../src/environment/environment';
import { OAuthService } from '../src/oauth/oauthService';
@ -47,7 +47,7 @@ interface LoginCall {
}
interface MockFacade {
facade: KimiAuthFacade;
facade: ServicesAuthFacade;
loginCalls: LoginCall[];
logoutCalls: Array<{ providerName: string | undefined }>;
}
@ -81,7 +81,7 @@ function makeMockFacade(): MockFacade {
logoutCalls.push({ providerName });
return { providerName: providerName ?? 'managed:kimi-code', ok: true as const };
}),
} as unknown as KimiAuthFacade;
} as unknown as ServicesAuthFacade;
return { facade, loginCalls, logoutCalls };
}

View file

@ -12,9 +12,6 @@ export default defineConfig({
clean: true,
plugins: [rawTextPlugin()],
alias: {
'@moonshot-ai/kimi-code-sdk': fileURLToPath(
new URL('../node-sdk/src/index.ts', import.meta.url),
),
'@moonshot-ai/agent-core': fileURLToPath(
new URL('../agent-core/src/index.ts', import.meta.url),
),

View file

@ -26,12 +26,6 @@ export default defineConfig({
new URL('../agent-core/src/di/test.ts', import.meta.url),
),
},
{
find: '@moonshot-ai/kimi-code-sdk',
replacement: fileURLToPath(
new URL('../node-sdk/src/index.ts', import.meta.url),
),
},
{
find: '@moonshot-ai/agent-core',
replacement: fileURLToPath(

3
pnpm-lock.yaml generated
View file

@ -565,9 +565,6 @@ importers:
'@moonshot-ai/kimi-code-oauth':
specifier: workspace:^
version: link:../oauth
'@moonshot-ai/kimi-code-sdk':
specifier: workspace:^
version: link:../node-sdk
'@moonshot-ai/protocol':
specifier: workspace:^
version: link:../protocol