feat(pi): verify bundled coding plugin artifact resources

This commit is contained in:
2026-08-27 18:56:24 +08:00
parent 8fea40238f
commit 7141a91a2c
2 changed files with 301 additions and 7 deletions

View File

@@ -35,6 +35,9 @@ const EXTENSION_CONTRACT_MARKERS = Object.freeze([
]);
const PI_AI_PROVIDER_PREFIX = 'pi-runtime/node_modules/@earendil-works/pi-ai/dist/providers/';
const PI_AI_PROVIDER_ASAR_PREFIX = 'app.asar/node_modules/@earendil-works/pi-ai/dist/providers/';
export const BUNDLED_CODING_PLUGIN_RESOURCE_ROOT = 'resources/coding-plugins';
export const CORE_CODING_SKILL_RESOURCE_ROOT = 'resources/coding-skills';
const SDK_ASSET_PATH_PATTERN = /^skills\/[^/]+\/assets\/.+\.(?:js|ts)$/u;
async function pathExists(path) {
try {
@@ -45,6 +48,67 @@ async function pathExists(path) {
}
}
async function listResourceFiles(root) {
const files = [];
const visit = async (directory, prefix = '') => {
if (!await pathExists(directory)) return;
for (const entry of await readdir(directory, { withFileTypes: true })) {
const path = join(directory, entry.name);
const relativePath = prefix ? `${prefix}/${entry.name}` : entry.name;
if (entry.isDirectory()) await visit(path, relativePath);
else if (entry.isFile()) files.push(relativePath);
}
};
await visit(root);
return files.sort();
}
function resolveBundledResource(root, baseDirectory, candidate, label) {
if (typeof candidate !== 'string' || candidate.trim().length === 0) {
throw new Error(`Bundled plugin ${label} is missing`);
}
const portableCandidate = candidate.replaceAll('\\', '/');
if (portableCandidate.startsWith('/') || /^[A-Za-z]:\//u.test(portableCandidate)
|| /^[A-Za-z][A-Za-z0-9+.-]*:/u.test(portableCandidate)) {
throw new Error(`Bundled plugin ${label} must be relative`);
}
const resolved = resolve(baseDirectory, candidate);
const relativePath = relative(root, resolved);
if (!relativePath || relativePath === '..' || relativePath.startsWith(`..${sep}`)
|| isAbsolute(relativePath)) {
throw new Error(`Bundled plugin ${label} escapes its package root`);
}
return resolved;
}
async function readJsonResource(path, label) {
try {
return JSON.parse(await readFile(path, 'utf8'));
} catch (error) {
throw new Error(`Bundled plugin ${label} is unreadable: ${error.message}`);
}
}
async function assertMirroredResourceTree(sourceRoot, packagedRoot, label) {
const [sourceFiles, packagedFiles] = await Promise.all([
listResourceFiles(sourceRoot),
listResourceFiles(packagedRoot),
]);
if (JSON.stringify(sourceFiles) !== JSON.stringify(packagedFiles)) {
throw new Error(
`Packaged ${label} files differ: expected ${sourceFiles.join(', ')}, got ${packagedFiles.join(', ')}`,
);
}
for (const file of sourceFiles) {
const [source, packaged] = await Promise.all([
readFile(join(sourceRoot, file)),
readFile(join(packagedRoot, file)),
]);
if (!source.equals(packaged)) throw new Error(`Packaged ${label} file differs: ${file}`);
}
return sourceFiles;
}
function portable(path) {
return path.split(sep).join('/');
}
@@ -264,6 +328,156 @@ async function sourceSkillIds(projectRoot) {
return entries.filter((entry) => entry.isDirectory()).map((entry) => entry.name).sort();
}
export async function verifyBundledCodingPluginResources({
projectRoot,
resourcesDirectory,
appAsarContents,
}) {
const root = resolve(projectRoot);
const sourcePluginRoot = join(root, BUNDLED_CODING_PLUGIN_RESOURCE_ROOT);
const packagedPluginRoot = join(resourcesDirectory, BUNDLED_CODING_PLUGIN_RESOURCE_ROOT);
if (!await pathExists(sourcePluginRoot)) {
throw new Error(`Bundled coding plugin resources are missing: ${sourcePluginRoot}`);
}
if (!await pathExists(packagedPluginRoot)) {
throw new Error(`Packaged coding plugin resources are missing: ${packagedPluginRoot}`);
}
const [sourcePackages, packagedPackages] = await Promise.all([
readdir(sourcePluginRoot, { withFileTypes: true }),
readdir(packagedPluginRoot, { withFileTypes: true }),
]);
const sourcePackageNames = sourcePackages
.filter((entry) => entry.isDirectory())
.map((entry) => entry.name)
.sort();
const packagedPackageNames = packagedPackages
.filter((entry) => entry.isDirectory())
.map((entry) => entry.name)
.sort();
if (JSON.stringify(sourcePackageNames) !== JSON.stringify(packagedPackageNames)) {
throw new Error(
`Packaged coding plugin packages differ: expected ${sourcePackageNames.join(', ')}, `
+ `got ${packagedPackageNames.join(', ')}`,
);
}
const packages = [];
const catalogMarkers = [];
for (const packageName of sourcePackageNames) {
const sourcePackageRoot = join(sourcePluginRoot, packageName);
const packagedPackageRoot = join(packagedPluginRoot, packageName);
const packageFiles = await assertMirroredResourceTree(
sourcePackageRoot,
packagedPackageRoot,
`coding plugin ${packageName}`,
);
const sourceManifestPath = join(sourcePackageRoot, 'plugin.json');
const packagedManifestPath = join(packagedPackageRoot, 'plugin.json');
const sourceManifest = await readJsonResource(sourceManifestPath, `${packageName} plugin.json`);
const packagedManifest = await readJsonResource(packagedManifestPath, `${packageName} plugin.json`);
const sourceCapabilityPath = resolveBundledResource(
sourcePackageRoot,
sourcePackageRoot,
sourceManifest.extensions?.['com.makelore']?.capabilityManifest,
`${packageName} capability manifest`,
);
const packagedCapabilityPath = resolveBundledResource(
packagedPackageRoot,
packagedPackageRoot,
packagedManifest.extensions?.['com.makelore']?.capabilityManifest,
`${packageName} capability manifest`,
);
const sourceCapability = await readJsonResource(sourceCapabilityPath, `${packageName} capability manifest`);
const packagedCapability = await readJsonResource(packagedCapabilityPath, `${packageName} capability manifest`);
if (packagedCapability.pluginId !== packagedManifest.name) {
throw new Error(`Packaged coding plugin ${packageName} identity does not match its manifest`);
}
const capabilityRelativePath = portable(relative(packagedPackageRoot, packagedCapabilityPath));
if (!packageFiles.includes(capabilityRelativePath)) {
throw new Error(`Packaged coding plugin capability manifest is not in its resource tree: ${packageName}`);
}
const skills = Array.isArray(packagedCapability.skills) ? packagedCapability.skills : [];
if (skills.length === 0) throw new Error(`Packaged coding plugin has no Skill: ${packageName}`);
const skillPaths = [];
for (const [index, skill] of skills.entries()) {
const skillPath = resolveBundledResource(
packagedPackageRoot,
dirname(packagedCapabilityPath),
skill?.entry,
`${packageName} Skill ${index}`,
);
const relativePath = portable(relative(packagedPackageRoot, skillPath));
if (!packageFiles.includes(relativePath)) {
throw new Error(`Packaged coding plugin Skill is missing: ${packageName}/${relativePath}`);
}
const content = await readFile(skillPath, 'utf8');
if (content.trim().length === 0) throw new Error(`Packaged coding plugin Skill is empty: ${packageName}/${relativePath}`);
skillPaths.push({ id: skill?.id, path: relativePath });
}
const sdkAssetPaths = packageFiles.filter((file) => SDK_ASSET_PATH_PATTERN.test(file));
if (sdkAssetPaths.length === 0) throw new Error(`Packaged coding plugin has no SDK assets: ${packageName}`);
const adapterId = packagedCapability.adapterId;
const tools = Array.isArray(packagedCapability.tools) ? packagedCapability.tools : [];
const toolNames = tools.map((tool) => tool?.name);
if (typeof adapterId !== 'string' || adapterId.length === 0 || tools.length === 0
|| toolNames.some((name) => typeof name !== 'string' || name.length === 0)
|| new Set(toolNames).size !== toolNames.length) {
throw new Error(`Packaged coding plugin adapter/tool catalog is invalid: ${packageName}`);
}
catalogMarkers.push(packagedManifest.name, adapterId, ...toolNames);
packages.push({
id: packagedManifest.name,
version: packagedManifest.version,
manifestPath: portable(relative(resourcesDirectory, packagedManifestPath)),
capabilityManifestPath: portable(relative(resourcesDirectory, packagedCapabilityPath)),
files: packageFiles.map((file) => portable(join(BUNDLED_CODING_PLUGIN_RESOURCE_ROOT, packageName, file))),
skills: skillPaths,
sdkAssets: sdkAssetPaths.map((file) => portable(join(BUNDLED_CODING_PLUGIN_RESOURCE_ROOT, packageName, file))),
adapterId,
tools: toolNames,
sourceManifest: sourceManifest.name,
sourceCapability: sourceCapability.pluginId,
});
}
const asar = appAsarContents ?? await readFile(join(resourcesDirectory, 'app.asar'));
const missingCatalogMarkers = [...new Set(catalogMarkers)].filter(
(marker) => !asar.includes(Buffer.from(marker)),
);
if (missingCatalogMarkers.length > 0) {
throw new Error(
`Packaged app.asar does not contain the coding plugin adapter/tool catalog: ${missingCatalogMarkers.join(', ')}`,
);
}
const sourceCoreRoot = join(root, CORE_CODING_SKILL_RESOURCE_ROOT);
const packagedCoreRoot = join(resourcesDirectory, CORE_CODING_SKILL_RESOURCE_ROOT);
const coreFiles = await assertMirroredResourceTree(sourceCoreRoot, packagedCoreRoot, 'core coding resources');
const expectedSkills = await sourceSkillIds(root);
const actualSkills = await packagedSkillIds(resourcesDirectory);
if (JSON.stringify(actualSkills) !== JSON.stringify(expectedSkills)) {
throw new Error(`Packaged coding skills differ: expected ${expectedSkills}, got ${actualSkills}`);
}
return {
root: BUNDLED_CODING_PLUGIN_RESOURCE_ROOT,
packages,
catalog: {
adapters: [...new Set(packages.map(({ adapterId }) => adapterId))],
tools: [...new Set(packages.flatMap(({ tools }) => tools))],
packagedInAsar: true,
},
coreResources: {
root: CORE_CODING_SKILL_RESOURCE_ROOT,
files: coreFiles,
skills: actualSkills,
},
result: 'pass',
};
}
export async function verifyPiProductArtifact({ projectRoot, executable }) {
const root = resolve(projectRoot);
const appExecutable = resolve(executable ?? defaultProductExecutable(root));
@@ -299,6 +513,7 @@ export async function verifyPiProductArtifact({ projectRoot, executable }) {
if (absoluteManifestValues.length > 0) {
throw new Error(`Pi runtime manifest contains absolute paths: ${JSON.stringify(absoluteManifestValues)}`);
}
const appAsarContents = await readFile(appAsar);
const physicalOpenCodePaths = await collectForbiddenResourcePaths(resourcesDirectory);
const asarOpenCodePaths = collectForbiddenAsarPaths(appAsar);
const openCodeResourcePaths = classifyOpenCodeResourcePaths([
@@ -311,12 +526,12 @@ export async function verifyPiProductArtifact({ projectRoot, executable }) {
);
}
const expectedSkills = await sourceSkillIds(root);
const actualSkills = await packagedSkillIds(resourcesDirectory);
if (JSON.stringify(actualSkills) !== JSON.stringify(expectedSkills)) {
throw new Error(`Packaged coding skills differ: expected ${expectedSkills}, got ${actualSkills}`);
}
const appAsarContents = await readFile(appAsar);
const bundledPluginResources = await verifyBundledCodingPluginResources({
projectRoot: root,
resourcesDirectory,
appAsarContents,
});
const actualSkills = bundledPluginResources.coreResources.skills;
const missingExtensionMarkers = EXTENSION_CONTRACT_MARKERS.filter(
(marker) => !appAsarContents.includes(Buffer.from(marker)),
);
@@ -366,6 +581,7 @@ export async function verifyPiProductArtifact({ projectRoot, executable }) {
nodeEngine: manifest.runtime.nodeEngine,
},
packagedClosure,
bundledPlugins: bundledPluginResources,
extension: {
contractMarkers: EXTENSION_CONTRACT_MARKERS,
packaged: true,