mirror of
https://github.com/MoonshotAI/kimi-code.git
synced 2026-08-31 02:14:58 +00:00
Some checks are pending
CI / build (push) Waiting to run
CI / test (1) (push) Waiting to run
CI / test (2) (push) Waiting to run
CI / test (3) (push) Waiting to run
CI / test (4) (push) Waiting to run
CI / test (5) (push) Waiting to run
CI / test-pi-tui (push) Waiting to run
CI / test-vscode-legacy (push) Waiting to run
CI / test-windows (push) Waiting to run
CI / lint (push) Waiting to run
CI / typecheck (push) Waiting to run
Nix Build / Check flake.nix workspace sync (push) Waiting to run
Nix Build / nix build .#kimi-code (push) Blocked by required conditions
Release / Release (push) Waiting to run
Release / Deploy docs (push) Blocked by required conditions
Release / Native release artifact (push) Blocked by required conditions
Release / Publish native release assets (push) Blocked by required conditions
* feat(agent-core-v2): add the unified MCP management plane Port the v1 MCP management plane (#2858) onto the v2 DI x Scope engine: - App-scope IMcpOAuthService shared by every workspace handler and session overlay: credential events, single-flight refresh, proactive refresh timers, OAuthTokenTransaction-serialized writes, offline tokenState, shutdown. Providers read tokens through the store so grants written or revoked by another process are honored immediately; http/sse transports ride the transaction fetch. - IMcpConfigStore: the single write point for the user-level mcp.json over the filesystem byte store, byte-identical to v1's format, with per-entry validation, name normalization, __proto__-safe parsing, a mutation tail, and an onDidWrite event. - IMcpRegistryService: the unified read view over the layered config files (with per-entry origins) and plugin manifests (full descriptors incl. disabled, with provenance); collisions stay visible and runtime resolution ranks an enabled plugin above the file layers. - IMcpManagementService: guarded CRUD, connection-test probes, the locator-addressed inspection/auth-status surface, and locator-addressed OAuth begin/complete/cancel/reset with ambiguity rejection. Engine services stay ungated; the mcp_management flag gates the edge exposure. - Workspace runtime aligns with v1 precedence (an enabled plugin entry wins over the file layers, shadows revive), and management writes reload immediately via onDidWrite instead of the watch debounce. - node-sdk v2 facade delegates to the engine service (deleting its in-process duplication); kap-server exposes /api/v2/mcp/* and klient gains global.mcp.*, both flag-gated. * fix(agent-core-v2): settle early and cancelled MCP OAuth callbacks * refactor(node-sdk): write session MCP persists through the engine config store * refactor(agent-core-v2): strip comments from the MCP management plane files * fix(agent-core-v2): harden MCP management readiness * test(node-sdk): cover offline MCP auth statuses * fix(agent-core-v2): isolate stdio MCP probes * fix(klient): normalize MCP OAuth errors * fix(mcp): honor workspace CRUD context and refresh timing * fix(mcp): drain OAuth refreshes during shutdown * fix(mcp): guard CRUD across registry collisions * fix(mcp): canonicalize trust and refresh scheduling * fix(mcp): close callback listener on setup failure * fix(mcp): preserve trust and oauth behavior * fix(oauth): retain refresh tokens after SDK saves * fix(oauth): stop proactive sweep during shutdown * fix: await MCP workspace reconciliation * fix: serialize MCP OAuth and trust cleanup * fix: reject persisted MCP plugin collisions * fix: reconcile MCP workspaces concurrently * fix(mcp): check project-layer trust at the queried cwd * fix(mcp): expire abandoned OAuth flows after an idle timeout * fix(mcp): keep mutable user entries writable past read-only collisions * fix(mcp): abort the auth::complete long poll on client disconnect * fix(mcp): map OAuth flow failures to wire code 40929 * docs(mcp): note probe credential effects and plane semantics * chore: add the SDK changeset for MCP management cwd params * feat(mcp): expose the management plane without the experimental flag * fix(mcp): preserve auth management semantics * fix(agent-core-v2): bound MCP OAuth auth-server requests and the shutdown drain * fix(node-sdk): restate engine MCP management errors as KimiError * fix(agent-core-v2): preserve shared OAuth flow lifetime * fix(agent-core-v2): close MCP OAuth cancellation and shutdown gaps - bound the authorization-code exchange with the request timeout and the flow/caller abort signals, and make shutdown abort hung begins and close their callback listeners immediately - keep token-transaction effect coalescing intact when durable tokens carry local stamps, and serialize the meta sidecar and tokens-saved event with the token write inside the lock - drain transport-driven grants, their trailing SDK save continuations, and interactive completions during shutdown, with a cancellable deadline * fix(agent-core-v2): harden MCP probe runtime resolution and path handling - resolve stdio probes against the containing workspace's runtimes and reject out-of-workspace probes for non-local runtime_id instead of silently falling back to a local-only transient registry - share one Windows-aware path canonicalization across the config loader, registry trust lookup, trust records, and workspace matching - keep a UTF-8 BOM fatal for the user-level mcp.json store, matching the workspace loader and v1 - validate completeServerAuth timeoutMs bounds at the engine boundary * fix(agent-core-v2): await workspace MCP reconciliation on plugin mutations Plugin install/enable/disable/remove now resolve only after reload listeners settle their waitUntil work, so a disabled plugin's MCP server cannot linger connected and an enabled one is visible to the next session, matching v1. The workspace MCP consumer joins the barrier while keeping its log-only failure tolerance; delivery is awaited outside the mutation queue to avoid self-deadlock through consumption reads. * fix(mcp): close the SDK, klient, and server edge gaps - register mcp.oauth_failed in the v1 error registry and restate unknown engine codes as internal instead of minting undeclared KimiError codes - route persisted session MCP adds through the same KimiError restating as the global management methods - give the klient IPC transport a per-call timeout so completeAuth's long poll outlives the 30s default, clamped to the Node timer ceiling, and align the contract timeoutMs upper bound with REST - await the MCP OAuth service shutdown directly in SDK and server close before scope disposal * fix(agent-core-v2): keep file-over-plugin MCP precedence and harden the plane - Revert the v1-style precedence flip: the workspace merge and resolveRuntimeTarget keep the file entry above plugins (v2's historical order; the divergence from v1 is deliberate and documented in AGENTS.md). - Guards follow each engine's winner: project-layer entries stay read-only, while plugin entries never block user-level writes, so a file entry may shadow a plugin and removing it revives the plugin. The parity suite pins the engine split for a persisted session add over a plugin-owned name. - inspectServers tolerates a wire-encoded null targets array: klient's ipc transport sends null for an omitted leading optional argument. - Fire the config store's onDidWrite after the mutation tail settles, so a write listener can re-enter the store without deadlocking the queue; concurrent-mutation and re-entrant-listener tests pin both contracts. * chore: condense the sdk MCP changeset to one sentence * test(node-sdk): pin verify:false auth-status parity and fix the sdk changeset
134 lines
5 KiB
TypeScript
134 lines
5 KiB
TypeScript
import { rm } from 'node:fs/promises';
|
|
import { join } from 'node:path';
|
|
import { tmpdir } from 'node:os';
|
|
|
|
import { IMcpManagementService } from '@moonshot-ai/agent-core-v2';
|
|
import { describe, expect, it, vi } from 'vitest';
|
|
|
|
import { defineKlientConformance } from './helpers/conformance.js';
|
|
import { createKlient, serveKlientIpc, type KlientIpcHost } from '../src/transports/ipc/index.js';
|
|
import { makeEngine, type TestEngine } from './helpers/engine.js';
|
|
|
|
defineKlientConformance('ipc', async () => {
|
|
const { homeDir, app } = await makeEngine();
|
|
const socketPath = join(homeDir, 'klient.sock');
|
|
const host = await serveKlientIpc({ scope: app, socketPath });
|
|
const klient = createKlient({ socketPath });
|
|
return {
|
|
klient,
|
|
app,
|
|
cleanup: async () => {
|
|
await klient.close();
|
|
await host.close();
|
|
app.dispose();
|
|
await rm(homeDir, { recursive: true, force: true, maxRetries: 3, retryDelay: 25 });
|
|
},
|
|
};
|
|
});
|
|
|
|
describe('ipc transport specifics', () => {
|
|
let homeDir: string;
|
|
let app: TestEngine['app'];
|
|
let host: KlientIpcHost | undefined;
|
|
|
|
async function setup(opts: { token?: string } = {}): Promise<string> {
|
|
({ homeDir, app } = await makeEngine());
|
|
const socketPath = join(homeDir, 'klient.sock');
|
|
host = await serveKlientIpc({ scope: app, socketPath, token: opts.token });
|
|
return socketPath;
|
|
}
|
|
|
|
async function teardown(): Promise<void> {
|
|
await host?.close();
|
|
host = undefined;
|
|
app.dispose();
|
|
await rm(homeDir, { recursive: true, force: true, maxRetries: 3, retryDelay: 25 });
|
|
}
|
|
|
|
it('rejects calls when the socket path does not exist', async () => {
|
|
const klient = createKlient({ socketPath: join(tmpdir(), 'klient-no-such.sock') });
|
|
await expect(klient.global.env()).rejects.toThrow();
|
|
await klient.close();
|
|
});
|
|
|
|
it('rejects calls made after close', async () => {
|
|
const socketPath = await setup();
|
|
const klient = createKlient({ socketPath });
|
|
await klient.global.env();
|
|
await klient.close();
|
|
// env() is served from its frozen-snapshot cache after the first call, so
|
|
// probe the closed channel with an uncached method instead.
|
|
await expect(klient.global.workspaces.list()).rejects.toThrow('ipc closed');
|
|
await teardown();
|
|
});
|
|
|
|
it('drops clients whose hello token mismatches', async () => {
|
|
const socketPath = await setup({ token: 'right' });
|
|
const klient = createKlient({ socketPath, token: 'wrong' });
|
|
await expect(klient.global.env()).rejects.toThrow();
|
|
await klient.close();
|
|
|
|
const ok = createKlient({ socketPath, token: 'right' });
|
|
await expect(ok.global.env()).resolves.toMatchObject({ platform: process.platform });
|
|
await ok.close();
|
|
await teardown();
|
|
});
|
|
|
|
it('completeAuth outlives the channel default call timeout', async () => {
|
|
const socketPath = await setup();
|
|
// A slow engine-side wait: without the facade's per-call deadline the
|
|
// channel's default would kill the long poll mid-flight.
|
|
const management = app.accessor.get(IMcpManagementService);
|
|
const completeSpy = vi
|
|
.spyOn(management, 'completeServerAuth')
|
|
.mockImplementation(
|
|
() => new Promise<void>((resolve) => setTimeout(resolve, 200)),
|
|
);
|
|
const cancelSpy = vi
|
|
.spyOn(management, 'cancelServerAuth')
|
|
.mockImplementation(
|
|
() => new Promise<void>((resolve) => setTimeout(resolve, 200)),
|
|
);
|
|
const klient = createKlient({ socketPath, callTimeoutMs: 25 });
|
|
try {
|
|
// completeAuth passes the engine wait + margin as its per-call deadline,
|
|
// so the 200ms wait resolves instead of dying at the 25ms default.
|
|
await expect(
|
|
klient.global.mcp.completeAuth({ flowId: 'flow-1', timeoutMs: 100 }),
|
|
).resolves.toBeUndefined();
|
|
// Calls without the override still die at the channel default.
|
|
await expect(klient.global.mcp.cancelAuth({ flowId: 'flow-1' })).rejects.toThrow(
|
|
'call timed out after 25ms',
|
|
);
|
|
} finally {
|
|
completeSpy.mockRestore();
|
|
cancelSpy.mockRestore();
|
|
await klient.close();
|
|
}
|
|
await teardown();
|
|
});
|
|
|
|
it('completeAuth clamps a near-max timeoutMs instead of overflowing the call timer', async () => {
|
|
const socketPath = await setup();
|
|
const management = app.accessor.get(IMcpManagementService);
|
|
const completeSpy = vi
|
|
.spyOn(management, 'completeServerAuth')
|
|
.mockImplementation(
|
|
() => new Promise<void>((resolve) => setTimeout(resolve, 50)),
|
|
);
|
|
const klient = createKlient({ socketPath, callTimeoutMs: 25 });
|
|
try {
|
|
// timeoutMs at the contract max plus the facade margin would overflow
|
|
// Node's 32-bit setTimeout into ~1ms; the clamp keeps the call alive
|
|
// until the engine-side wait resolves.
|
|
await expect(
|
|
klient.global.mcp.completeAuth({ flowId: 'flow-1', timeoutMs: 2 ** 31 - 1 }),
|
|
).resolves.toBeUndefined();
|
|
expect(completeSpy).toHaveBeenCalled();
|
|
} finally {
|
|
completeSpy.mockRestore();
|
|
await klient.close();
|
|
}
|
|
await teardown();
|
|
});
|
|
});
|