393 lines
19 KiB
TypeScript
393 lines
19 KiB
TypeScript
// @vitest-environment node
|
|
|
|
import { cp, mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises';
|
|
import { createRequire } from 'node:module';
|
|
import { tmpdir } from 'node:os';
|
|
import path from 'node:path';
|
|
import { finished } from 'node:stream/promises';
|
|
import { afterEach, describe, expect, it } from 'vitest';
|
|
|
|
import {
|
|
assertNodeEngineCompatible,
|
|
classifyOpenCodeResourcePaths,
|
|
collectAbsoluteManifestValues,
|
|
collectForbiddenAsarPaths,
|
|
collectForbiddenResourcePaths,
|
|
verifyBundledCodingPluginResources,
|
|
defaultProductExecutable,
|
|
readPackagedMarketplaceTrustSource,
|
|
verifyPackagedMarketplaceClientArtifact,
|
|
validatePiArtifactMetadata,
|
|
verifyMarketplaceClientArtifact,
|
|
} from '../../scripts/lib/pi-product-artifact.mjs';
|
|
import { parsePiArtifactVerifierArgs } from '../../scripts/verify-pi-product-artifact.mjs';
|
|
|
|
const roots: string[] = [];
|
|
const PI_PACKAGE = '@earendil-works/pi-coding-agent';
|
|
const { createPackage } = createRequire(import.meta.url)('@electron/asar');
|
|
|
|
async function createAsarFixture(source: string, archive: string) {
|
|
const output = await createPackage(source, archive);
|
|
await finished(output);
|
|
}
|
|
|
|
afterEach(async () => {
|
|
await Promise.all(roots.splice(0).map((root) => rm(root, { recursive: true, force: true })));
|
|
});
|
|
|
|
function matchingMetadata() {
|
|
return {
|
|
rootPackage: { dependencies: { [PI_PACKAGE]: '0.84.2' } },
|
|
lockfile: {
|
|
importers: {
|
|
'.': { dependencies: { [PI_PACKAGE]: { specifier: '0.84.2', version: '0.84.2(ws@8.20.0)' } } },
|
|
},
|
|
},
|
|
packagedPackage: { dependencies: { [PI_PACKAGE]: '0.84.2' } },
|
|
runtimePackage: {
|
|
name: PI_PACKAGE,
|
|
version: '0.84.2',
|
|
bin: { pi: 'dist/cli.js' },
|
|
},
|
|
manifest: {
|
|
runtime: {
|
|
packageName: PI_PACKAGE,
|
|
version: '0.84.2',
|
|
cliEntry: 'dist/cli.js',
|
|
nodeEngine: '>=22.19.0',
|
|
},
|
|
target: { platform: 'win32', arch: 'x64' },
|
|
},
|
|
runtimePlatform: { platform: 'win32', arch: 'x64', node: '24.18.1' },
|
|
};
|
|
}
|
|
|
|
async function bundledResourceFixture() {
|
|
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 = {
|
|
pluginId: 'example.plugin',
|
|
adapterId: 'example-adapter',
|
|
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(sourceResources, 'coding-skills', 'agent-browser'), { recursive: true });
|
|
await writeFile(path.join(pluginRoot, 'plugin.json'), JSON.stringify({
|
|
name: 'example.plugin',
|
|
version: '1.0.0',
|
|
extensions: { 'com.makelore': { capabilityManifest: './com.makelore/capability.json' } },
|
|
}));
|
|
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');
|
|
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 });
|
|
return {
|
|
projectRoot,
|
|
resourcesDirectory,
|
|
appAsarContents: Buffer.from('example.plugin example-adapter example_read'),
|
|
};
|
|
}
|
|
|
|
describe('final Pi product artifact verification', () => {
|
|
it('requires the same pinned Pi version across package, lock, app, runtime, and manifest', () => {
|
|
expect(validatePiArtifactMetadata(matchingMetadata())).toMatchObject({
|
|
expected: '0.84.2',
|
|
rootDependency: '0.84.2',
|
|
lockedVersion: '0.84.2',
|
|
manifest: '0.84.2',
|
|
});
|
|
const mismatch = matchingMetadata();
|
|
mismatch.packagedPackage.dependencies[PI_PACKAGE] = '0.84.1';
|
|
expect(() => validatePiArtifactMetadata(mismatch)).toThrow('versions do not match');
|
|
});
|
|
|
|
it('checks the packaged Node version against the exact supported engine shape', () => {
|
|
expect(() => assertNodeEngineCompatible('>=22.19.0', '24.18.1')).not.toThrow();
|
|
expect(() => assertNodeEngineCompatible('>=22.19.0', '22.18.9')).toThrow('does not satisfy');
|
|
expect(() => assertNodeEngineCompatible('^22.19.0', '24.18.1')).toThrow('Unsupported');
|
|
});
|
|
|
|
it('rejects absolute manifest values and OpenCode-named artifact resources', async () => {
|
|
expect(collectAbsoluteManifestValues({
|
|
entry: 'dist/cli.js',
|
|
asset: 'node_modules/example/file.wasm',
|
|
leaked: 'D:\\work\\pi-runtime',
|
|
})).toEqual([{ at: '$.leaked', value: 'D:\\work\\pi-runtime' }]);
|
|
|
|
const root = await mkdtemp(path.join(tmpdir(), 'makelore-pi-artifact-test-'));
|
|
roots.push(root);
|
|
await mkdir(path.join(root, 'pi-runtime', 'node_modules', 'opencode-ai'), { recursive: true });
|
|
await writeFile(path.join(root, 'pi-runtime', 'node_modules', 'opencode-ai', 'package.json'), '{}');
|
|
expect(await collectForbiddenResourcePaths(root)).toEqual([
|
|
'pi-runtime/node_modules/opencode-ai',
|
|
]);
|
|
expect(classifyOpenCodeResourcePaths([
|
|
'app.asar.unpacked/resources/opencode-runtime',
|
|
'pi-runtime/node_modules/@earendil-works/pi-ai/dist/providers/opencode.js',
|
|
'app.asar/node_modules/@earendil-works/pi-ai/dist/providers/opencode-codex-responses.js',
|
|
])).toEqual({
|
|
productOwned: ['app.asar.unpacked/resources/opencode-runtime'],
|
|
upstreamPiProvider: [
|
|
'pi-runtime/node_modules/@earendil-works/pi-ai/dist/providers/opencode.js',
|
|
'app.asar/node_modules/@earendil-works/pi-ai/dist/providers/opencode-codex-responses.js',
|
|
],
|
|
});
|
|
});
|
|
|
|
it('enumerates OpenCode-named paths inside app.asar instead of scanning only physical resources', async () => {
|
|
const root = await mkdtemp(path.join(tmpdir(), 'makelore-pi-asar-paths-'));
|
|
roots.push(root);
|
|
const source = path.join(root, 'source');
|
|
await mkdir(path.join(source, 'node_modules', '@earendil-works', 'pi-ai', 'dist', 'providers'), {
|
|
recursive: true,
|
|
});
|
|
await mkdir(path.join(source, 'resources', 'opencode-runtime'), { recursive: true });
|
|
await writeFile(
|
|
path.join(source, 'node_modules', '@earendil-works', 'pi-ai', 'dist', 'providers', 'opencode.js'),
|
|
'export {};',
|
|
);
|
|
await writeFile(path.join(source, 'resources', 'opencode-runtime', 'legacy.js'), 'export {};');
|
|
const archive = path.join(root, 'app.asar');
|
|
await createAsarFixture(source, archive);
|
|
|
|
const inventory = collectForbiddenAsarPaths(archive);
|
|
expect(inventory.entryCount).toBeGreaterThan(2);
|
|
expect(inventory.matches).toEqual(expect.arrayContaining([
|
|
'app.asar/node_modules/@earendil-works/pi-ai/dist/providers/opencode.js',
|
|
'app.asar/resources/opencode-runtime',
|
|
]));
|
|
expect(classifyOpenCodeResourcePaths(inventory.matches)).toMatchObject({
|
|
productOwned: expect.arrayContaining(['app.asar/resources/opencode-runtime']),
|
|
upstreamPiProvider: expect.arrayContaining([
|
|
'app.asar/node_modules/@earendil-works/pi-ai/dist/providers/opencode.js',
|
|
]),
|
|
});
|
|
});
|
|
|
|
it('proves bundled plugin manifests, Skills, SDK assets, catalog, and core resources', async () => {
|
|
const fixture = await bundledResourceFixture();
|
|
await expect(verifyBundledCodingPluginResources(fixture)).resolves.toMatchObject({
|
|
root: 'resources/coding-plugins',
|
|
packages: [{
|
|
id: 'example.plugin',
|
|
manifestPath: 'resources/coding-plugins/example/plugin.json',
|
|
capabilityManifestPath: 'resources/coding-plugins/example/com.makelore/capability.json',
|
|
skills: [{ id: 'example', path: 'skills/example/SKILL.md' }],
|
|
sdkAssets: expect.arrayContaining([
|
|
'resources/coding-plugins/example/skills/example/assets/sdk.ts',
|
|
'resources/coding-plugins/example/skills/example/assets/sdk.js',
|
|
]),
|
|
adapterId: 'example-adapter',
|
|
tools: ['example_read'],
|
|
}],
|
|
catalog: { adapters: ['example-adapter'], tools: ['example_read'], packagedInAsar: true },
|
|
coreResources: { root: 'resources/coding-skills', skills: ['agent-browser'] },
|
|
result: 'pass',
|
|
});
|
|
});
|
|
|
|
it('proves the packaged Marketplace trust, routes, effective snapshot, and Renderer assets', () => {
|
|
const artifact = Buffer.from([
|
|
'makelore-plugin-release.v1', 'skill_only', 'plugin_signature_invalid',
|
|
'signing key is not trusted', '/api/coding/plugin-marketplace',
|
|
'plugin-marketplace\\/install\\/', 'plugin-marketplace\\/update\\/',
|
|
'effectiveSkillIds', 'pluginReleaseIds',
|
|
'/api/coding/plugin-marketplace/catalog', '/api/coding/plugin-marketplace/library',
|
|
'免费获取', '我的插件',
|
|
].join('\n'));
|
|
|
|
const trustSource = `export const CODE_OWNED_PLUGIN_SIGNING_KEYS = Object.freeze(
|
|
{} as Readonly<Record<string, string>>,
|
|
);
|
|
export const sourceMarker = 'makelore.plugin-trust.code-owned.v1';`;
|
|
expect(verifyMarketplaceClientArtifact(artifact, trustSource)).toMatchObject({
|
|
schema2SkillOnly: true,
|
|
productionTrust: 'official-key-absent-fail-closed',
|
|
libraryInstallAndEffectiveRoutes: true,
|
|
rendererAssets: true,
|
|
productionKeyIds: [],
|
|
privateKeyMaterialInTrustSource: false,
|
|
result: 'pass',
|
|
});
|
|
});
|
|
|
|
it('rejects a packaged Marketplace missing a required asset or containing a private key', () => {
|
|
const emptyTrust = 'CODE_OWNED_PLUGIN_SIGNING_KEYS = Object.freeze({} as Readonly<Record<string, string>>);';
|
|
expect(() => verifyMarketplaceClientArtifact(Buffer.from('makelore-plugin-release.v1'), emptyTrust))
|
|
.toThrow('Marketplace contract markers');
|
|
|
|
const complete = Buffer.from([
|
|
'makelore-plugin-release.v1', 'skill_only', 'plugin_signature_invalid',
|
|
'signing key is not trusted', '/api/coding/plugin-marketplace',
|
|
'plugin-marketplace\\/install\\/', 'plugin-marketplace\\/update\\/',
|
|
'effectiveSkillIds', 'pluginReleaseIds',
|
|
'/api/coding/plugin-marketplace/catalog', '/api/coding/plugin-marketplace/library',
|
|
'免费获取', '我的插件',
|
|
].join('\n'));
|
|
expect(() => verifyMarketplaceClientArtifact(
|
|
complete,
|
|
`${emptyTrust}\nprocess.env.PLUGIN_KEY`,
|
|
)).toThrow('empty code-owned fail-closed store');
|
|
});
|
|
|
|
it('proves Marketplace trust from the packaged app.asar rather than checkout source', async () => {
|
|
const root = await mkdtemp(path.join(tmpdir(), 'makelore-marketplace-trust-asar-'));
|
|
roots.push(root);
|
|
const source = path.join(root, 'source');
|
|
await mkdir(path.join(source, 'dist-electron'), { recursive: true });
|
|
await writeFile(path.join(source, 'package.json'), JSON.stringify({ main: 'dist-electron/main.js' }));
|
|
await writeFile(path.join(source, 'dist-electron', 'main.js'), [
|
|
'const CODE_OWNED_PLUGIN_SIGNING_KEYS = Object.freeze({});',
|
|
'const unrelatedConfiguration = process.env.NIANCODE_E2E;',
|
|
'export const sourceMarker = "makelore.plugin-trust.code-owned.v1";',
|
|
'export const marketplace = true;',
|
|
].join('\n'));
|
|
const appAsar = path.join(root, 'app.asar');
|
|
await createAsarFixture(source, appAsar);
|
|
await expect(readPackagedMarketplaceTrustSource(appAsar)).resolves.toBe(
|
|
'CODE_OWNED_PLUGIN_SIGNING_KEYS = Object.freeze({})',
|
|
);
|
|
});
|
|
|
|
it('recognizes a minified trust table only when its packaged provenance marker is present', async () => {
|
|
const root = await mkdtemp(path.join(tmpdir(), 'makelore-marketplace-compiled-trust-asar-'));
|
|
roots.push(root);
|
|
const source = path.join(root, 'source');
|
|
await mkdir(path.join(source, 'dist-electron'), { recursive: true });
|
|
await writeFile(path.join(source, 'package.json'), JSON.stringify({ main: 'dist-electron/main.js' }));
|
|
await writeFile(path.join(source, 'dist-electron', 'main.js'), [
|
|
'const dC = Object.freeze({});',
|
|
'function createTrust() { return Object.freeze({ get: load, sourceMarker: "makelore.plugin-trust.code-owned.v1" }); }',
|
|
].join('\n'));
|
|
const appAsar = path.join(root, 'app.asar');
|
|
await createAsarFixture(source, appAsar);
|
|
await expect(readPackagedMarketplaceTrustSource(appAsar)).resolves.toContain(
|
|
'makelore.plugin-trust.code-owned.v1',
|
|
);
|
|
expect(verifyMarketplaceClientArtifact(
|
|
Buffer.from([
|
|
'makelore-plugin-release.v1', 'skill_only', 'plugin_signature_invalid',
|
|
'signing key is not trusted', '/api/coding/plugin-marketplace',
|
|
'plugin-marketplace\\/install\\/', 'plugin-marketplace\\/update\\/',
|
|
'effectiveSkillIds', 'pluginReleaseIds',
|
|
'/api/coding/plugin-marketplace/catalog', '/api/coding/plugin-marketplace/library',
|
|
'免费获取', '我的插件',
|
|
].join('\n')),
|
|
'const dC = Object.freeze({}); sourceMarker: makelore.plugin-trust.code-owned.v1',
|
|
)).toMatchObject({ productionTrust: 'official-key-absent-fail-closed' });
|
|
});
|
|
|
|
it('does not accept a stale unreachable trust marker outside package.json.main reachability', async () => {
|
|
const root = await mkdtemp(path.join(tmpdir(), 'makelore-marketplace-stale-trust-asar-'));
|
|
roots.push(root);
|
|
const source = path.join(root, 'source');
|
|
await mkdir(path.join(source, 'dist-electron'), { recursive: true });
|
|
await writeFile(path.join(source, 'package.json'), JSON.stringify({ main: 'dist-electron/main.js' }));
|
|
await writeFile(path.join(source, 'dist-electron', 'main.js'), 'export const app = true;');
|
|
await writeFile(path.join(source, 'dist-electron', 'stale-trusted-keys.js'), [
|
|
'const CODE_OWNED_PLUGIN_SIGNING_KEYS = Object.freeze({});',
|
|
'export const sourceMarker = "makelore.plugin-trust.code-owned.v1";',
|
|
].join('\n'));
|
|
const appAsar = path.join(root, 'app.asar');
|
|
await createAsarFixture(source, appAsar);
|
|
await expect(readPackagedMarketplaceTrustSource(appAsar)).rejects.toThrow('trust source');
|
|
});
|
|
|
|
it('binds Marketplace route, Renderer, and effective markers to the package main graph', async () => {
|
|
const root = await mkdtemp(path.join(tmpdir(), 'makelore-marketplace-contract-asar-'));
|
|
roots.push(root);
|
|
const source = path.join(root, 'source');
|
|
await mkdir(path.join(source, 'dist-electron'), { recursive: true });
|
|
await mkdir(path.join(source, 'dist', 'assets'), { recursive: true });
|
|
await writeFile(path.join(source, 'package.json'), JSON.stringify({ main: 'dist-electron/main.js' }));
|
|
await writeFile(path.join(source, 'dist-electron', 'main.js'), [
|
|
'const CODE_OWNED_PLUGIN_SIGNING_KEYS = Object.freeze({});',
|
|
'export const sourceMarker = "makelore.plugin-trust.code-owned.v1";',
|
|
'loadFile("../dist/index.html");',
|
|
].join('\n'));
|
|
await writeFile(path.join(source, 'dist', 'index.html'), [
|
|
'<script type="module" src="./assets/index.js"></script>',
|
|
].join('\n'));
|
|
await writeFile(path.join(source, 'dist', 'assets', 'index.js'), 'import("./project-plugins.js");');
|
|
await writeFile(
|
|
path.join(source, 'dist', 'assets', 'project-plugins.js'),
|
|
'import{marketplace}from"./plugin-marketplace.js";export{marketplace};',
|
|
);
|
|
await writeFile(path.join(source, 'dist', 'assets', 'plugin-marketplace.js'), [
|
|
'makelore-plugin-release.v1 skill_only plugin_signature_invalid signing key is not trusted',
|
|
'/api/coding/plugin-marketplace plugin-marketplace\\/install\\/ plugin-marketplace\\/update\\/',
|
|
'effectiveSkillIds pluginReleaseIds',
|
|
'/api/coding/plugin-marketplace/catalog /api/coding/plugin-marketplace/library 免费获取 我的插件',
|
|
].join('\n'));
|
|
const appAsar = path.join(root, 'app.asar');
|
|
await createAsarFixture(source, appAsar);
|
|
await expect(verifyPackagedMarketplaceClientArtifact(appAsar)).resolves.toMatchObject({ result: 'pass' });
|
|
|
|
const staleSource = path.join(root, 'stale-source');
|
|
await mkdir(path.join(staleSource, 'dist-electron'), { recursive: true });
|
|
await writeFile(path.join(staleSource, 'package.json'), JSON.stringify({ main: 'dist-electron/main.js' }));
|
|
await writeFile(path.join(staleSource, 'dist-electron', 'main.js'), [
|
|
'const CODE_OWNED_PLUGIN_SIGNING_KEYS = Object.freeze({});',
|
|
'export const sourceMarker = "makelore.plugin-trust.code-owned.v1";',
|
|
].join('\n'));
|
|
await writeFile(path.join(staleSource, 'dist-electron', 'stale-marketplace.js'), [
|
|
'makelore-plugin-release.v1 skill_only plugin_signature_invalid signing key is not trusted',
|
|
'/api/coding/plugin-marketplace plugin-marketplace\\/install\\/ plugin-marketplace\\/update\\/',
|
|
'effectiveSkillIds pluginReleaseIds',
|
|
'/api/coding/plugin-marketplace/catalog /api/coding/plugin-marketplace/library 免费获取 我的插件',
|
|
].join('\n'));
|
|
const staleAsar = path.join(root, 'stale.asar');
|
|
await createAsarFixture(staleSource, staleAsar);
|
|
await expect(verifyPackagedMarketplaceClientArtifact(staleAsar))
|
|
.rejects.toThrow('Marketplace contract markers');
|
|
});
|
|
|
|
it('rejects a packaged plugin tree that drops an SDK asset or catalog marker', async () => {
|
|
const fixture = await bundledResourceFixture();
|
|
await rm(path.join(
|
|
fixture.resourcesDirectory,
|
|
'resources',
|
|
'coding-plugins',
|
|
'example',
|
|
'skills',
|
|
'example',
|
|
'assets',
|
|
'sdk.js',
|
|
));
|
|
await expect(verifyBundledCodingPluginResources(fixture)).rejects.toThrow('files differ');
|
|
|
|
const complete = await bundledResourceFixture();
|
|
await expect(verifyBundledCodingPluginResources({
|
|
...complete,
|
|
appAsarContents: Buffer.from('example.plugin example-adapter'),
|
|
})).rejects.toThrow('adapter/tool catalog');
|
|
});
|
|
|
|
it('uses final unpacked-product paths and strictly parses verifier options', () => {
|
|
expect(defaultProductExecutable('/repo', 'linux'))
|
|
.toBe(path.resolve('/repo', 'release', 'linux-unpacked', 'niancode'));
|
|
expect(defaultProductExecutable('D:\\repo', 'win32'))
|
|
.toBe(path.resolve('D:\\repo', 'release', 'win-unpacked', 'Makelore.exe'));
|
|
expect(parsePiArtifactVerifierArgs([
|
|
'--app-exe', 'release/custom/Makelore.exe',
|
|
'--samples', '5',
|
|
'--timeout-ms', '12000',
|
|
'--report', 'release/evidence/pi.json',
|
|
], 'D:\\repo')).toMatchObject({ samples: 5, timeoutMs: 12_000 });
|
|
expect(parsePiArtifactVerifierArgs(['--', '--samples', '3'], 'D:\\repo'))
|
|
.toMatchObject({ samples: 3 });
|
|
expect(() => parsePiArtifactVerifierArgs(['--samples', '0'], 'D:\\repo'))
|
|
.toThrow('--samples must be a positive integer');
|
|
expect(() => parsePiArtifactVerifierArgs(['--unknown'], 'D:\\repo'))
|
|
.toThrow('Unknown argument');
|
|
});
|
|
});
|