feat(coding): add Pi Data Service product tools

This commit is contained in:
2026-08-26 19:58:38 +08:00
parent 44bcdf52fc
commit 1d63233cd0
12 changed files with 790 additions and 26 deletions

View File

@@ -15,9 +15,24 @@ import { PiProductTools } from '../../electron/coding-runtime/pi/product-tools';
type ExtensionHandler = (...arguments_: unknown[]) => Promise<unknown> | unknown;
type ExtensionTool = {
name: string;
parameters?: Record<string, unknown>;
execute?: (...arguments_: unknown[]) => Promise<unknown>;
};
async function post(
registration: Awaited<ReturnType<PiManagedExtensionHost['registerWorker']>>,
body: Record<string, unknown>,
): Promise<Response> {
return await fetch(registration.env.MAKELORE_PI_BRIDGE_URL as string, {
method: 'POST',
headers: {
authorization: `Bearer ${registration.env.MAKELORE_PI_WORKER_TOKEN}`,
'content-type': 'application/json',
},
body: JSON.stringify(body),
});
}
const roots: string[] = [];
const hosts: PiManagedExtensionHost[] = [];
@@ -209,7 +224,26 @@ describe('Makelore Pi extension bundle', () => {
expect([...tools.keys()]).toEqual([
'ask_user', 'subagent', 'agent_browser', 'game_asset_browser',
'game_asset_review', 'task_state', 'changed_file', 'runtime_context',
'data_service_configure', 'data_service_inspect', 'data_service_list_projects',
'data_service_get_document', 'data_service_list_documents', 'data_service_put_document',
'data_service_delete_document', 'data_service_remove_collection', 'data_service_reset',
'data_service_remove_project',
]);
const dataServiceTools = [
'data_service_configure', 'data_service_inspect', 'data_service_list_projects',
'data_service_get_document', 'data_service_list_documents', 'data_service_put_document',
'data_service_delete_document', 'data_service_remove_collection', 'data_service_reset',
'data_service_remove_project',
];
for (const name of dataServiceTools) {
const tool = tools.get(name);
expect(tool?.parameters).toMatchObject({
type: 'object', additionalProperties: false,
});
expect(Object.keys(tool?.parameters?.properties ?? {})).not.toEqual(
expect.arrayContaining(['owner', 'project', 'path', 'token', 'endpoint', 'url', 'handle']),
);
}
const updates: unknown[] = [];
const subagentResult = await tools.get('subagent')?.execute?.(
@@ -317,6 +351,12 @@ describe('Makelore Pi extension bundle', () => {
on: (event, handler) => handlers.set(event, handler),
});
expect(tools).toEqual([]);
const forgedParentTool = await post(child, {
action: 'product.invoke', conversationId: 'conversation-child', workerGeneration: 1,
runId: 'run-parent', resourceId: 'forged-data-tool',
toolName: 'data_service_inspect', input: {},
});
expect(forgedParentTool.status).toBe(403);
await writeFile(path.join(root, 'child.txt'), 'after\n', 'utf8');
await handlers.get('tool_call')?.({
toolName: 'write', toolCallId: 'child-write', input: { path: 'child.txt' },

View File

@@ -5,7 +5,7 @@ import { mkdtemp, mkdir, rm, utimes, writeFile } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import path from 'node:path';
import { promisify } from 'node:util';
import { afterEach, describe, expect, it } from 'vitest';
import { afterEach, describe, expect, it, vi } from 'vitest';
import type { AgentBrowserModule } from '../../electron/agent-browser';
import { CodingAttachmentStore } from '../../electron/coding-projects/attachment-store';
import {
@@ -18,6 +18,7 @@ import {
} from '../../electron/coding-projects/skill-registry';
import { PiProductTools } from '../../electron/coding-runtime/pi/product-tools';
import { productToolDetails } from '../../electron/coding-runtime/product-tool-protocol';
import type { DataServiceOperations } from '../../electron/services/data-service-client';
const exec = promisify(execFile);
const roots: string[] = [];
@@ -231,6 +232,36 @@ describe('PI-090 product tools', () => {
schema: 'agent-browser.v1', action: 'send_cdp', attachmentId: 'attachment-a',
})).toBeNull();
expect(productToolDetails({ schema: 'task-state.v1', tasks: [] })).toBeNull();
expect(productToolDetails({
schema: 'data-service.v1',
operation: 'data_service_inspect',
success: true,
status: 200,
code: null,
error: null,
retryable: false,
data: { instance_id: 'instance-a' },
owner: 'must-be-dropped',
})).toEqual({
schema: 'data-service.v1',
operation: 'data_service_inspect',
success: true,
status: 200,
code: null,
error: null,
retryable: false,
data: { instance_id: 'instance-a' },
});
expect(productToolDetails({
schema: 'data-service.v1',
operation: 'data_service_unknown',
success: true,
status: 200,
code: null,
error: null,
retryable: false,
data: null,
})).toBeNull();
});
it('stores browser screenshots as attachment ids and never returns base64', async () => {
@@ -288,4 +319,108 @@ describe('PI-090 product tools', () => {
expect(JSON.stringify(result)).not.toContain(root);
expect(JSON.stringify(result)).not.toContain('data:');
});
it('dispatches all Data Service tools through the shared adapter and trusted project path', 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 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 tools.execute('data_service_configure', context, { collections: ['todos'] });
await tools.execute('data_service_inspect', context, {});
await tools.execute('data_service_list_projects', context, {});
await tools.execute('data_service_get_document', context, {
collection: 'todos', document_id: 'one',
});
await tools.execute('data_service_list_documents', context, {
collection: 'todos', limit: 50, cursor: 'cursor-a',
});
await tools.execute('data_service_put_document', context, {
collection: 'todos', document_id: 'one', data: { done: false }, if_revision: 1,
});
await tools.execute('data_service_delete_document', context, {
collection: 'todos', document_id: 'one', if_revision: 1, confirmed: true,
});
await tools.execute('data_service_remove_collection', context, {
collection: 'todos', confirmed: true,
});
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(removed.details).toMatchObject({
schema: 'data-service.v1', operation: 'data_service_remove_project',
success: true, status: 200, data: { removed: true },
});
expect(JSON.stringify(removed)).not.toContain(root);
});
it('rejects forbidden tool fields and destructive calls without literal confirmation', 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' })).rejects.toThrow(
'Data Service tool input is invalid',
);
await expect(tools.execute('data_service_put_document', context, {
collection: 'todos', document_id: 'one', data: {}, path: root,
})).rejects.toThrow('Data Service tool input is invalid');
await expect(tools.execute('data_service_remove_project', context, { confirmed: false })).rejects.toThrow(
'Data Service tool input is invalid',
);
expect(dataService.inspect).not.toHaveBeenCalled();
expect(dataService.removeProject).not.toHaveBeenCalled();
});
});