785 lines
31 KiB
JavaScript
785 lines
31 KiB
JavaScript
import { spawn } from 'node:child_process';
|
|
import { readFile, readdir, stat } from 'node:fs/promises';
|
|
import { createRequire } from 'node:module';
|
|
import { arch as hostArch, platform as hostPlatform } from 'node:os';
|
|
import {
|
|
dirname,
|
|
isAbsolute,
|
|
join,
|
|
relative,
|
|
resolve,
|
|
sep,
|
|
} from 'node:path';
|
|
import YAML from 'yaml';
|
|
|
|
import {
|
|
PI_RUNTIME_CLI_ENTRY,
|
|
PI_RUNTIME_MANIFEST,
|
|
PI_RUNTIME_PACKAGE,
|
|
PI_RUNTIME_VERSION,
|
|
} from './pi-runtime-bundle.mjs';
|
|
import {
|
|
inspectPackagedClosure,
|
|
packagedResourcesDirectory,
|
|
} from '../probe-pi-packaged-runtime.mjs';
|
|
|
|
const { extractFile, listPackage } = createRequire(import.meta.url)('@electron/asar');
|
|
|
|
const PRODUCT_NAME = 'Makelore';
|
|
const LINUX_EXECUTABLE_NAME = 'niancode';
|
|
const EXTENSION_CONTRACT_MARKERS = Object.freeze([
|
|
'Makelore runtime bridge rejected the request',
|
|
'subagent.dispatch',
|
|
'makelore.write-lease',
|
|
'MAKELORE_PI_BRIDGE_URL',
|
|
]);
|
|
const MARKETPLACE_ARTIFACT_MARKERS = Object.freeze({
|
|
packageTrust: Object.freeze([
|
|
'makelore-plugin-release.v1',
|
|
'skill_only',
|
|
'plugin_signature_invalid',
|
|
'signing key is not trusted',
|
|
]),
|
|
mainRoutes: Object.freeze([
|
|
'/api/coding/plugin-marketplace',
|
|
'plugin-marketplace\\/install\\/',
|
|
'plugin-marketplace\\/update\\/',
|
|
]),
|
|
effectiveSnapshot: Object.freeze([
|
|
'effectiveSkillIds',
|
|
'pluginReleaseIds',
|
|
]),
|
|
renderer: Object.freeze([
|
|
'/api/coding/plugin-marketplace/catalog',
|
|
'/api/coding/plugin-marketplace/library',
|
|
'免费获取',
|
|
'我的插件',
|
|
]),
|
|
});
|
|
const CODE_OWNED_PLUGIN_SIGNING_KEYS_SOURCE_MARKER = 'makelore.plugin-trust.code-owned.v1';
|
|
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 {
|
|
await stat(path);
|
|
return true;
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
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('/');
|
|
}
|
|
|
|
function normalizeLockedVersion(value) {
|
|
return typeof value === 'string' ? value.split('(', 1)[0] : null;
|
|
}
|
|
|
|
function runCommand(executable, args, options = {}) {
|
|
return new Promise((resolvePromise, reject) => {
|
|
const child = spawn(executable, args, {
|
|
cwd: options.cwd,
|
|
env: options.env ?? process.env,
|
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
windowsHide: true,
|
|
});
|
|
let stdout = '';
|
|
let stderr = '';
|
|
child.stdout.on('data', (chunk) => { stdout += chunk.toString(); });
|
|
child.stderr.on('data', (chunk) => { stderr += chunk.toString(); });
|
|
child.once('error', reject);
|
|
child.once('exit', (code, signal) => {
|
|
if (code === 0) resolvePromise({ stdout, stderr });
|
|
else reject(new Error(
|
|
`${executable} exited with code ${code ?? 'null'} signal ${signal ?? 'none'}: ${stderr || stdout}`,
|
|
));
|
|
});
|
|
});
|
|
}
|
|
|
|
export function defaultProductExecutable(projectRoot, currentPlatform = hostPlatform()) {
|
|
const root = resolve(projectRoot);
|
|
if (currentPlatform === 'win32') {
|
|
return join(root, 'release', 'win-unpacked', `${PRODUCT_NAME}.exe`);
|
|
}
|
|
if (currentPlatform === 'darwin') {
|
|
return join(root, 'release', 'mac', `${PRODUCT_NAME}.app`, 'Contents', 'MacOS', PRODUCT_NAME);
|
|
}
|
|
if (currentPlatform === 'linux') {
|
|
return join(root, 'release', 'linux-unpacked', LINUX_EXECUTABLE_NAME);
|
|
}
|
|
throw new Error(`Unsupported product artifact platform: ${currentPlatform}`);
|
|
}
|
|
|
|
export function assertNodeEngineCompatible(engine, nodeVersion) {
|
|
const match = /^>=(\d+)\.(\d+)\.(\d+)$/.exec(engine ?? '');
|
|
if (!match) throw new Error(`Unsupported Pi Node engine expression: ${engine ?? 'missing'}`);
|
|
const required = match.slice(1).map(Number);
|
|
const actualMatch = /^(\d+)\.(\d+)\.(\d+)/.exec(nodeVersion ?? '');
|
|
if (!actualMatch) throw new Error(`Invalid packaged Node version: ${nodeVersion ?? 'missing'}`);
|
|
const actual = actualMatch.slice(1).map(Number);
|
|
for (let index = 0; index < required.length; index += 1) {
|
|
if (actual[index] > required[index]) return;
|
|
if (actual[index] < required[index]) {
|
|
throw new Error(`Packaged Node ${nodeVersion} does not satisfy Pi engine ${engine}`);
|
|
}
|
|
}
|
|
}
|
|
|
|
export function validatePiArtifactMetadata({
|
|
rootPackage,
|
|
lockfile,
|
|
packagedPackage,
|
|
runtimePackage,
|
|
manifest,
|
|
runtimePlatform,
|
|
}) {
|
|
const rootDependency = rootPackage.dependencies?.[PI_RUNTIME_PACKAGE];
|
|
const packagedDependency = packagedPackage.dependencies?.[PI_RUNTIME_PACKAGE];
|
|
const lockDependency = lockfile.importers?.['.']?.dependencies?.[PI_RUNTIME_PACKAGE];
|
|
const lockedVersion = normalizeLockedVersion(lockDependency?.version);
|
|
const versions = {
|
|
expected: PI_RUNTIME_VERSION,
|
|
rootDependency,
|
|
packagedDependency,
|
|
lockSpecifier: lockDependency?.specifier ?? null,
|
|
lockedVersion,
|
|
runtimePackage: runtimePackage.version,
|
|
manifest: manifest.runtime?.version ?? null,
|
|
};
|
|
const mismatched = Object.entries(versions)
|
|
.filter(([key, value]) => key !== 'expected' && value !== PI_RUNTIME_VERSION);
|
|
if (mismatched.length > 0) {
|
|
throw new Error(`Pi artifact versions do not match: ${JSON.stringify(versions)}`);
|
|
}
|
|
if (runtimePackage.name !== PI_RUNTIME_PACKAGE) {
|
|
throw new Error(`Packaged Pi package name is ${runtimePackage.name ?? 'missing'}`);
|
|
}
|
|
if (runtimePackage.bin?.pi !== PI_RUNTIME_CLI_ENTRY) {
|
|
throw new Error(`Packaged Pi CLI entry is ${runtimePackage.bin?.pi ?? 'missing'}`);
|
|
}
|
|
if (manifest.runtime?.packageName !== PI_RUNTIME_PACKAGE
|
|
|| manifest.runtime?.cliEntry !== PI_RUNTIME_CLI_ENTRY) {
|
|
throw new Error(`Packaged Pi manifest identity is invalid: ${JSON.stringify(manifest.runtime)}`);
|
|
}
|
|
if (manifest.target?.platform !== runtimePlatform.platform
|
|
|| manifest.target?.arch !== runtimePlatform.arch) {
|
|
throw new Error(
|
|
`Packaged Pi target ${manifest.target?.platform ?? 'missing'}-${manifest.target?.arch ?? 'missing'} `
|
|
+ `does not match executable ${runtimePlatform.platform}-${runtimePlatform.arch}`,
|
|
);
|
|
}
|
|
assertNodeEngineCompatible(manifest.runtime?.nodeEngine, runtimePlatform.node);
|
|
return versions;
|
|
}
|
|
|
|
export function collectAbsoluteManifestValues(value, at = '$', results = []) {
|
|
if (typeof value === 'string') {
|
|
if (isAbsolute(value) || /^[A-Za-z]:[\\/]/.test(value)) results.push({ at, value });
|
|
return results;
|
|
}
|
|
if (Array.isArray(value)) {
|
|
value.forEach((entry, index) => collectAbsoluteManifestValues(entry, `${at}[${index}]`, results));
|
|
return results;
|
|
}
|
|
if (value && typeof value === 'object') {
|
|
for (const [key, entry] of Object.entries(value)) {
|
|
collectAbsoluteManifestValues(entry, `${at}.${key}`, results);
|
|
}
|
|
}
|
|
return results;
|
|
}
|
|
|
|
export async function collectForbiddenResourcePaths(root, pattern = /opencode/i) {
|
|
const matches = [];
|
|
const visit = async (directory) => {
|
|
if (!await pathExists(directory)) return;
|
|
for (const entry of await readdir(directory, { withFileTypes: true })) {
|
|
const path = join(directory, entry.name);
|
|
if (pattern.test(entry.name)) matches.push(portable(relative(root, path)));
|
|
if (entry.isDirectory()) await visit(path);
|
|
}
|
|
};
|
|
await visit(root);
|
|
return matches.sort();
|
|
}
|
|
|
|
export function classifyOpenCodeResourcePaths(paths) {
|
|
const isUpstreamPiProvider = (path) => path.startsWith(PI_AI_PROVIDER_PREFIX)
|
|
|| path.startsWith(PI_AI_PROVIDER_ASAR_PREFIX);
|
|
const upstreamPiProvider = paths.filter(isUpstreamPiProvider);
|
|
const productOwned = paths.filter((path) => !isUpstreamPiProvider(path));
|
|
return { productOwned, upstreamPiProvider };
|
|
}
|
|
|
|
export function collectForbiddenAsarPaths(appAsar, pattern = /opencode/i) {
|
|
const paths = listPackage(appAsar, { isPack: false }).map((entry) => (
|
|
`app.asar/${entry.replace(/^[\\/]+/, '').replaceAll('\\', '/')}`
|
|
));
|
|
return {
|
|
entryCount: paths.length,
|
|
matches: paths.filter((entry) => pattern.test(entry)).sort(),
|
|
};
|
|
}
|
|
|
|
export function verifyMarketplaceClientArtifact(appAsarContents, productionTrustSource) {
|
|
const missing = Object.entries(MARKETPLACE_ARTIFACT_MARKERS).flatMap(([group, markers]) => (
|
|
markers
|
|
.filter((marker) => !appAsarContents.includes(Buffer.from(marker)))
|
|
.map((marker) => `${group}:${marker}`)
|
|
));
|
|
if (missing.length > 0) {
|
|
throw new Error(`Packaged app.asar does not contain Marketplace contract markers: ${missing.join(', ')}`);
|
|
}
|
|
const hasEmptyCodeOwnedTrust = /(?:CODE_OWNED_PLUGIN_SIGNING_KEYS\s*=\s*)?Object\.freeze\(\s*\{\}\s*(?:as\s+[^)]*)?\)/u.test(productionTrustSource);
|
|
if (!hasEmptyCodeOwnedTrust
|
|
|| !productionTrustSource.includes(CODE_OWNED_PLUGIN_SIGNING_KEYS_SOURCE_MARKER)
|
|
|| productionTrustSource.includes('process.env')
|
|
|| productionTrustSource.includes('-----BEGIN PRIVATE KEY-----')
|
|
|| productionTrustSource.includes('-----BEGIN ED25519 PRIVATE KEY-----')) {
|
|
throw new Error('Marketplace production trust source is not an empty code-owned fail-closed store');
|
|
}
|
|
return {
|
|
schema2SkillOnly: true,
|
|
productionTrust: 'official-key-absent-fail-closed',
|
|
libraryInstallAndEffectiveRoutes: true,
|
|
rendererAssets: true,
|
|
productionKeyIds: [],
|
|
privateKeyMaterialInTrustSource: false,
|
|
markers: MARKETPLACE_ARTIFACT_MARKERS,
|
|
result: 'pass',
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Read the package.main-reachable application graph from the packaged app.
|
|
* Checkout sources and unrelated app.asar files are not product evidence.
|
|
*/
|
|
async function readPackagedApplicationGraph(appAsar) {
|
|
const entries = new Set(listPackage(appAsar, { isPack: false })
|
|
.map((entry) => entry.replace(/^[/\\]+/u, '').replaceAll('\\', '/')));
|
|
const readArchiveText = (entry) => {
|
|
if (!entries.has(entry)) throw new Error(`Packaged app.asar entry is missing: ${entry}`);
|
|
return Buffer.from(extractFile(appAsar, entry.replaceAll('/', sep))).toString('utf8');
|
|
};
|
|
let packageJson;
|
|
try {
|
|
packageJson = JSON.parse(readArchiveText('package.json'));
|
|
} catch (error) {
|
|
const reason = error instanceof Error ? error.message : 'unknown error';
|
|
throw new Error(`Packaged app.asar package.json is unreadable: ${reason}`);
|
|
}
|
|
if (typeof packageJson.main !== 'string' || packageJson.main.trim().length === 0) {
|
|
throw new Error('Packaged app.asar package.json.main is missing');
|
|
}
|
|
|
|
const normalizeAsarPath = (value) => {
|
|
const result = [];
|
|
for (const segment of value.replaceAll('\\', '/').split('/')) {
|
|
if (!segment || segment === '.') continue;
|
|
if (segment === '..') {
|
|
if (result.length === 0) return null;
|
|
result.pop();
|
|
} else result.push(segment);
|
|
}
|
|
return result.join('/');
|
|
};
|
|
const resolveEntry = (from, specifier) => {
|
|
if (!specifier.startsWith('.')) return null;
|
|
const withoutSuffix = specifier.split(/[?#]/u, 1)[0];
|
|
const base = normalizeAsarPath(`${dirname(from).replaceAll('\\', '/')}/${withoutSuffix}`);
|
|
if (!base) return null;
|
|
const candidates = [base, `${base}.js`, `${base}.mjs`, `${base}.cjs`, `${base}/index.js`];
|
|
return candidates.find((candidate) => entries.has(candidate)) ?? null;
|
|
};
|
|
const main = normalizeAsarPath(packageJson.main);
|
|
if (!main || !entries.has(main)) throw new Error('Packaged app.asar package.json.main is not present');
|
|
const pending = [main];
|
|
const reachable = [];
|
|
const seen = new Set();
|
|
while (pending.length > 0) {
|
|
const entry = pending.shift();
|
|
if (!entry || seen.has(entry)) continue;
|
|
seen.add(entry);
|
|
let source;
|
|
try {
|
|
source = readArchiveText(entry);
|
|
} catch {
|
|
continue;
|
|
}
|
|
reachable.push({ entry, source });
|
|
const specifiers = [];
|
|
const importPattern = /\b(?:import|export)(?:[^"'`;]*?\bfrom)?\s*['"]([^'"]+)['"]/gu;
|
|
const dynamicImportPattern = /\bimport\(\s*['"]([^'"]+)['"]\s*\)/gu;
|
|
const requirePattern = /\brequire\(\s*['"]([^'"]+)['"]\s*\)/gu;
|
|
const htmlEntryPattern = /['"]([^'"]+\.html(?:[?#][^'"]*)?)['"]/gu;
|
|
const htmlAssetPattern = /\b(?:src|href)\s*=\s*['"]([^'"]+)['"]/gu;
|
|
for (const match of source.matchAll(importPattern)) specifiers.push(match[1]);
|
|
for (const match of source.matchAll(dynamicImportPattern)) specifiers.push(match[1]);
|
|
for (const match of source.matchAll(requirePattern)) specifiers.push(match[1]);
|
|
for (const match of source.matchAll(htmlEntryPattern)) specifiers.push(match[1]);
|
|
if (entry.endsWith('.html')) {
|
|
for (const match of source.matchAll(htmlAssetPattern)) specifiers.push(match[1]);
|
|
}
|
|
for (const specifier of specifiers) {
|
|
const resolved = resolveEntry(entry, specifier);
|
|
if (resolved) pending.push(resolved);
|
|
}
|
|
}
|
|
|
|
return { packageJson, reachable };
|
|
}
|
|
|
|
function findPackagedMarketplaceTrustSource(reachable) {
|
|
for (const { source } of reachable) {
|
|
const trustAssignment = source.match(
|
|
/CODE_OWNED_PLUGIN_SIGNING_KEYS\s*=\s*Object\.freeze\(\s*\{\}\s*(?:as\s+[^)]*)?\)/u,
|
|
);
|
|
if (trustAssignment) return trustAssignment[0];
|
|
|
|
const compiledTrust = source.match(
|
|
/(?:const|let|var)\s+[A-Za-z_$][\w$]*\s*=\s*Object\.freeze\(\s*\{\}\s*\)\s*;(?=[\s\S]{0,512}?sourceMarker\s*:\s*["']makelore\.plugin-trust\.code-owned\.v1["'])/u,
|
|
);
|
|
if (compiledTrust) {
|
|
return `${compiledTrust[0]} sourceMarker: ${CODE_OWNED_PLUGIN_SIGNING_KEYS_SOURCE_MARKER}`;
|
|
}
|
|
}
|
|
throw new Error('Packaged app.asar does not contain the Marketplace trust source');
|
|
}
|
|
|
|
/**
|
|
* Read the Marketplace trust source from the packaged application itself.
|
|
* Checkout sources are not evidence of what an installed app will trust.
|
|
*/
|
|
export async function readPackagedMarketplaceTrustSource(appAsar) {
|
|
const { reachable } = await readPackagedApplicationGraph(appAsar);
|
|
return findPackagedMarketplaceTrustSource(reachable);
|
|
}
|
|
|
|
/**
|
|
* Verify Marketplace contract markers only in the package.main-reachable app
|
|
* graph. An arbitrary string elsewhere in app.asar is not evidence that the
|
|
* installed Main/Renderer contract is present.
|
|
*/
|
|
export async function verifyPackagedMarketplaceClientArtifact(appAsar) {
|
|
const { reachable } = await readPackagedApplicationGraph(appAsar);
|
|
const reachableContents = Buffer.from(reachable.map(({ source }) => source).join('\n'));
|
|
const trustSource = findPackagedMarketplaceTrustSource(reachable);
|
|
if (!reachableContents.includes(Buffer.from(CODE_OWNED_PLUGIN_SIGNING_KEYS_SOURCE_MARKER))) {
|
|
throw new Error('Packaged app.asar reachable graph does not contain the Marketplace trust provenance marker');
|
|
}
|
|
const verifiedTrustSource = `${trustSource}\n${CODE_OWNED_PLUGIN_SIGNING_KEYS_SOURCE_MARKER}`;
|
|
return verifyMarketplaceClientArtifact(reachableContents, verifiedTrustSource);
|
|
}
|
|
|
|
async function filesContainingNeedles(root, needles) {
|
|
const matches = [];
|
|
const visit = async (path) => {
|
|
const details = await stat(path);
|
|
if (details.isDirectory()) {
|
|
for (const entry of await readdir(path, { withFileTypes: true })) {
|
|
await visit(join(path, entry.name));
|
|
}
|
|
return;
|
|
}
|
|
const contents = await readFile(path);
|
|
const found = needles.filter((needle) => needle.length > 0 && contents.includes(needle));
|
|
if (found.length > 0) matches.push({ path, needles: found.map((value) => value.toString()) });
|
|
};
|
|
await visit(root);
|
|
return matches;
|
|
}
|
|
|
|
async function inspectProductRuntime(executable, resourcesDirectory) {
|
|
const script = String.raw`
|
|
const { createRequire } = require('node:module');
|
|
const path = require('node:path');
|
|
const resources = process.env.MAKELORE_PI_PRODUCT_RESOURCES;
|
|
const appRequire = createRequire(path.join(resources, 'app.asar', 'package.json'));
|
|
const packagedPackage = appRequire('./package.json');
|
|
process.stdout.write(JSON.stringify({
|
|
platform: process.platform,
|
|
arch: process.arch,
|
|
node: process.versions.node,
|
|
electron: process.versions.electron,
|
|
packagedPackage,
|
|
}));
|
|
`;
|
|
const { stdout } = await runCommand(executable, ['-e', script], {
|
|
env: {
|
|
...process.env,
|
|
ELECTRON_RUN_AS_NODE: '1',
|
|
MAKELORE_PI_PRODUCT_RESOURCES: resourcesDirectory,
|
|
},
|
|
});
|
|
return JSON.parse(stdout.trim());
|
|
}
|
|
|
|
async function packagedSkillIds(resourcesDirectory) {
|
|
const root = join(resourcesDirectory, 'resources', 'coding-skills');
|
|
if (!await pathExists(root)) throw new Error(`Packaged coding skills are missing: ${root}`);
|
|
const entries = await readdir(root, { withFileTypes: true });
|
|
const ids = [];
|
|
for (const entry of entries) {
|
|
if (!entry.isDirectory()) continue;
|
|
if (!await pathExists(join(root, entry.name, 'SKILL.md'))) {
|
|
throw new Error(`Packaged coding skill has no SKILL.md: ${entry.name}`);
|
|
}
|
|
ids.push(entry.name);
|
|
}
|
|
return ids.sort();
|
|
}
|
|
|
|
async function sourceSkillIds(projectRoot) {
|
|
const root = join(projectRoot, 'resources', 'coding-skills');
|
|
const entries = await readdir(root, { withFileTypes: true });
|
|
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));
|
|
if (!await pathExists(appExecutable)) throw new Error(`Product executable is missing: ${appExecutable}`);
|
|
const resourcesDirectory = packagedResourcesDirectory(appExecutable);
|
|
const runtimeRoot = join(resourcesDirectory, 'pi-runtime');
|
|
const appAsar = join(resourcesDirectory, 'app.asar');
|
|
for (const required of [runtimeRoot, appAsar]) {
|
|
if (!await pathExists(required)) throw new Error(`Product artifact resource is missing: ${required}`);
|
|
}
|
|
|
|
const [rootPackageSource, lockfileSource, runtimePackageSource, manifestSource] = await Promise.all([
|
|
readFile(join(root, 'package.json'), 'utf8'),
|
|
readFile(join(root, 'pnpm-lock.yaml'), 'utf8'),
|
|
readFile(join(runtimeRoot, 'package.json'), 'utf8'),
|
|
readFile(join(runtimeRoot, PI_RUNTIME_MANIFEST), 'utf8'),
|
|
]);
|
|
const rootPackage = JSON.parse(rootPackageSource);
|
|
const lockfile = YAML.parse(lockfileSource);
|
|
const runtimePackage = JSON.parse(runtimePackageSource);
|
|
const manifest = JSON.parse(manifestSource);
|
|
const runtimePlatform = await inspectProductRuntime(appExecutable, resourcesDirectory);
|
|
const versions = validatePiArtifactMetadata({
|
|
rootPackage,
|
|
lockfile,
|
|
packagedPackage: runtimePlatform.packagedPackage,
|
|
runtimePackage,
|
|
manifest,
|
|
runtimePlatform,
|
|
});
|
|
|
|
const absoluteManifestValues = collectAbsoluteManifestValues(manifest);
|
|
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([
|
|
...physicalOpenCodePaths,
|
|
...asarOpenCodePaths.matches,
|
|
]);
|
|
if (openCodeResourcePaths.productOwned.length > 0) {
|
|
throw new Error(
|
|
`Product-owned resources contain OpenCode paths: ${openCodeResourcePaths.productOwned.join(', ')}`,
|
|
);
|
|
}
|
|
|
|
const bundledPluginResources = await verifyBundledCodingPluginResources({
|
|
projectRoot: root,
|
|
resourcesDirectory,
|
|
appAsarContents,
|
|
});
|
|
const marketplace = await verifyPackagedMarketplaceClientArtifact(appAsar);
|
|
const actualSkills = bundledPluginResources.coreResources.skills;
|
|
const missingExtensionMarkers = EXTENSION_CONTRACT_MARKERS.filter(
|
|
(marker) => !appAsarContents.includes(Buffer.from(marker)),
|
|
);
|
|
if (missingExtensionMarkers.length > 0) {
|
|
throw new Error(
|
|
`Packaged app.asar does not contain Pi extension contract markers: ${missingExtensionMarkers.join(', ')}`,
|
|
);
|
|
}
|
|
|
|
const sourceNeedles = [
|
|
Buffer.from(root),
|
|
Buffer.from(portable(root)),
|
|
];
|
|
const devPathResidue = [
|
|
...await filesContainingNeedles(appAsar, sourceNeedles),
|
|
...await filesContainingNeedles(runtimeRoot, sourceNeedles),
|
|
];
|
|
if (devPathResidue.length > 0) {
|
|
throw new Error(`Product artifact contains development path residue: ${JSON.stringify(devPathResidue)}`);
|
|
}
|
|
|
|
const packagedClosure = await inspectPackagedClosure(
|
|
appExecutable,
|
|
resourcesDirectory,
|
|
manifest.runtimeAssets,
|
|
);
|
|
return {
|
|
schemaVersion: 1,
|
|
artifact: {
|
|
executable: appExecutable,
|
|
resourcesDirectory,
|
|
appAsar,
|
|
runtimeRoot,
|
|
cliPath: join(runtimeRoot, ...PI_RUNTIME_CLI_ENTRY.split('/')),
|
|
},
|
|
platform: {
|
|
platform: runtimePlatform.platform,
|
|
arch: runtimePlatform.arch,
|
|
electron: runtimePlatform.electron,
|
|
node: runtimePlatform.node,
|
|
},
|
|
versions,
|
|
manifest: {
|
|
target: manifest.target,
|
|
packageCount: manifest.productionPackages.length,
|
|
assetCount: manifest.runtimeAssets.length,
|
|
nodeEngine: manifest.runtime.nodeEngine,
|
|
},
|
|
packagedClosure,
|
|
bundledPlugins: bundledPluginResources,
|
|
marketplace,
|
|
extension: {
|
|
contractMarkers: EXTENSION_CONTRACT_MARKERS,
|
|
packaged: true,
|
|
executionProof: 'smoke:pi:real final-product extension/subagent run',
|
|
},
|
|
skills: actualSkills,
|
|
openCodeResourcePaths: {
|
|
...openCodeResourcePaths,
|
|
physicalMatches: physicalOpenCodePaths,
|
|
asar: asarOpenCodePaths,
|
|
upstreamDecision: openCodeResourcePaths.upstreamPiProvider.length > 0
|
|
? 'retained-required-files-from-exact-pinned-pi-production-package'
|
|
: 'none',
|
|
},
|
|
developmentPathResidue: [],
|
|
result: 'pass',
|
|
};
|
|
}
|
|
|
|
export const PI_PRODUCT_ARTIFACT_DEFAULTS = Object.freeze({
|
|
platform: hostPlatform(),
|
|
arch: hostArch(),
|
|
extensionContractMarkers: EXTENSION_CONTRACT_MARKERS,
|
|
});
|