kimi-code/packages/agent-core/test/rpc/plugins-rpc.test.ts
Liu Zhongnuo 504e6292ed
feat(mcp): inspect effective authorization state in v1 (#2856)
* feat(mcp): inspect effective authorization state

* test(agent-core-v2): register MCP auth coordinator fixture

* fix(mcp): validate runtime names against full catalog

* fix(mcp): reconnect after pending auth updates

* docs(mcp): describe auth coordinator collaborator

* fix(mcp): ignore disabled runtime name collisions

* fix(mcp): serialize OAuth token refresh

* test(mcp): await OAuth credential writes

* fix(mcp): queue trailing credential reconnect

* fix(oauth): preserve access-only refresh winners

* fix(mcp): preserve legacy offline auth state

* fix(mcp): redact inspection credentials

* refactor(mcp): keep app inspection on v1

* fix(mcp): guard legacy auth status mutations

* fix(mcp): avoid deterministic legacy auth probes

* fix(mcp): cover initialization credential updates

---------

Co-authored-by: 刘仲诺 <liuzhongnuo@dev.msh.team>
2026-08-13 12:00:04 +08:00

315 lines
11 KiB
TypeScript

import { mkdir, mkdtemp, writeFile } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import path from 'node:path';
import { describe, expect, it } from 'vitest';
import { McpOAuthService } from '../../src/mcp/oauth/service';
import { KimiCore } from '../../src/rpc/core-impl';
describe('KimiCore plugin RPCs', () => {
it('install → list → setEnabled → remove round trip', async () => {
const home = await mkdtemp(path.join(tmpdir(), 'kimi-home-'));
const pluginRoot = await mkdtemp(path.join(tmpdir(), 'plugin-'));
await writeFile(
path.join(pluginRoot, 'kimi.plugin.json'),
JSON.stringify({ name: 'demo', version: '1.0.0' }),
'utf8',
);
const core = new KimiCore(async () => ({}) as never, { homeDir: home });
await new Promise((r) => setImmediate(r));
const installed = await core.installPlugin({ source: pluginRoot });
expect(installed.id).toBe('demo');
expect(installed.version).toBe('1.0.0');
const list = await core.listPlugins({});
expect(list).toHaveLength(1);
await core.setPluginEnabled({ id: 'demo', enabled: false });
const after = await core.listPlugins({});
expect(after[0]?.enabled).toBe(false);
await core.removePlugin({ id: 'demo' });
await expect(core.listPlugins({})).resolves.toEqual([]);
});
it('installPlugin ignores forged marketplace context from public RPC callers', async () => {
const home = await mkdtemp(path.join(tmpdir(), 'kimi-home-'));
const pluginRoot = await mkdtemp(path.join(tmpdir(), 'plugin-'));
await writeFile(
path.join(pluginRoot, 'kimi.plugin.json'),
JSON.stringify({ name: 'demo', version: '1.0.0' }),
'utf8',
);
const core = new KimiCore(async () => ({}) as never, { homeDir: home });
await new Promise((r) => setImmediate(r));
const installed = await core.installPlugin({
source: pluginRoot,
marketplace: { id: 'demo', tier: 'official' },
} as never);
expect((installed as { marketplace?: unknown }).marketplace).toBeUndefined();
});
it('setPluginMcpServerEnabled toggles plugin MCP state', async () => {
const home = await mkdtemp(path.join(tmpdir(), 'kimi-home-'));
const pluginRoot = await mkdtemp(path.join(tmpdir(), 'plugin-'));
await writeFile(
path.join(pluginRoot, 'kimi.plugin.json'),
JSON.stringify({
name: 'demo',
mcpServers: {
finance: { command: 'finance-mcp' },
},
}),
'utf8',
);
const core = new KimiCore(async () => ({}) as never, { homeDir: home });
await new Promise((r) => setImmediate(r));
await core.installPlugin({ source: pluginRoot });
await core.setPluginMcpServerEnabled({ id: 'demo', server: 'finance', enabled: true });
await expect(core.getPluginInfo({ id: 'demo' })).resolves.toEqual(
expect.objectContaining({
mcpServers: expect.arrayContaining([
expect.objectContaining({ name: 'finance', enabled: true }),
]),
}),
);
});
it('inspects global and plugin MCP servers through the v1 app catalog', async () => {
const home = await mkdtemp(path.join(tmpdir(), 'kimi-home-'));
const pluginRoot = await mkdtemp(path.join(tmpdir(), 'plugin-'));
await writeFile(
path.join(home, 'mcp.json'),
JSON.stringify({
mcpServers: {
global: { command: 'global-mcp', env: { GLOBAL_SECRET: 'global-secret-value' } },
},
}),
'utf8',
);
await writeFile(
path.join(pluginRoot, 'kimi.plugin.json'),
JSON.stringify({
name: 'demo',
mcpServers: {
local: { command: 'local-mcp', env: { PLUGIN_SECRET: 'plugin-secret-value' } },
remote: {
transport: 'http',
url: 'https://mcp.example.test/service',
auth: 'oauth',
},
},
}),
'utf8',
);
const core = new KimiCore(async () => ({}) as never, { homeDir: home });
await core.installPlugin({ source: pluginRoot });
await core.setPluginMcpServerEnabled({ id: 'demo', server: 'remote', enabled: false });
const inspections = await core.inspectAppMcpServers({});
expect(inspections).toEqual([
expect.objectContaining({
serverId: 'global:global',
locator: { source: 'global', name: 'global' },
runtimeName: 'global',
origin: 'global',
editable: true,
authStatus: 'not-applicable',
config: expect.objectContaining({ envKeys: ['GLOBAL_SECRET'] }),
}),
expect.objectContaining({
serverId: 'plugin:demo:local',
locator: { source: 'plugin', pluginId: 'demo', serverName: 'local' },
runtimeName: 'plugin-demo:local',
origin: 'plugin',
editable: false,
enabled: true,
authStatus: 'not-applicable',
config: expect.objectContaining({ envKeys: expect.arrayContaining(['PLUGIN_SECRET']) }),
}),
expect.objectContaining({
serverId: 'plugin:demo:remote',
locator: { source: 'plugin', pluginId: 'demo', serverName: 'remote' },
runtimeName: 'plugin-demo:remote',
origin: 'plugin',
editable: false,
enabled: false,
authStatus: 'not-applicable',
}),
]);
expect(JSON.stringify(inspections)).not.toContain('global-secret-value');
expect(JSON.stringify(inspections)).not.toContain('plugin-secret-value');
const oauth = new McpOAuthService({ kimiHomeDir: home });
await oauth
.getProvider('plugin-demo:remote', 'https://mcp.example.test/service')
.saveTokens({ access_token: 'plugin-test-token', token_type: 'Bearer' });
await core.resetMcpServerAuth({
locator: { source: 'plugin', pluginId: 'demo', serverName: 'remote' },
});
expect(oauth.hasTokens('plugin-demo:remote', 'https://mcp.example.test/service')).toBe(false);
await oauth
.getProvider('plugin-demo:remote', 'https://mcp.example.test/service')
.saveTokens({ access_token: 'plugin-test-token', token_type: 'Bearer' });
await core.setPluginMcpServerEnabled({ id: 'demo', server: 'remote', enabled: true });
await core.addGlobalMcpServer({
server: {
name: 'plugin-demo:remote',
transport: 'http',
url: 'https://global.example.test/service',
auth: 'oauth',
},
});
const locator = { source: 'plugin', pluginId: 'demo', serverName: 'remote' } as const;
await expect(core.beginMcpServerAuth({ locator })).rejects.toThrow(
'is shared by multiple enabled servers',
);
await expect(core.resetMcpServerAuth({ locator })).rejects.toThrow(
'is shared by multiple enabled servers',
);
expect(oauth.hasTokens('plugin-demo:remote', 'https://mcp.example.test/service')).toBe(true);
});
it('injects persisted managed Kimi Code environment into the datasource plugin MCP server', async () => {
const previousBaseUrl = process.env['KIMI_CODE_BASE_URL'];
const previousCodeOAuthHost = process.env['KIMI_CODE_OAUTH_HOST'];
const previousOAuthHost = process.env['KIMI_OAUTH_HOST'];
delete process.env['KIMI_CODE_BASE_URL'];
delete process.env['KIMI_CODE_OAUTH_HOST'];
delete process.env['KIMI_OAUTH_HOST'];
const home = await mkdtemp(path.join(tmpdir(), 'kimi-home-'));
const pluginRoot = await mkdtemp(path.join(tmpdir(), 'plugin-'));
try {
await writeFile(
path.join(home, 'config.toml'),
`
[providers."managed:kimi-code"]
type = "kimi"
base_url = "https://api.dev.example.test/coding/v1"
api_key = ""
oauth = { storage = "file", key = "oauth/kimi-code-env-1234", oauth_host = "https://auth.dev.example.test" }
`,
'utf8',
);
await writeFile(
path.join(pluginRoot, 'kimi.plugin.json'),
JSON.stringify({
name: 'kimi-datasource',
mcpServers: {
data: { command: 'node', args: ['./bin/kimi-datasource.mjs'] },
},
}),
'utf8',
);
const core = new KimiCore(async () => ({}) as never, { homeDir: home });
await new Promise((r) => setImmediate(r));
await core.installPlugin({ source: pluginRoot });
const mcpConfig = (
core as unknown as {
mergePluginMcpConfig(base: undefined): {
servers: Record<string, { env?: Record<string, string> }>;
};
}
).mergePluginMcpConfig(undefined);
expect(mcpConfig.servers['plugin-kimi-datasource:data']?.env).toEqual(
expect.objectContaining({
KIMI_CODE_BASE_URL: 'https://api.dev.example.test/coding/v1',
KIMI_CODE_OAUTH_HOST: 'https://auth.dev.example.test',
}),
);
} finally {
restoreEnv('KIMI_CODE_BASE_URL', previousBaseUrl);
restoreEnv('KIMI_CODE_OAUTH_HOST', previousCodeOAuthHost);
restoreEnv('KIMI_OAUTH_HOST', previousOAuthHost);
}
});
it('throws PLUGIN_LOAD_FAILED on every RPC when installed.json is corrupt', async () => {
const home = await mkdtemp(path.join(tmpdir(), 'kimi-home-'));
await mkdir(path.join(home, 'plugins'), { recursive: true });
await writeFile(path.join(home, 'plugins', 'installed.json'), '{ not json', 'utf8');
const core = new KimiCore(async () => ({}) as never, { homeDir: home });
// Driving an awaiting RPC first ensures the load promise has settled
// and captured pluginsLoadError before the read RPCs run.
await expect(core.installPlugin({ source: '/tmp/nonexistent' })).rejects.toThrow(/load/i);
await expect(core.listPlugins({})).rejects.toThrow(/load/i);
await expect(core.getPluginInfo({ id: 'demo' })).rejects.toThrow(/load/i);
await expect(core.setPluginEnabled({ id: 'demo', enabled: false })).rejects.toThrow(/load/i);
await expect(
core.setPluginMcpServerEnabled({ id: 'demo', server: 'finance', enabled: true }),
).rejects.toThrow(/load/i);
await expect(core.removePlugin({ id: 'demo' })).rejects.toThrow(/load/i);
// installed.json must NOT have been overwritten by the failed install.
const { readFile } = await import('node:fs/promises');
const onDisk = await readFile(path.join(home, 'plugins', 'installed.json'), 'utf8');
expect(onDisk).toBe('{ not json');
// Fixing the file and calling reload clears the error state.
await writeFile(
path.join(home, 'plugins', 'installed.json'),
JSON.stringify({ version: 1, plugins: [] }),
'utf8',
);
await core.reloadPlugins({});
await expect(core.listPlugins({})).resolves.toEqual([]);
});
it('listPlugins waits for initial plugin load', async () => {
const home = await mkdtemp(path.join(tmpdir(), 'kimi-home-'));
const pluginRoot = await mkdtemp(path.join(tmpdir(), 'plugin-'));
await writeFile(
path.join(pluginRoot, 'kimi.plugin.json'),
JSON.stringify({ name: 'demo' }),
'utf8',
);
await mkdir(path.join(home, 'plugins'), { recursive: true });
await writeFile(
path.join(home, 'plugins', 'installed.json'),
JSON.stringify({
version: 1,
plugins: [
{
id: 'demo',
root: pluginRoot,
source: 'local-path',
enabled: true,
installedAt: '2026-05-25T09:00:00Z',
},
],
}),
'utf8',
);
const core = new KimiCore(async () => ({}) as never, { homeDir: home });
await expect(core.listPlugins({})).resolves.toContainEqual(
expect.objectContaining({ id: 'demo' }),
);
});
});
function restoreEnv(name: string, value: string | undefined): void {
if (value === undefined) {
delete process.env[name];
return;
}
process.env[name] = value;
}