fix(plugins): load official hosted plugins from bundled resources

This commit is contained in:
2026-09-01 18:20:24 +08:00
parent 52e2a97a12
commit 1530ac7740
23 changed files with 835 additions and 48 deletions

View File

@@ -395,4 +395,58 @@ describe('effective plugin resolver', () => {
})).resolves.toMatchObject({ effectiveSkillIds: ['notes'], toolDefinitions: [], runtimePolicies: [] });
expect(refresh).not.toHaveBeenCalled();
});
it('materializes an acquired code-owned bundled hosted Plugin without Package Store bytes', async () => {
const bundled: CodingPluginDefinition = {
...serverDefinition,
id: 'makelore.web-search',
releaseId: '00000000-0000-4000-8000-000000000204',
provenance: { source: 'bundled', packageRoot: 'web-search' },
skills: [{
id: 'makelore-web-search',
entryPath: 'skills/makelore-web-search/SKILL.md',
grants: ['remote.read'],
}],
};
const getInstalled = vi.fn(async () => null);
const getInstalledRelease = vi.fn(async () => null);
const policy = currentPolicy();
policy.catalog.plugins[0] = { ...policy.catalog.plugins[0], plugin_id: bundled.id };
const effective = createEffectivePluginResolver({
definitions: [bundled],
getAccountBinding: () => binding,
getLibrary: vi.fn(async () => ({
...library(),
items: [{ ...library().items[0], pluginId: bundled.id }],
})),
packageStore: {
readInstalledIndex: vi.fn(async () => []),
getInstalled,
getInstalledRelease,
},
getEnabledPluginIds: vi.fn(async () => [bundled.id]),
policyClient: { getState: () => policy, refresh: vi.fn() },
});
await expect(effective.resolve({
projectId: 'project-a',
projectPath: 'C:/project-a',
assignedSkillIds: ['makelore-web-search'],
role: 'parent',
})).resolves.toMatchObject({
pluginReleaseIds: [bundled.releaseId],
effectiveSkillIds: ['makelore-web-search'],
skillEntries: [{
id: 'makelore-web-search',
entryPath: 'skills/makelore-web-search/SKILL.md',
}],
toolDefinitions: [{ name: 'remote_read' }],
unavailableReasons: [],
});
expect(getInstalled).not.toHaveBeenCalled();
await expect(effective.getSkillSources()).resolves.toEqual([]);
await expect(effective.getInstalledDefinition(bundled.id, bundled.releaseId))
.resolves.toMatchObject({ id: bundled.id, releaseId: bundled.releaseId });
expect(getInstalledRelease).not.toHaveBeenCalled();
});
});

View File

@@ -20,6 +20,8 @@ import {
} from '../../shared/coding-plugins';
const PACKAGE_ROOT = path.resolve('resources/coding-plugins/data-service');
const GAME_RESOURCE_ROOT = path.resolve('resources/coding-plugins/game-resource');
const WEB_SEARCH_ROOT = path.resolve('resources/coding-plugins/web-search');
async function packageManifests(): Promise<{ root: Record<string, unknown>; capability: Record<string, unknown> }> {
return {
@@ -32,9 +34,13 @@ describe('bundled coding plugin manifests', () => {
it('loads the fixed Data Service package and immutable declarations', async () => {
const definitions = await loadBundledCodingPluginDefinitions(path.resolve('resources/coding-plugins'));
const startupDefinitions = loadBundledCodingPluginDefinitionsSync(path.resolve('resources/coding-plugins'));
expect(BUNDLED_CODING_PLUGIN_ROOTS).toEqual(['data-service']);
expect(resolveBundledCodingPluginRootPaths(path.resolve('resources/coding-plugins'))).toEqual([PACKAGE_ROOT]);
expect(definitions).toHaveLength(1);
expect(BUNDLED_CODING_PLUGIN_ROOTS).toEqual(['data-service', 'game-resource', 'web-search']);
expect(resolveBundledCodingPluginRootPaths(path.resolve('resources/coding-plugins'))).toEqual([
PACKAGE_ROOT,
GAME_RESOURCE_ROOT,
WEB_SEARCH_ROOT,
]);
expect(definitions).toHaveLength(3);
expect(definitions[0]).toMatchObject({
id: 'makelore.data-service',
adapterId: 'data-service',
@@ -42,7 +48,21 @@ describe('bundled coding plugin manifests', () => {
skills: [{ id: 'data-service', entryPath: 'skills/data-service/SKILL.md' }],
});
expect(definitions[0]?.tools.map(({ name }) => name)).toEqual(DATA_SERVICE_TOOL_NAMES);
expect(definitions).toEqual([DATA_SERVICE_PLUGIN_DEFINITION]);
expect(definitions[0]).toEqual(DATA_SERVICE_PLUGIN_DEFINITION);
expect(definitions.slice(1)).toMatchObject([
{
id: 'makelore.game-resource', version: '1.0.0', runtimeKind: 'platform_hosted',
acquisitionMode: 'user_acquired', releaseId: '00000000-0000-4000-8000-000000000105',
provenance: { source: 'bundled', packageRoot: 'game-resource' },
skills: [{ id: 'game-resource', entryPath: 'skills/game-resource/SKILL.md' }],
},
{
id: 'makelore.web-search', version: '1.0.0', runtimeKind: 'platform_hosted',
acquisitionMode: 'user_acquired', releaseId: '00000000-0000-4000-8000-000000000204',
provenance: { source: 'bundled', packageRoot: 'web-search' },
skills: [{ id: 'makelore-web-search', entryPath: 'skills/makelore-web-search/SKILL.md' }],
},
]);
expect(startupDefinitions).toEqual(definitions);
expect(Object.isFrozen(startupDefinitions)).toBe(true);
expect(Object.isFrozen(startupDefinitions[0]?.operations)).toBe(true);
@@ -115,6 +135,8 @@ describe('bundled coding plugin manifests', () => {
it('does not infer package roots from arbitrary directories', async () => {
expect(resolveBundledCodingPluginRootPaths(path.resolve('tmp'))).toEqual([
path.resolve('tmp/data-service'),
path.resolve('tmp/game-resource'),
path.resolve('tmp/web-search'),
]);
});

View File

@@ -135,4 +135,30 @@ describe('MarketplaceHostedAdmissionResolver', () => {
retryable: false,
});
});
it('resolves a code-owned bundled Release without consulting the device Package Store', async () => {
const getInstalledRelease = vi.fn(async () => null);
const resolve = vi.fn(async () => resolved({ sha256: null, sizeBytes: null }));
const options = {
packageStore: { getInstalledRelease },
marketplace: { resolve },
makeloreVersion: '2.0.0',
bundledReleases: {
[PLUGIN_ID]: { releaseId: RELEASE_ID, version: '1.0.0', channel: 'stable' as const },
},
} as unknown as ConstructorParameters<typeof MarketplaceHostedAdmissionResolver>[0];
const bundledResolver = new MarketplaceHostedAdmissionResolver(options);
await expect(bundledResolver.resolve({
pluginId: PLUGIN_ID,
workerSnapshot: { requestId: REQUEST_ID, pluginReleaseId: RELEASE_ID },
})).resolves.toEqual({ releaseId: RELEASE_ID, releaseAdmissionId: 'admission-game-1' });
expect(getInstalledRelease).not.toHaveBeenCalled();
expect(resolve).toHaveBeenCalledWith({
resolveRequestId: REQUEST_ID,
makeloreVersion: '2.0.0',
channel: 'stable',
installed: [],
});
});
});

View File

@@ -74,20 +74,24 @@ function matchingMetadata() {
};
}
async function bundledResourceFixture() {
async function bundledResourceFixture({ schemaVersion = 1, includeSdkAssets = true } = {}) {
const root = await mkdtemp(path.join(tmpdir(), 'makelore-bundled-plugin-'));
roots.push(root);
const projectRoot = path.join(root, 'project');
const sourceResources = path.join(projectRoot, 'resources');
const pluginRoot = path.join(sourceResources, 'coding-plugins', 'example');
const capability = {
schemaVersion,
pluginId: 'example.plugin',
adapterId: 'example-adapter',
...(schemaVersion === 2
? { runtime: { kind: 'platform_hosted', protocol: 'makelore-hosted.v1' } }
: {}),
skills: [{ id: 'example', entry: '../skills/example/SKILL.md', grants: [] }],
tools: [{ name: 'example_read' }],
};
await mkdir(path.join(pluginRoot, 'com.makelore'), { recursive: true });
await mkdir(path.join(pluginRoot, 'skills', 'example', 'assets'), { recursive: true });
await mkdir(path.join(pluginRoot, 'skills', 'example'), { recursive: true });
await mkdir(path.join(sourceResources, 'coding-skills', 'agent-browser'), { recursive: true });
await writeFile(path.join(pluginRoot, 'plugin.json'), JSON.stringify({
name: 'example.plugin',
@@ -96,8 +100,11 @@ async function bundledResourceFixture() {
}));
await writeFile(path.join(pluginRoot, 'com.makelore', 'capability.json'), JSON.stringify(capability));
await writeFile(path.join(pluginRoot, 'skills', 'example', 'SKILL.md'), '# Example Skill\n');
await writeFile(path.join(pluginRoot, 'skills', 'example', 'assets', 'sdk.ts'), 'export {};\n');
await writeFile(path.join(pluginRoot, 'skills', 'example', 'assets', 'sdk.js'), 'export {};\n');
if (includeSdkAssets) {
await mkdir(path.join(pluginRoot, 'skills', 'example', 'assets'), { recursive: true });
await writeFile(path.join(pluginRoot, 'skills', 'example', 'assets', 'sdk.ts'), 'export {};\n');
await writeFile(path.join(pluginRoot, 'skills', 'example', 'assets', 'sdk.js'), 'export {};\n');
}
await writeFile(path.join(sourceResources, 'coding-skills', 'agent-browser', 'SKILL.md'), '# Browser\n');
const resourcesDirectory = path.join(root, 'packaged');
await cp(sourceResources, path.join(resourcesDirectory, 'resources'), { recursive: true });
@@ -206,6 +213,20 @@ describe('final Pi product artifact verification', () => {
});
});
it('accepts schema-2 hosted plugins whose implementation has no client SDK assets', async () => {
const fixture = await bundledResourceFixture({ schemaVersion: 2, includeSdkAssets: false });
await expect(verifyBundledCodingPluginResources(fixture)).resolves.toMatchObject({
packages: [{
id: 'example.plugin',
skills: [{ id: 'example', path: 'skills/example/SKILL.md' }],
sdkAssets: [],
adapterId: 'example.plugin',
tools: ['example_read'],
}],
result: 'pass',
});
});
it('proves the packaged Marketplace trust, routes, effective snapshot, and Renderer assets', () => {
const artifact = Buffer.from(MARKETPLACE_ARTIFACT_TEXT);

View File

@@ -40,6 +40,8 @@ async function fixtureRoot(): Promise<{
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 }),
mkdir(path.join(path.dirname(skillsDir), 'coding-plugins', 'game-resource', 'skills', 'game-resource'), { recursive: true }),
mkdir(path.join(path.dirname(skillsDir), 'coding-plugins', 'web-search', 'skills', 'makelore-web-search'), { recursive: true }),
]);
await Promise.all([
writeFile(path.join(projectDir, '.pi', 'skills', 'untrusted-project-skill', 'SKILL.md'), 'untrusted', 'utf8'),
@@ -51,6 +53,16 @@ async function fixtureRoot(): Promise<{
'---\nname: data-service\n---\n',
'utf8',
),
writeFile(
path.join(path.dirname(skillsDir), 'coding-plugins', 'game-resource', 'skills', 'game-resource', 'SKILL.md'),
'---\nname: game-resource\n---\n',
'utf8',
),
writeFile(
path.join(path.dirname(skillsDir), 'coding-plugins', 'web-search', 'skills', 'makelore-web-search', 'SKILL.md'),
'---\nname: makelore-web-search\n---\n',
'utf8',
),
]);
return { root, userDataDir, projectDir, skillsDir };
}
@@ -160,15 +172,21 @@ describe('Pi managed resource loader', () => {
projectId: 'project-1',
agentId: 'agent-1',
prompt: 'plugin prompt',
skillEntries: [{ id: 'data-service', entryPath: 'skills/data-service/SKILL.md' }],
skillEntries: [
{ id: 'data-service', entryPath: 'skills/data-service/SKILL.md' },
{ id: 'game-resource', entryPath: 'skills/game-resource/SKILL.md' },
{ id: 'makelore-web-search', entryPath: 'skills/makelore-web-search/SKILL.md' },
],
catalogRevision: 13,
bundledSkillsDir: fixture.skillsDir,
revision: { provider: 1, resources: 1 },
});
expect(resources.skillIds).toEqual(['data-service']);
expect(resources.skillIds).toEqual(['data-service', 'game-resource', 'makelore-web-search']);
expect(resources.skillPaths).toEqual([
path.join(path.dirname(fixture.skillsDir), 'coding-plugins', 'data-service', 'skills', 'data-service', 'SKILL.md'),
path.join(path.dirname(fixture.skillsDir), 'coding-plugins', 'game-resource', 'skills', 'game-resource', 'SKILL.md'),
path.join(path.dirname(fixture.skillsDir), 'coding-plugins', 'web-search', 'skills', 'makelore-web-search', 'SKILL.md'),
]);
expect(resources.catalogRevision).toBe(13);
});

View File

@@ -129,6 +129,29 @@ describe('My Plugins', () => {
expect(screen.getByRole('link', { name: '分配给伙伴' })).toBeVisible();
});
it('shows acquired code-owned bundled Plugins as supplied without device download actions', () => {
const bundled = {
...library.items[0],
pluginId: 'makelore.web-search',
title: '联网搜索',
stableVersion: '1.0.0',
betaVersion: null,
};
render(<MemoryRouter><MyPluginsView
library={{ ...library, items: [bundled] }} installations={{}} state="ready"
pending={{}} onRefresh={vi.fn()} onInstall={vi.fn()} onUpdate={vi.fn()}
onInstallBeta={vi.fn()} onUninstall={vi.fn()} onRemove={vi.fn()} onReacquire={vi.fn()}
/></MemoryRouter>);
expect(screen.getByText('随 MakeLore 提供')).toBeVisible();
expect(screen.queryByText('尚未下载到设备')).not.toBeInTheDocument();
expect(screen.queryByRole('button', { name: '下载联网搜索' })).not.toBeInTheDocument();
expect(screen.queryByRole('button', { name: '更新联网搜索' })).not.toBeInTheDocument();
expect(screen.queryByRole('button', { name: '删除设备上的联网搜索' })).not.toBeInTheDocument();
expect(screen.getByRole('button', { name: '移除联网搜索' })).toBeVisible();
expect(screen.getByRole('link', { name: '启用到项目' })).toBeVisible();
});
it('updates an installed Beta package only through the explicit Beta action', () => {
const onUpdate = vi.fn();
const onInstallBeta = vi.fn();