Files
PaperClipAI/scripts/ci/pi-intel/verify-pack.mjs
2026-10-02 03:28:42 -05:00

78 lines
5.5 KiB
JavaScript

import { createHash } from 'node:crypto';
import { createRequire } from 'node:module';
import { readFileSync, readdirSync, readlinkSync, lstatSync, realpathSync } from 'node:fs';
import { resolve, relative, isAbsolute, join } from 'node:path';
import { pathToFileURL } from 'node:url';
export function requireTrue(value, message) { if (!value) throw new Error(message); }
export function canonicalJson(value) {
if (Array.isArray(value)) return `[${value.map(canonicalJson).join(',')}]`;
if (value !== null && typeof value === 'object') return `{${Object.keys(value).sort().map(key => `${JSON.stringify(key)}:${canonicalJson(value[key])}`).join(',')}}`;
return JSON.stringify(value);
}
export function sha256File(path) { return `sha256:${createHash('sha256').update(readFileSync(path)).digest('hex')}`; }
// Exactly the frozen builder's tree framing and localeCompare ordering.
export function sha256Tree(root) {
const hash = createHash('sha256');
const visit = (directory, prefix = '') => {
for (const entry of readdirSync(directory, { withFileTypes: true }).sort((a,b) => a.name.localeCompare(b.name))) {
const p = prefix ? `${prefix}/${entry.name}` : entry.name;
const absolute = join(directory, entry.name);
if (entry.isDirectory()) { hash.update(`directory\0${p}\n`); visit(absolute, p); }
else if (entry.isFile()) hash.update(`file\0${p}\0${sha256File(absolute)}\n`);
else if (entry.isSymbolicLink()) hash.update(`symlink\0${p}\0${readlinkSync(absolute)}\n`);
else throw new Error('Unsupported entry');
}
};
visit(root); return `sha256:${hash.digest('hex')}`;
}
function contained(root, candidate) {
const rel = relative(realpathSync(root), realpathSync(candidate));
requireTrue(rel !== '..' && !rel.startsWith('../') && !isAbsolute(rel), 'Artifact escaped pack');
}
export function checkedArtifact(pack, entry, kind) {
requireTrue(typeof entry?.path === 'string' && /^sha256:[a-f0-9]{64}$/.test(entry?.sha256), 'Malformed artifact');
requireTrue(!isAbsolute(entry.path) && !entry.path.split('/').includes('..'), 'Unsafe artifact path');
const path = resolve(pack, entry.path); contained(pack, path);
const st = lstatSync(path);
requireTrue(kind === 'directory' ? st.isDirectory() && !st.isSymbolicLink() : st.isFile() || st.isSymbolicLink(), 'Artifact type mismatch');
const actual = kind === 'directory' ? sha256Tree(path) : sha256File(path);
requireTrue(actual === entry.sha256, 'Artifact bytes mismatch: ' + entry.path);
return { path: entry.path, kind, sha256: actual };
}
export function verifyNormalPiSelection(payload, platform, architecture) {
const pins = { pi: '47306e6d2a9b59e8f9189f725ebb7a0a7f91826044d1739e1a35ab31f228ba1f' };
const inputs = JSON.parse(readFileSync(new URL('./profile-inputs.json', import.meta.url)));
requireTrue(Object.keys(payload.candidateProviders ?? {}).sort().join(',') === 'pi', 'Wrong normal provider set');
const candidate = payload.candidateProviders.pi;
requireTrue(candidate?.profileDigest === 'sha256:' + pins.pi && candidate.qualification === 'qualified', 'Normal Pi identity mismatch');
requireTrue(candidate.path === `provider-assets/pi/${platform}-${architecture}`, 'Unexpected Pi path');
requireTrue(candidate.closureDigest === inputs.candidateClosureDigests.pi?.[`${platform}-${architecture}`], 'Native Pi closure pin mismatch');
requireTrue(inputs.normalProviderSelection?.pi?.qualification === 'qualified' && inputs.normalProviderSelection.pi.profileVersion === 12 && inputs.normalProviderSelection.pi.profileDigest === candidate.profileDigest, 'Normal selection input mismatch');
return candidate;
}
export function verifyPack(pack, source, platform, architecture, verifySelection = verifyNormalPiSelection) {
const manifest = JSON.parse(readFileSync(join(pack, 'provider-pack.json')));
const p = manifest.payload;
requireTrue(manifest.schema === 'paperclip-runner/remote-provider-pack/v1', 'Wrong schema');
requireTrue(manifest.digest === `sha256:${createHash('sha256').update(canonicalJson(p)).digest('hex')}`, 'Canonical manifest mismatch');
requireTrue(p.runnerSourceRevision === source, 'Wrong source');
requireTrue(p.target.platform === platform && p.target.architecture === architecture, 'Wrong target');
const candidate = verifySelection(p, platform, architecture);
const checks = [checkedArtifact(pack, candidate, 'directory')];
for (const entry of Object.values(p.artifacts)) checks.push(checkedArtifact(pack, entry, 'file'));
requireTrue(sha256Tree(join(pack, 'dist')) === p.distDigest, 'Packed dist mismatch');
const bridge = createHash('sha256').update(p.artifacts.opencodeProxy.sha256).update('\n').update(p.artifacts.acpxSidecar.sha256).update('\n').update(p.distDigest).digest('hex');
requireTrue('sha256:' + bridge === p.bridgeDigest, 'Bridge mismatch');
const r = createRequire(join(pack, 'package.json'));
const acpx = r.resolve('acpx/package.json');
const sdk = createRequire(acpx).resolve('@agentclientprotocol/sdk');
requireTrue(JSON.parse(readFileSync(acpx)).version === '0.13.1' && sdk.includes('@agentclientprotocol+sdk@1.4.0_'), 'Wrong production ACPX/SDK');
contained(pack, acpx); contained(pack, sdk);
return { manifestDigest: manifest.digest, checks, distDigest: p.distDigest, bridgeDigest: p.bridgeDigest, acpx, sdk, target: p.target, sourceRevision: source };
}
if (process.argv[1] && import.meta.url === pathToFileURL(resolve(process.argv[1])).href) {
requireTrue(process.argv.length === 6, 'Expected pack source platform architecture');
console.log(JSON.stringify(verifyPack(...process.argv.slice(2))));
}