fix(coding): remediate ML-07 plugin authority
This commit is contained in:
1 parent
9407c67df2
commit
cf13aa7eba
29 files changed
+1008
-214
No files matched your search
@@ -19,7 +19,7 @@ import {
|
||||
import { PiProductTools } from '../../electron/coding-runtime/pi/product-tools';
|
||||
import { productToolDetails } from '../../electron/coding-runtime/product-tool-protocol';
|
||||
import type { CodingCapabilityRegistry } from '../../electron/coding-plugins/registry';
|
||||
import type { DataServiceOperations } from '../../electron/services/data-service-client';
|
||||
import { DATA_SERVICE_PLUGIN_DEFINITION } from '../../shared/coding-plugins';
|
||||
|
||||
const exec = promisify(execFile);
|
||||
const roots: string[] = [];
|
||||
@@ -220,6 +220,57 @@ describe('PI-090 product tools', () => {
|
||||
)).rejects.toThrow('Unknown bundled coding skill');
|
||||
});
|
||||
|
||||
it('hides unassigned disabled plugin Skills and retains assigned ones as ineffective', async () => {
|
||||
const source = [{
|
||||
id: 'data-service',
|
||||
directory: path.resolve('resources/coding-plugins/data-service/skills/data-service'),
|
||||
available: false,
|
||||
}];
|
||||
const unassigned = await listProductCodingSkills(
|
||||
path.resolve('resources/coding-skills'), [], source,
|
||||
);
|
||||
expect(unassigned.some(({ id }) => id === 'data-service')).toBe(false);
|
||||
|
||||
const retained = await listProductCodingSkills(
|
||||
path.resolve('resources/coding-skills'), ['data-service'], source,
|
||||
);
|
||||
expect(retained.find(({ id }) => id === 'data-service')).toMatchObject({
|
||||
selected: true, available: false, effective: false,
|
||||
});
|
||||
|
||||
const reenabled = await listProductCodingSkills(
|
||||
path.resolve('resources/coding-skills'), ['data-service'], [{ ...source[0], available: true }],
|
||||
);
|
||||
expect(reenabled.find(({ id }) => id === 'data-service')).toMatchObject({
|
||||
selected: true, available: true, effective: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('projects effective plugin Skills in the worker runtime context', async () => {
|
||||
const root = await temporaryRoot('makelore-pi-runtime-context-');
|
||||
const tools = new PiProductTools({
|
||||
browser: {} as AgentBrowserModule,
|
||||
attachments: new CodingAttachmentStore(path.join(root, 'attachments')),
|
||||
bundledSkillsDir: path.resolve('resources/coding-skills'),
|
||||
pluginSkillSources: [{
|
||||
id: 'data-service',
|
||||
pluginId: 'makelore.data-service',
|
||||
directory: path.resolve('resources/coding-plugins/data-service/skills/data-service'),
|
||||
}],
|
||||
});
|
||||
const result = await tools.execute('runtime_context', {
|
||||
conversationId: 'conversation-a', runId: 'run-a', resourceId: 'resource-a',
|
||||
projectId: 'project-a', projectPath: root, skillIds: ['data-service'],
|
||||
}, {});
|
||||
|
||||
expect(result.details.schema).toBe('runtime-context.v1');
|
||||
if (result.details.schema !== 'runtime-context.v1') throw new Error('runtime context missing');
|
||||
expect(result.details.skills.find(({ id }) => id === 'data-service')).toMatchObject({
|
||||
id: 'data-service', selected: true, available: true, effective: true,
|
||||
});
|
||||
expect(result.details.commands).toContainEqual(expect.objectContaining({ skillId: 'data-service' }));
|
||||
});
|
||||
|
||||
it('accepts only safe versioned product detail projections', () => {
|
||||
expect(productToolDetails({
|
||||
schema: 'changed-file.v1', paths: ['src/app.ts', '.niancode/project.json'],
|
||||
@@ -351,28 +402,22 @@ describe('PI-090 product tools', () => {
|
||||
expect(JSON.stringify(result)).not.toContain('data:');
|
||||
});
|
||||
|
||||
it('dispatches all Data Service tools through the shared adapter and trusted project path', async () => {
|
||||
it('dispatches all Data Service tools only through the capability registry', async () => {
|
||||
const root = await temporaryRoot('makelore-pi-data-tools-');
|
||||
const response = (data: unknown) => ({
|
||||
success: true, status: 200, code: null, error: null, retryable: false, data,
|
||||
});
|
||||
const dataService = {
|
||||
configure: vi.fn().mockResolvedValue(response({ configured: true })),
|
||||
inspect: vi.fn().mockResolvedValue(response({ instance_id: 'instance-a' })),
|
||||
listProjects: vi.fn().mockResolvedValue(response({ items: [], total: 0, instance_limit: 20 })),
|
||||
getDocument: vi.fn().mockResolvedValue(response({ id: 'one', data: {}, revision: 1 })),
|
||||
listDocuments: vi.fn().mockResolvedValue(response({ items: [], next_cursor: null, limit: 50 })),
|
||||
putDocument: vi.fn().mockResolvedValue(response({ id: 'one', data: {}, revision: 1 })),
|
||||
deleteDocument: vi.fn().mockResolvedValue(response(null)),
|
||||
removeCollection: vi.fn().mockResolvedValue(response({ removed: true, usage: { document_count: 0, total_bytes: 0 } })),
|
||||
reset: vi.fn().mockResolvedValue(response({ instance_id: 'instance-a' })),
|
||||
removeProject: vi.fn().mockResolvedValue(response({ removed: true })),
|
||||
} as unknown as DataServiceOperations;
|
||||
const invoke = vi.fn().mockImplementation(({ toolName, context }) => Promise.resolve({
|
||||
content: [{ type: 'text', text: toolName }],
|
||||
details: {
|
||||
schema: 'makelore-capability.v1', operation: toolName,
|
||||
plugin_id: 'makelore.data-service', request_id: `pi:${context.runId}:${context.resourceId}`,
|
||||
billing: { mode: 'included', status: 'included' }, payload_schema: 'data-service.v1',
|
||||
success: true, status: 200, data: { delegated: true },
|
||||
},
|
||||
}));
|
||||
const tools = new PiProductTools({
|
||||
browser: {} as AgentBrowserModule,
|
||||
attachments: new CodingAttachmentStore(path.join(root, 'attachments')),
|
||||
bundledSkillsDir: path.resolve('resources/coding-skills'),
|
||||
dataService,
|
||||
capabilityRegistry: { invoke } as unknown as CodingCapabilityRegistry,
|
||||
});
|
||||
const context = {
|
||||
conversationId: 'conversation-a', runId: 'run-a', resourceId: 'resource-a',
|
||||
@@ -400,27 +445,15 @@ describe('PI-090 product tools', () => {
|
||||
await tools.execute('data_service_reset', context, { confirmed: true });
|
||||
const removed = await tools.execute('data_service_remove_project', context, { confirmed: true });
|
||||
|
||||
expect(dataService.configure).toHaveBeenCalledWith({ collections: ['todos'] }, root);
|
||||
expect(dataService.inspect).toHaveBeenCalledWith(root);
|
||||
expect(dataService.listProjects).toHaveBeenCalledWith();
|
||||
expect(dataService.getDocument).toHaveBeenCalledWith({ collection: 'todos', document_id: 'one' }, root);
|
||||
expect(dataService.listDocuments).toHaveBeenCalledWith({
|
||||
collection: 'todos', limit: 50, cursor: 'cursor-a',
|
||||
}, root);
|
||||
expect(dataService.putDocument).toHaveBeenCalledWith({
|
||||
collection: 'todos', document_id: 'one', data: { done: false }, if_revision: 1,
|
||||
}, root);
|
||||
expect(dataService.deleteDocument).toHaveBeenCalledWith({
|
||||
collection: 'todos', document_id: 'one', if_revision: 1, confirmed: true,
|
||||
}, root);
|
||||
expect(dataService.removeCollection).toHaveBeenCalledWith({ collection: 'todos', confirmed: true }, root);
|
||||
expect(dataService.reset).toHaveBeenCalledWith({ confirmed: true }, root);
|
||||
expect(dataService.removeProject).toHaveBeenCalledWith({ confirmed: true }, root);
|
||||
expect(invoke).toHaveBeenCalledTimes(DATA_SERVICE_PLUGIN_DEFINITION.tools.length);
|
||||
expect(invoke.mock.calls.map(([input]) => input.toolName)).toEqual(
|
||||
DATA_SERVICE_PLUGIN_DEFINITION.tools.map(({ name }) => name),
|
||||
);
|
||||
expect(removed.details).toMatchObject({
|
||||
schema: 'makelore-capability.v1', operation: 'remove_project',
|
||||
schema: 'makelore-capability.v1', operation: 'data_service_remove_project',
|
||||
plugin_id: 'makelore.data-service', request_id: 'pi:run-a:resource-a',
|
||||
billing: { mode: 'included', status: 'included' }, payload_schema: 'data-service.v1',
|
||||
success: true, status: 200, data: { removed: true },
|
||||
success: true, status: 200, data: { delegated: true },
|
||||
});
|
||||
expect(JSON.stringify(removed)).not.toContain(root);
|
||||
});
|
||||
@@ -453,37 +486,19 @@ describe('PI-090 product tools', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('rejects forbidden tool fields and destructive calls without literal confirmation', async () => {
|
||||
it('does not fabricate Data Service validation or billing without a registry', async () => {
|
||||
const root = await temporaryRoot('makelore-pi-data-input-');
|
||||
const dataService = {
|
||||
inspect: vi.fn().mockResolvedValue({
|
||||
success: true, status: 200, code: null, error: null, retryable: false, data: null,
|
||||
}),
|
||||
removeProject: vi.fn(),
|
||||
} as unknown as DataServiceOperations;
|
||||
const tools = new PiProductTools({
|
||||
browser: {} as AgentBrowserModule,
|
||||
attachments: new CodingAttachmentStore(path.join(root, 'attachments')),
|
||||
bundledSkillsDir: path.resolve('resources/coding-skills'),
|
||||
dataService,
|
||||
});
|
||||
const context = {
|
||||
conversationId: 'conversation-a', runId: 'run-a', resourceId: 'resource-a',
|
||||
projectId: 'local-project-a', projectPath: root, skillIds: [],
|
||||
};
|
||||
|
||||
await expect(tools.execute('data_service_inspect', context, { owner: 'owner-a' })).resolves.toMatchObject({
|
||||
details: { schema: 'makelore-capability.v1', code: 'plugin_input_invalid', status: 422 },
|
||||
});
|
||||
await expect(tools.execute('data_service_put_document', context, {
|
||||
collection: 'todos', document_id: 'one', data: {}, path: root,
|
||||
})).resolves.toMatchObject({
|
||||
details: { schema: 'makelore-capability.v1', code: 'plugin_input_invalid', status: 422 },
|
||||
});
|
||||
await expect(tools.execute('data_service_remove_project', context, { confirmed: false })).resolves.toMatchObject({
|
||||
details: { schema: 'makelore-capability.v1', code: 'plugin_input_invalid', status: 422 },
|
||||
});
|
||||
expect(dataService.inspect).not.toHaveBeenCalled();
|
||||
expect(dataService.removeProject).not.toHaveBeenCalled();
|
||||
await expect(tools.execute('data_service_inspect', context, { owner: 'owner-a' }))
|
||||
.rejects.toThrow('Product tool is unavailable');
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user