feat(pi): materialize effective plugin worker tools

This commit is contained in:
2026-08-27 17:24:16 +08:00
parent c4dd8923a0
commit fd891ff3bb
12 changed files with 617 additions and 212 deletions

View File

@@ -3,6 +3,7 @@ import { tmpdir } from 'node:os';
import path from 'node:path';
import { afterEach, describe, expect, it } from 'vitest';
import YAML from 'yaml';
import { CodingCapabilityRegistryImpl } from '@electron/coding-plugins/registry';
import type { PiProviderSelection } from '@electron/coding-runtime/pi/provider-config';
import {
buildPiManagedInputArgs,
@@ -11,6 +12,8 @@ import {
resolveBundledCodingSkillsDir,
resolveExplicitCodingSkillPaths,
} from '@electron/coding-runtime/pi/resource-loader';
import type { PluginPolicyClientState } from '@electron/services/plugin-policy-client';
import { DATA_SERVICE_PLUGIN_DEFINITION } from '../../shared/coding-plugins';
const temporaryRoots: string[] = [];
@@ -34,12 +37,18 @@ async function fixtureRoot(): Promise<{
mkdir(path.join(projectDir, '.agents', 'skills', 'untrusted-agent-skill'), { recursive: true }),
mkdir(path.join(skillsDir, 'grilling'), { recursive: true }),
mkdir(path.join(skillsDir, 'agent-browser'), { recursive: true }),
mkdir(path.join(path.dirname(skillsDir), 'coding-plugins', 'data-service', 'skills', 'data-service'), { recursive: true }),
]);
await Promise.all([
writeFile(path.join(projectDir, '.pi', 'skills', 'untrusted-project-skill', 'SKILL.md'), 'untrusted', 'utf8'),
writeFile(path.join(projectDir, '.agents', 'skills', 'untrusted-agent-skill', 'SKILL.md'), 'untrusted', 'utf8'),
writeFile(path.join(skillsDir, 'grilling', 'SKILL.md'), '---\nname: grilling\n---\n', 'utf8'),
writeFile(path.join(skillsDir, 'agent-browser', 'SKILL.md'), '---\nname: agent-browser\n---\n', 'utf8'),
writeFile(
path.join(path.dirname(skillsDir), 'coding-plugins', 'data-service', 'skills', 'data-service', 'SKILL.md'),
'---\nname: data-service\n---\n',
'utf8',
),
]);
return { root, userDataDir, projectDir, skillsDir };
}
@@ -53,13 +62,21 @@ describe('Pi managed resource loader', () => {
projectId: 'project-1',
agentId: 'agent-1',
prompt,
skillIds: ['grilling', 'grilling'],
skillEntries: [
{ id: 'grilling', entryPath: 'grilling/SKILL.md' },
{ id: 'grilling', entryPath: 'grilling/SKILL.md' },
],
catalogRevision: 11,
bundledSkillsDir: fixture.skillsDir,
revision: { provider: 3, resources: 7 },
});
expect(await readFile(resources.promptPath, 'utf8')).toBe(prompt);
expect(resources.skillIds).toEqual(['grilling']);
expect(resources.skillEntries).toEqual([
{ id: 'grilling', entryPath: 'grilling/SKILL.md' },
]);
expect(resources.catalogRevision).toBe(11);
expect(resources.skillPaths).toEqual([path.join(fixture.skillsDir, 'grilling', 'SKILL.md')]);
expect(JSON.stringify(resources.summary)).not.toContain(prompt);
const manifest = JSON.parse(await readFile(resources.manifestPath, 'utf8')) as Record<string, unknown>;
@@ -69,6 +86,8 @@ describe('Pi managed resource loader', () => {
agentId: 'agent-1',
promptFile: 'agent-1.md',
skillIds: ['grilling'],
skillEntries: [{ id: 'grilling', entryPath: 'grilling/SKILL.md' }],
catalogRevision: 11,
revision: { provider: 3, resources: 7 },
});
expect(JSON.stringify(manifest)).not.toContain(prompt);
@@ -84,7 +103,8 @@ describe('Pi managed resource loader', () => {
projectId: 'project-1',
agentId: 'agent-1',
prompt,
skillIds: ['agent-browser'],
skillEntries: [{ id: 'agent-browser', entryPath: 'agent-browser/SKILL.md' }],
catalogRevision: 12,
bundledSkillsDir: fixture.skillsDir,
revision: { provider: 1, resources: 1 },
});
@@ -111,16 +131,112 @@ describe('Pi managed resource loader', () => {
expect(serialized).not.toContain(path.join(fixture.projectDir, '.agents'));
});
it('materializes effective plugin Skill entries from their bundled package roots', async () => {
const fixture = await fixtureRoot();
const resources = await materializePiAgentResources({
userDataDir: fixture.userDataDir,
projectId: 'project-1',
agentId: 'agent-1',
prompt: 'plugin prompt',
skillEntries: [{ id: 'data-service', entryPath: 'skills/data-service/SKILL.md' }],
catalogRevision: 13,
bundledSkillsDir: fixture.skillsDir,
revision: { provider: 1, resources: 1 },
});
expect(resources.skillIds).toEqual(['data-service']);
expect(resources.skillPaths).toEqual([
path.join(path.dirname(fixture.skillsDir), 'coding-plugins', 'data-service', 'skills', 'data-service', 'SKILL.md'),
]);
expect(resources.catalogRevision).toBe(13);
});
it('filters a known disabled plugin Skill for the next worker and restores it after re-enable', async () => {
const fixture = await fixtureRoot();
const assignedSkillIds = ['grilling', 'data-service'];
let enabled = true;
const policy: PluginPolicyClientState = {
status: 'current',
revision: 17,
lastVerifiedAt: 1,
catalog: {
schema_version: 1,
catalog_version: 'catalog-17',
pricing_version: null,
plugins: [{
plugin_id: DATA_SERVICE_PLUGIN_DEFINITION.id,
supported_contract_versions: [DATA_SERVICE_PLUGIN_DEFINITION.contractVersion],
status: 'active',
capabilities: DATA_SERVICE_PLUGIN_DEFINITION.skills[0].grants.map((capabilityId) => ({
capability_id: capabilityId,
operations: DATA_SERVICE_PLUGIN_DEFINITION.tools
.filter((tool) => tool.capabilityId === capabilityId)
.map((tool) => ({
operation: tool.operation,
billing: { mode: 'included' as const, entitlement_scope: null, notice: 'Included' },
})),
})),
}],
},
};
const registry = new CodingCapabilityRegistryImpl({
policyClient: { getState: () => policy },
getEnabledPluginIds: async () => enabled ? [DATA_SERVICE_PLUGIN_DEFINITION.id] : [],
adapters: [{
pluginId: DATA_SERVICE_PLUGIN_DEFINITION.id,
async inspect() { return { status: 'ready' }; },
async invoke() {
return {
success: true, status: 200, code: null, error: null, retryable: false,
payload_schema: 'data-service.v1', data: null,
};
},
}],
});
const resolve = async () => await registry.resolveWorkerResources({
projectPath: fixture.projectDir,
assignedSkillIds,
role: 'parent',
});
const first = await resolve();
expect(first.effectiveSkillIds).toEqual(assignedSkillIds);
expect(first.tools).toHaveLength(10);
enabled = false;
const disabled = await resolve();
expect(assignedSkillIds).toEqual(['grilling', 'data-service']);
expect(disabled.effectiveSkillIds).toEqual(['grilling']);
expect(disabled.tools).toEqual([]);
await expect(materializePiAgentResources({
userDataDir: fixture.userDataDir,
projectId: 'project-1',
agentId: 'agent-1',
prompt: 'disabled plugin prompt',
skillEntries: disabled.skillEntries,
catalogRevision: disabled.catalogRevision,
bundledSkillsDir: fixture.skillsDir,
revision: { provider: 1, resources: 2 },
})).resolves.toMatchObject({ skillIds: ['grilling'], catalogRevision: 17 });
enabled = true;
const restored = await resolve();
expect(restored.effectiveSkillIds).toEqual(assignedSkillIds);
expect(restored.tools).toHaveLength(10);
});
it('rejects unknown skills and unsafe managed path segments', async () => {
const fixture = await fixtureRoot();
await expect(resolveExplicitCodingSkillPaths(fixture.skillsDir, ['not-bundled']))
.rejects.toThrow('Unknown bundled coding skill');
await expect(resolveExplicitCodingSkillPaths(fixture.skillsDir, [
{ id: 'not-bundled', entryPath: 'not-bundled/SKILL.md' },
])).rejects.toThrow('Bundled coding Skill entry is not a file');
await expect(materializePiAgentResources({
userDataDir: fixture.userDataDir,
projectId: '../outside',
agentId: 'agent-1',
prompt: '',
skillIds: [],
skillEntries: [],
catalogRevision: 1,
bundledSkillsDir: fixture.skillsDir,
revision: { provider: 1, resources: 1 },
})).rejects.toThrow('Project id');