fix(plugin): remediate ML-07 R2 findings

This commit is contained in:
2026-08-27 21:16:27 +08:00
parent 29cf322f1a
commit f0ac7d70d1
10 changed files with 381 additions and 29 deletions

View File

@@ -86,7 +86,7 @@ function adapter(): CodingPluginAdapter {
function registry(overrides: Partial<ConstructorParameters<typeof CodingCapabilityRegistryImpl>[0]> = {}) {
return new CodingCapabilityRegistryImpl({
policyClient: { getState: () => policy },
policyClient: { getState: () => policy, refresh: vi.fn().mockResolvedValue(undefined) },
getEnabledPluginIds: async () => ['makelore.data-service'],
adapters: [adapter()],
definitions: [DATA_SERVICE_PLUGIN_DEFINITION],
@@ -114,13 +114,51 @@ describe('CodingCapabilityRegistry', () => {
expect(child.pluginIds).toEqual([]);
expect(child.tools).toEqual([]);
const unavailable = await registry({
policyClient: { getState: () => ({ ...policy, catalog: null, status: 'unavailable', revision: 0 }) },
policyClient: {
getState: () => ({ ...policy, catalog: null, status: 'unavailable', revision: 0 }),
refresh: vi.fn().mockResolvedValue(undefined),
},
}).resolveWorkerResources({
projectPath: context.projectPath, assignedSkillIds: context.skillIds, role: 'parent',
});
expect(unavailable.tools).toEqual([]);
});
it('does not wait for policy refresh when child or parent core-only resources cannot consume plugins', async () => {
const refresh = vi.fn(() => new Promise<void>(() => {}));
const policyClient = { getState: () => policy, refresh };
const child = await registry({ policyClient }).resolveWorkerResources({
projectPath: context.projectPath,
assignedSkillIds: context.skillIds,
role: 'child',
});
const coreOnly = await registry({ policyClient }).resolveWorkerResources({
projectPath: context.projectPath,
assignedSkillIds: ['grilling'],
role: 'parent',
});
expect(child.tools).toEqual([]);
expect(coreOnly.effectiveSkillIds).toEqual(['grilling']);
expect(refresh).not.toHaveBeenCalled();
});
it('refreshes policy before resolving an assigned server-backed parent plugin', async () => {
const refresh = vi.fn().mockResolvedValue(undefined);
const getEnabledPluginIds = vi.fn().mockResolvedValue(['makelore.data-service']);
await registry({
policyClient: { getState: () => policy, refresh },
getEnabledPluginIds,
}).resolveWorkerResources({
projectPath: context.projectPath,
assignedSkillIds: context.skillIds,
role: 'parent',
});
expect(refresh).toHaveBeenCalledOnce();
expect(refresh.mock.invocationCallOrder[0]).toBeLessThan(getEnabledPluginIds.mock.invocationCallOrder[0]);
});
it('revalidates invocation, derives the stable Pi request id, and preserves domain faults', async () => {
const result = await registry().invoke({
toolName: 'data_service_get_document',

View File

@@ -1,5 +1,11 @@
import { describe, expect, it, vi } from 'vitest';
import { createCodingPluginsStore } from '@/stores/coding-plugins';
import type {
DataServiceCollectionRemoval,
DataServiceHostResult,
DataServiceInstanceRemoval,
DataServiceInstanceState,
} from '../../shared/data-service';
function projection(enabled = false, projectId = 'local-project') {
return {
@@ -15,6 +21,31 @@ function projection(enabled = false, projectId = 'local-project') {
};
}
function deferred<T>() {
let resolve!: (value: T) => void;
const promise = new Promise<T>((done) => { resolve = done; });
return { promise, resolve };
}
function dataServiceInstance(instanceId: string): DataServiceInstanceState {
return {
instance_id: instanceId, project_id: 'cloud-project', collections: [],
usage: { document_count: 0, total_bytes: 0 },
limits: {
max_collections: 20, max_documents: 1000, max_total_bytes: 20_971_520,
max_document_bytes: 65_536, list_default_limit: 50, list_max_limit: 100,
list_max_data_bytes: 1_048_576, mutations_per_minute: 120,
},
created_at: '2026-08-27T00:00:00Z', updated_at: '2026-08-27T00:00:00Z',
};
}
function success<T>(data: T): DataServiceHostResult<T> {
return {
success: true, status: 200, code: null, error: null, retryable: false, data,
};
}
describe('coding plugins store', () => {
it('coalesces duplicate loads and mutations while retaining the last projection on failure', async () => {
let resolveLoad!: (value: ReturnType<typeof projection>) => void;
@@ -87,6 +118,66 @@ describe('coding plugins store', () => {
});
});
it('does not let a late project A enable mutation overwrite loaded project B', async () => {
const enabledA = deferred<ReturnType<typeof projection>>();
const inspectDataService = vi.fn();
const store = createCodingPluginsStore({
list: vi.fn().mockResolvedValue(projection(false, 'project-b')),
setEnabled: vi.fn(() => enabledA.promise),
inspectDataService,
});
store.setState({ projectId: 'project-a', projection: projection(false, 'project-a') });
const mutation = store.getState().setEnabled('project-a', 'makelore.data-service', true);
await store.getState().load('project-b');
enabledA.resolve(projection(true, 'project-a'));
await mutation;
expect(store.getState()).toMatchObject({
projectId: 'project-b', projection: { project: { localProjectId: 'project-b' } },
});
expect(inspectDataService).not.toHaveBeenCalled();
});
it('does not let adjacent late Data Service mutations commit or reload over project B', async () => {
const configure = deferred<DataServiceHostResult<DataServiceInstanceState>>();
const reset = deferred<DataServiceHostResult<DataServiceInstanceState>>();
const removeCollection = deferred<DataServiceHostResult<DataServiceCollectionRemoval>>();
const removeProject = deferred<DataServiceHostResult<DataServiceInstanceRemoval>>();
const inspectDataService = vi.fn();
const list = vi.fn().mockResolvedValue(projection(false, 'project-b'));
const store = createCodingPluginsStore({
list,
inspectDataService,
configureDataService: vi.fn(() => configure.promise),
resetDataService: vi.fn(() => reset.promise),
removeCollection: vi.fn(() => removeCollection.promise),
removeProject: vi.fn(() => removeProject.promise),
});
store.setState({ projectId: 'project-a', projection: projection(false, 'project-a') });
const mutations = [
store.getState().configure(['todos']),
store.getState().reset(),
store.getState().removeCollection('todos'),
store.getState().removeProject(),
];
await store.getState().load('project-b');
configure.resolve(success(dataServiceInstance('late-configure')));
reset.resolve(success(dataServiceInstance('late-reset')));
removeCollection.resolve(success({ removed: true, usage: { document_count: 0, total_bytes: 0 } }));
removeProject.resolve(success({ removed: true }));
await Promise.all(mutations);
expect(store.getState()).toMatchObject({
projectId: 'project-b',
projection: { project: { localProjectId: 'project-b' } },
dataService: null,
});
expect(inspectDataService).not.toHaveBeenCalled();
expect(list).toHaveBeenCalledOnce();
});
it('retains ready Data Service usage when enabling an existing instance', async () => {
const readyProjection = projection(true);
readyProjection.items[0] = {
@@ -104,6 +195,7 @@ describe('coding plugins store', () => {
const store = createCodingPluginsStore({
setEnabled: vi.fn().mockResolvedValue(readyProjection), inspectDataService,
});
store.setState({ projectId: 'local-project' });
await store.getState().setEnabled('local-project', 'makelore.data-service', true);

View File

@@ -46,6 +46,28 @@ function operations(): DataServiceOperations {
}
describe('Data Service plugin adapter', () => {
it('validates the parsed tool supplied by the capability registry', async () => {
const dataService = operations();
const adapter = createDataServicePluginAdapter(dataService);
const inspect = DATA_SERVICE_TOOL_DEFINITIONS.find(({ name }) => name === 'data_service_inspect');
if (!inspect) throw new Error('inspect definition missing');
const result = await adapter.invoke(context, {
...inspect,
inputSchema: {
type: 'object',
additionalProperties: false,
required: ['registry_token'],
properties: { registry_token: { type: 'string', minLength: 1 } },
},
}, {});
expect(result).toMatchObject({
success: false, status: 422, code: 'plugin_input_invalid', data: null,
});
expect(dataService.inspect).not.toHaveBeenCalled();
});
it('maps all ten package operations through DataServiceOperations', async () => {
const dataService = operations();
const adapter = createDataServicePluginAdapter(dataService);

View File

@@ -4,13 +4,15 @@ import { mkdtemp, readFile, rm, writeFile } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import path from 'node:path';
import { pathToFileURL } from 'node:url';
import { afterEach, describe, expect, it } from 'vitest';
import { afterEach, describe, expect, it, vi } from 'vitest';
import { PiManagedExtensionHost } from '../../electron/coding-runtime/pi/extension-host';
import { PiSubagentScheduler } from '../../electron/coding-runtime/pi/subagent';
import { PiProcessBudget } from '../../electron/coding-runtime/pi/worker-pool';
import type { AgentBrowserModule } from '../../electron/agent-browser';
import { CodingAttachmentStore } from '../../electron/coding-projects/attachment-store';
import { PiProductTools } from '../../electron/coding-runtime/pi/product-tools';
import { CodingCapabilityRegistryImpl } from '../../electron/coding-plugins/registry';
import type { PluginPolicyClientState } from '../../electron/services/plugin-policy-client';
import {
DATA_SERVICE_PLUGIN_DEFINITION,
type CodingPluginToolDefinition,
@@ -46,6 +48,118 @@ afterEach(async () => {
});
describe('Makelore Pi extension bundle', () => {
it('hydrates persisted Pi identity through reconnect and event replay into the capability registry', async () => {
const root = await mkdtemp(path.join(tmpdir(), 'makelore-pi-persisted-identity-'));
roots.push(root);
const policy: PluginPolicyClientState = {
status: 'current', revision: 23, lastVerifiedAt: 1,
catalog: {
schema_version: 1, catalog_version: 'catalog-23', pricing_version: null,
plugins: [{
plugin_id: DATA_SERVICE_PLUGIN_DEFINITION.id,
supported_contract_versions: [DATA_SERVICE_PLUGIN_DEFINITION.contractVersion],
status: 'active',
capabilities: [{
capability_id: 'data-service.control',
operations: [{
operation: 'inspect',
billing: { mode: 'included', entitlement_scope: null, notice: 'Included' },
}],
}],
}],
},
};
const invoke = vi.fn(async () => ({
success: true as const, status: 200, code: null, error: null, retryable: false as const,
payload_schema: 'data-service.v1', data: { instance_id: 'instance-a' },
}));
const capabilityRegistry = new CodingCapabilityRegistryImpl({
policyClient: { getState: () => policy, refresh: vi.fn().mockResolvedValue(undefined) },
getEnabledPluginIds: async () => [DATA_SERVICE_PLUGIN_DEFINITION.id],
definitions: [DATA_SERVICE_PLUGIN_DEFINITION],
adapters: [{
pluginId: DATA_SERVICE_PLUGIN_DEFINITION.id,
async inspect() { return { status: 'ready' }; },
invoke,
}],
});
const host = new PiManagedExtensionHost();
host.configureProductTools(new PiProductTools({
browser: {} as AgentBrowserModule,
attachments: new CodingAttachmentStore(path.join(root, 'attachments')),
bundledSkillsDir: path.resolve('resources/coding-skills'),
capabilityRegistry,
}));
hosts.push(host);
const inspectTool = DATA_SERVICE_PLUGIN_DEFINITION.tools.find(
({ name }) => name === 'data_service_inspect',
);
if (!inspectTool) throw new Error('inspect definition missing');
const worker = await host.registerWorker({
conversationId: 'persisted-conversation', generation: 1, projectId: 'project-a',
projectPath: root, extensionsDir: root,
skillEntries: [{ id: 'data-service', entryPath: 'skills/data-service/SKILL.md' }],
catalogRevision: policy.revision,
tools: [inspectTool],
});
await host.bindRun('persisted-conversation', 1, 'persisted-run');
const persistedContext = JSON.parse(await readFile(
worker.env.MAKELORE_PI_CONTEXT_FILE as string,
'utf8',
)) as { runId?: string };
expect(persistedContext.runId).toBe('persisted-run');
const previous = {
bridge: process.env.MAKELORE_PI_BRIDGE_URL,
token: process.env.MAKELORE_PI_WORKER_TOKEN,
context: process.env.MAKELORE_PI_CONTEXT_FILE,
role: process.env.MAKELORE_PI_WORKER_ROLE,
};
Object.assign(process.env, worker.env);
try {
const executeAfterHydration = async (connection: string) => {
const module = await import(
/* @vite-ignore */ `${pathToFileURL(worker.extensionPath).href}?connection=${connection}`
) as {
default(factory: {
registerTool(tool: ExtensionTool): void;
on(event: string, handler: ExtensionHandler): void;
}): void | Promise<void>;
};
const tools = new Map<string, ExtensionTool>();
await module.default({
registerTool: (tool) => tools.set(tool.name, tool),
on: () => undefined,
});
return await tools.get('data_service_inspect')?.execute?.(
'persisted-resource', {}, new AbortController().signal,
);
};
const first = await executeAfterHydration('initial');
const replay = await executeAfterHydration('reconnect');
expect(first).toMatchObject({
details: {
plugin_id: DATA_SERVICE_PLUGIN_DEFINITION.id,
request_id: 'pi:persisted-run:persisted-resource',
},
});
expect(replay).toMatchObject({
details: { request_id: 'pi:persisted-run:persisted-resource' },
});
expect(invoke).toHaveBeenCalledTimes(2);
} finally {
for (const [key, value] of Object.entries(previous)) {
const environmentKey = key === 'bridge' ? 'MAKELORE_PI_BRIDGE_URL'
: key === 'token' ? 'MAKELORE_PI_WORKER_TOKEN'
: key === 'context' ? 'MAKELORE_PI_CONTEXT_FILE'
: 'MAKELORE_PI_WORKER_ROLE';
if (value === undefined) delete process.env[environmentKey];
else process.env[environmentKey] = value;
}
}
});
it('materializes only the frozen plugin declarations and lease metadata', async () => {
const root = await mkdtemp(path.join(tmpdir(), 'makelore-pi-dynamic-bundle-'));
roots.push(root);

View File

@@ -180,7 +180,7 @@ describe('Pi managed resource loader', () => {
},
};
const registry = new CodingCapabilityRegistryImpl({
policyClient: { getState: () => policy },
policyClient: { getState: () => policy, refresh: vi.fn().mockResolvedValue(undefined) },
getEnabledPluginIds: async () => enabled ? [DATA_SERVICE_PLUGIN_DEFINITION.id] : [],
definitions: [DATA_SERVICE_PLUGIN_DEFINITION],
adapters: [{