mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-28 12:04:41 +01:00
feat(runner): qualify native Codex platforms and cache hosted builds
Extract the tested runtime and installed-package prerequisite without changing agent creation defaults or experimental gates. Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
1 parent
7d9b93aee2
commit
ddcd746eb6
40 files changed
+3474
-565
No files matched your search
@@ -1,16 +1,64 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import { mkdirSync, mkdtempSync, readFileSync, realpathSync, rmSync, writeFileSync } from 'node:fs';
|
||||
import { createHash } from 'node:crypto';
|
||||
import { mkdirSync, mkdtempSync, readFileSync, realpathSync, rmSync, symlinkSync, writeFileSync } from 'node:fs';
|
||||
import { spawnSync } from 'node:child_process';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { join } from 'node:path';
|
||||
import test from 'node:test';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { isWorkspacePackage, parseWorkspaceEntries } from '../build-standalone-public-packages.mjs';
|
||||
import { GROK_PUBLIC_INSTALL_IMAGE, GROK_PUBLIC_INSTALL_LIFECYCLE, assertInstalledCodexPlatformProvenance, assertNoBundledCodexPayloads, grokConsumerDockerArgs, installedCodexProbeSource } from '../grok-public-install-sandbox.mjs';
|
||||
import { GROK_PUBLIC_INSTALL_IMAGE, GROK_PUBLIC_INSTALL_LIFECYCLE, assertInstalledCodexPlatformProvenance, assertInstalledNativeConsumerContinuity, assertNoBundledCodexPayloads, grokConsumerDockerArgs, installedCodexProbeSource, installedDaemonProbeSource, macNodeHeadersDirectory, macPublicInstallLifecycleProfile, macInstalledStartupProbeSource } from '../grok-public-install-sandbox.mjs';
|
||||
import { verifyPiPackagedDaemon } from '../pi-public-install-probe.mjs';
|
||||
import { runnerBinaryTarget } from '../../packages/paperclip-runner/src/live/runner-binary.ts';
|
||||
|
||||
const paths = { assets: '/private/staging/assets', consumer: '/private/staging/consumer', cache: '/private/staging/cache', uid: 1001, gid: 1001 };
|
||||
const values = (args, flag) => args.flatMap((value, index) => value === flag ? [args[index + 1]] : []);
|
||||
|
||||
test('native host startup preserves the consumer lock and both executable byte identities', () => {
|
||||
const before = { lockSha256: 'a'.repeat(64), runnerSha256: 'b'.repeat(64), codexCommandSha256: 'c'.repeat(64), codexNativeExecutableSha256: 'e'.repeat(64) };
|
||||
assert.doesNotThrow(() => assertInstalledNativeConsumerContinuity(before, { ...before }));
|
||||
for (const field of Object.keys(before)) {
|
||||
assert.throws(() => assertInstalledNativeConsumerContinuity(before, { ...before, [field]: 'd'.repeat(64) }), new RegExp(field));
|
||||
assert.throws(() => assertInstalledNativeConsumerContinuity({ ...before, [field]: '' }, { ...before, [field]: '' }), new RegExp(field));
|
||||
}
|
||||
const source = readFileSync(new URL('../verify-grok-npm-install.mjs', import.meta.url), 'utf8');
|
||||
const arm = source.split('async function verifyLinuxArmConsumer')[1].split("if (process.argv[2] === '--linux-arm64-consumer')")[0];
|
||||
assert.match(arm, /const hostEnv = \{ PATH: .*HOME: home, TMPDIR: hostTmp, LC_ALL: 'C', TZ: 'UTC' \}/);
|
||||
assert.match(arm, /execFileSync\(process.execPath, \[file\], \{ cwd: consumer, env: hostEnv/);
|
||||
assert.ok(arm.indexOf('isolated(GROK_PUBLIC_INSTALL_LIFECYCLE)') < arm.indexOf("hostProbe('host-startup-probe.mjs'"));
|
||||
assert.ok(arm.indexOf('const before =') < arm.indexOf("hostProbe('host-startup-probe.mjs'"));
|
||||
assert.ok(arm.indexOf("hostProbe('host-startup-probe.mjs'") < arm.indexOf('const after ='));
|
||||
assert.match(arm, /assertInstalledNativeConsumerContinuity\(before, after\)/);
|
||||
assert.doesNotMatch(arm, /externallySandboxed|externalSandbox|sudo|sysctl|--privileged|seccomp=unconfined|unshare/);
|
||||
});
|
||||
|
||||
test('Mac offline lifecycle uses only the matching executing Node SDK', () => {
|
||||
const root = realpathSync(mkdtempSync(join(tmpdir(), 'mac-node-headers-test-')));
|
||||
try {
|
||||
const prefix = join(root, 'sdk'), executable = join(prefix, 'bin/node'), include = join(prefix, 'include/node');
|
||||
mkdirSync(join(prefix, 'bin'), { recursive: true }); mkdirSync(include, { recursive: true });
|
||||
writeFileSync(executable, 'fixture Node executable');
|
||||
writeFileSync(join(include, 'node.h'), '#include "node_version.h"\n');
|
||||
const header = '#define NODE_MAJOR_VERSION 24\n#define NODE_MINOR_VERSION 20\n#define NODE_PATCH_VERSION 0\n';
|
||||
writeFileSync(join(include, 'node_version.h'), header);
|
||||
assert.equal(macNodeHeadersDirectory(executable, '24.20.0'), prefix);
|
||||
assert.throws(() => macNodeHeadersDirectory(executable, '24.19.0'), /must match the executing Node/);
|
||||
assert.throws(() => macNodeHeadersDirectory(executable, '24.20.0-custom'), /exact release version/);
|
||||
writeFileSync(join(include, 'node_version.h'), header + '#define NODE_PATCH_VERSION 0\n');
|
||||
assert.throws(() => macNodeHeadersDirectory(executable, '24.20.0'), /declare one patch version/);
|
||||
writeFileSync(join(include, 'node_version.h'), header);
|
||||
rmSync(join(include, 'node.h'));
|
||||
assert.throws(() => macNodeHeadersDirectory(executable, '24.20.0'), /ENOENT/);
|
||||
writeFileSync(join(root, 'outside-node.h'), '#include "node_version.h"\n');
|
||||
symlinkSync(join(root, 'outside-node.h'), join(include, 'node.h'));
|
||||
assert.throws(() => macNodeHeadersDirectory(executable, '24.20.0'), /inside the executing Node prefix/);
|
||||
const source = readFileSync(new URL('../verify-grok-npm-install.mjs', import.meta.url), 'utf8');
|
||||
assert.ok(source.includes('macNodeHeadersDirectory(process.execPath, process.versions.node)'));
|
||||
assert.ok(source.includes('consumer, { ...env, npm_config_nodedir: nodeHeaders }'), 'SDK is bound only to the deferred lifecycle environment');
|
||||
assert.equal(source.split('npm_config_nodedir:').length, 2);
|
||||
} finally { rmSync(root, { recursive: true, force: true }); }
|
||||
});
|
||||
|
||||
test('lifecycle execution has no network, host credentials, checkout, or elevated privileges', () => {
|
||||
const args = grokConsumerDockerArgs({ ...paths, command: GROK_PUBLIC_INSTALL_LIFECYCLE });
|
||||
assert.deepEqual(values(args, '--network'), ['none']);
|
||||
@@ -85,7 +133,9 @@ test('verification never elevates PR-controlled provisioning or cleanup on the h
|
||||
|
||||
test('the image and Grok downloads start before staging and join without weakening the npm checks', () => {
|
||||
const source = readFileSync(new URL('../verify-grok-npm-install.mjs', import.meta.url), 'utf8');
|
||||
const position = fragment => { const index = source.indexOf(fragment); assert.notEqual(index, -1, `verifier must contain ${fragment}`); return index; };
|
||||
// The optional Mac consumer owns separate scratch and no Linux prefetches.
|
||||
const linuxSource = source.slice(source.indexOf("assert.equal(process.platform, 'linux'"));
|
||||
const position = fragment => { const index = linuxSource.indexOf(fragment); assert.notEqual(index, -1, `verifier must contain ${fragment}`); return index; };
|
||||
// Exactly the pinned consumer image is pulled, for the platform the probes run on.
|
||||
const imagePull = position("prefetch('image', 'docker', ['pull', '--platform', 'linux/amd64', GROK_PUBLIC_INSTALL_IMAGE])");
|
||||
// Grok is provisioned into the prefetch directory, never straight into `native/grok`.
|
||||
@@ -109,7 +159,7 @@ test('the image and Grok downloads start before staging and join without weakeni
|
||||
// cleanup waits for them before removing the directory they write into.
|
||||
assert.ok(source.includes("import { startPrefetch, stopPrefetches } from './prefetch-child.mjs'"));
|
||||
assert.ok(position('await stopPrefetches(prefetches)') < position('rmSync(root, { recursive: true, force: true })'));
|
||||
assert.equal(source.match(/rmSync\(root/g).length, 1);
|
||||
assert.equal(linuxSource.match(/rmSync\(root/g).length, 1);
|
||||
});
|
||||
|
||||
test('the bundled-dependency installs run together while the other packages are copied, and pack in order', () => {
|
||||
@@ -137,7 +187,8 @@ test('the bundled-dependency installs run together while the other packages are
|
||||
const checks = position("for (const tarball of tarballs) assertNoBundledCodexPayloads(run('tar', ['-tzf', tarball])");
|
||||
const copied = position('for (const tarball of tarballs) {');
|
||||
assert.ok(bundledSources < start && start < registered && registered < otherSources && otherSources < join && join < packs && packs < reordered && reordered < checks && checks < copied);
|
||||
assert.equal(source.match(/run\('npm', \['pack'/g).length, 1, 'only the lifecycle sentinel is packed in-process');
|
||||
const packagingLane = source.slice(position("} else {\nassert.equal(process.platform, 'linux'"));
|
||||
assert.equal(packagingLane.match(/run\('npm', \['pack'/g).length, 1, 'only the lifecycle sentinel is packed in-process in the packaging lane');
|
||||
assert.ok(source.includes("destination: join(root, `pack-${index}`)"), 'each pack writes into its own directory');
|
||||
// The timing line reports the split staging phase, each install, and the
|
||||
// pack step with what the packs would have cost one after another.
|
||||
@@ -201,6 +252,14 @@ test('the installed Codex probe exercises the public export and rejects incomple
|
||||
const platformPath = `node_modules/@openai/codex-${target}`;
|
||||
const platform = join(consumer, platformPath); mkdirSync(platform, { recursive: true });
|
||||
const nativeVendor = join(platform, 'vendor'); mkdirSync(nativeVendor);
|
||||
const triple = { 'linux-x64': 'x86_64-unknown-linux-musl', 'linux-arm64': 'aarch64-unknown-linux-musl',
|
||||
'darwin-x64': 'x86_64-apple-darwin', 'darwin-arm64': 'aarch64-apple-darwin', 'win32-x64': 'x86_64-pc-windows-msvc', 'win32-arm64': 'aarch64-pc-windows-msvc' }[target];
|
||||
const nativeBytes = Buffer.alloc(64);
|
||||
if (process.platform === 'darwin') { nativeBytes.writeUInt32LE(0xfeedfacf); nativeBytes.writeUInt32LE(process.arch === 'arm64' ? 0x0100000c : 0x01000007, 4); }
|
||||
else if (process.platform === 'win32') { nativeBytes.set([0x4d, 0x5a]); nativeBytes.writeUInt32LE(40, 60); nativeBytes.writeUInt32LE(0x00004550, 40); nativeBytes.writeUInt16LE(process.arch === 'arm64' ? 0xaa64 : 0x8664, 44); }
|
||||
else { nativeBytes.set([0x7f, 0x45, 0x4c, 0x46, 2, 1]); nativeBytes.writeUInt16LE(process.arch === 'arm64' ? 183 : 62, 18); }
|
||||
const nativeFile = join(nativeVendor, triple, 'bin', process.platform === 'win32' ? 'codex.exe' : 'codex');
|
||||
mkdirSync(join(nativeVendor, triple, 'bin'), { recursive: true }); writeFileSync(nativeFile, nativeBytes, { mode: 0o755 });
|
||||
const manifest = { name: '@openai/codex', version: `0.160.0-${target}`, os: [process.platform], cpu: [process.arch] };
|
||||
writeFileSync(join(platform, 'package.json'), JSON.stringify(manifest));
|
||||
const entry = { name: manifest.name, version: manifest.version,
|
||||
@@ -209,6 +268,8 @@ test('the installed Codex probe exercises the public export and rejects incomple
|
||||
writeFileSync(join(consumer, 'package-lock.json'), JSON.stringify(lock));
|
||||
const server = join(consumer, 'node_modules/@paperclipai/server');
|
||||
const vendor = join(server, 'dist/vendor/paperclip-runner'), drivers = join(vendor, 'drivers/acpx'); mkdirSync(drivers, { recursive: true });
|
||||
mkdirSync(join(vendor, 'live'));
|
||||
writeFileSync(join(vendor, 'live/runner-binary.js'), `export const runnerBinaryTarget = ${runnerBinaryTarget.toString()};`);
|
||||
writeFileSync(join(server, 'package.json'), JSON.stringify({ name: '@paperclipai/server', type: 'module' }));
|
||||
const integrity = join(drivers, 'installation-integrity.js');
|
||||
const security = join(vendor, 'drivers/codex/codex-security-config.js'); mkdirSync(join(vendor, 'drivers/codex'));
|
||||
@@ -233,6 +294,24 @@ test('the installed Codex probe exercises the public export and rejects incomple
|
||||
assert.equal(JSON.parse(result.stdout).pinnedCodexCommandVerified, true);
|
||||
assert.equal(JSON.parse(result.stdout).codexPlatformPackageSource, 'official npm');
|
||||
assert.equal(JSON.parse(result.stdout).codexConsumerPlatform, target);
|
||||
assert.equal(JSON.parse(result.stdout).codexCommandSha256, createHash('sha256').update(readFileSync(command)).digest('hex'));
|
||||
assert.equal(JSON.parse(result.stdout).codexNativeExecutableSha256, createHash('sha256').update(nativeBytes).digest('hex'));
|
||||
const original = JSON.parse(result.stdout);
|
||||
nativeBytes[63] = 1; writeFileSync(nativeFile, nativeBytes);
|
||||
result = run(); assert.equal(result.status, 0, result.stderr);
|
||||
const changed = JSON.parse(result.stdout);
|
||||
assert.equal(changed.codexCommandSha256, original.codexCommandSha256, 'native mutation does not change the npm launcher');
|
||||
assert.notEqual(changed.codexNativeExecutableSha256, original.codexNativeExecutableSha256);
|
||||
assert.throws(() => assertInstalledNativeConsumerContinuity({ lockSha256: 'a'.repeat(64), runnerSha256: 'b'.repeat(64), ...original },
|
||||
{ lockSha256: 'a'.repeat(64), runnerSha256: 'b'.repeat(64), ...changed }), /codexNativeExecutableSha256/);
|
||||
nativeBytes[63] = 0; writeFileSync(nativeFile, nativeBytes);
|
||||
rmSync(nativeFile);
|
||||
result = run(); assert.notEqual(result.status, 0); assert.match(result.stderr, /one native executable/);
|
||||
const foreignNative = join(root, 'foreign-codex'); writeFileSync(foreignNative, nativeBytes, { mode: 0o755 });
|
||||
symlinkSync(foreignNative, nativeFile);
|
||||
result = run(); assert.notEqual(result.status, 0); assert.match(result.stderr, /bounded regular file in its selected platform package/);
|
||||
rmSync(nativeFile); writeFileSync(nativeFile, nativeBytes, { mode: 0o755 });
|
||||
result = run(); assert.equal(result.status, 0, result.stderr);
|
||||
assert.equal(JSON.parse(result.stdout).codexQualifiedInstallationVerified, true);
|
||||
assert.equal(JSON.parse(result.stdout).codexCommandLeaseVerified, true);
|
||||
assert.equal(JSON.parse(result.stdout).codexNativeSandboxResourcesVerified, true);
|
||||
@@ -253,6 +332,20 @@ test('the installed Codex probe exercises the public export and rejects incomple
|
||||
writeFileSync(legacyCommand, '#!/bin/sh\nexit 0\n', { mode: 0o755 });
|
||||
writeFileSync(index, `export const resolvePinnedCodexCommand = () => ${JSON.stringify(legacyCommand)};`);
|
||||
result = run(); assert.notEqual(result.status, 0); assert.match(result.stderr, /Selected Codex platform version must match the qualified pin/);
|
||||
// Native Codex uses protocol compatibility, not the ACPX version allowlist.
|
||||
// Keep exact official-npm host provenance and the selected resource boundary.
|
||||
mkdirSync(join(legacy, 'vendor'));
|
||||
const legacyNative = join(legacy, 'vendor', triple, 'codex', process.platform === 'win32' ? 'codex.exe' : 'codex');
|
||||
mkdirSync(join(legacy, 'vendor', triple, 'codex'), { recursive: true }); writeFileSync(legacyNative, nativeBytes, { mode: 0o755 });
|
||||
writeFileSync(legacyCommand, '#!/bin/sh\nprintf "codex-cli 0.156.1\\n"\n', { mode: 0o755 });
|
||||
writeFileSync(security, securitySource.replaceAll(JSON.stringify(nativeVendor), JSON.stringify(join(legacy, 'vendor'))));
|
||||
writeFileSync(probe, installedCodexProbeSource(index, consumer, undefined, { nativeCodex: true }));
|
||||
result = run(); assert.equal(result.status, 0, result.stderr);
|
||||
assert.equal(JSON.parse(result.stdout).installedCodexVersion, 'codex-cli 0.156.1');
|
||||
assert.equal(JSON.parse(result.stdout).codexQualifiedInstallationVerified, undefined);
|
||||
writeFileSync(probe, installedCodexProbeSource(index, consumer, '0.160.0'));
|
||||
writeFileSync(security, securitySource);
|
||||
|
||||
writeFileSync(index, `import { writeFileSync } from 'node:fs'; export const resolvePinnedCodexCommand = () => {
|
||||
writeFileSync(${JSON.stringify(join(platform, 'package.json'))}, ${JSON.stringify(JSON.stringify({ ...manifest, altered: true }))});
|
||||
return ${JSON.stringify(command)}; };`);
|
||||
@@ -308,6 +401,7 @@ test('actual tar member checks allow Codex JavaScript but reject nested platform
|
||||
assert.doesNotThrow(() => assertNoBundledCodexPayloads(javascript));
|
||||
for (const payload of [
|
||||
'package/node_modules/@openai/codex-linux-x64/package.json',
|
||||
'package/node_modules/@openai/codex-linux-arm64/package.json',
|
||||
'package/node_modules/bridge/node_modules/@openai/codex-darwin-arm64/package.json',
|
||||
'package/node_modules/@openai/codex-win32-x64/package.json',
|
||||
'package/node_modules/@openai/codex/vendor/x86_64-unknown-linux-musl/codex/codex',
|
||||
@@ -320,24 +414,129 @@ test('actual tar member checks allow Codex JavaScript but reject nested platform
|
||||
});
|
||||
|
||||
test('consumer provenance rejects bundled, foreign, inconsistent and non-official Codex payloads', () => {
|
||||
const path = 'node_modules/@openai/codex-linux-x64';
|
||||
const manifest = { name: '@openai/codex', version: '0.160.0-linux-x64', os: ['linux'], cpu: ['x64'] };
|
||||
for (const target of ['linux-x64', 'linux-arm64']) {
|
||||
const path = `node_modules/@openai/codex-${target}`;
|
||||
const manifest = { name: '@openai/codex', version: `0.160.0-${target}`, os: ['linux'], cpu: [target.slice('linux-'.length)] };
|
||||
const entry = { version: manifest.version, resolved: `https://registry.npmjs.org/@openai/codex/-/codex-${manifest.version}.tgz`,
|
||||
integrity: `sha512-${Buffer.alloc(64, 1).toString('base64')}` };
|
||||
const check = (value = entry, installed = [{ path, manifest }]) => assertInstalledCodexPlatformProvenance({ packages: { [path]: value } }, installed, 'linux-x64');
|
||||
const check = (value = entry, installed = [{ path, manifest }]) => assertInstalledCodexPlatformProvenance({ packages: { [path]: value } }, installed, target);
|
||||
assert.equal(check().codexConsumerHostOnly, true);
|
||||
const legacyManifest = { ...manifest, version: '0.156.1-linux-x64' };
|
||||
const legacyManifest = { ...manifest, version: `0.156.1-${target}` };
|
||||
assert.equal(check({ ...entry, version: legacyManifest.version,
|
||||
resolved: 'https://registry.npmjs.org/@openai/codex/-/codex-0.156.1-linux-x64.tgz' }, [{ path, manifest: legacyManifest }]).codexConsumerHostOnly, true);
|
||||
assert.throws(() => check(entry, [{ path, manifest: { ...manifest, version: '^0.160.0-linux-x64' } }]), /exact host version/);
|
||||
resolved: `https://registry.npmjs.org/@openai/codex/-/codex-0.156.1-${target}.tgz` }, [{ path, manifest: legacyManifest }]).codexConsumerHostOnly, true);
|
||||
assert.throws(() => check(entry, [{ path, manifest: { ...manifest, version: `^0.160.0-${target}` } }]), /exact host version/);
|
||||
for (const change of [
|
||||
{ inBundle: true }, { link: true }, { version: '0.156.1-linux-x64' },
|
||||
{ resolved: 'https://example.com/@openai/codex/-/codex-0.160.0-linux-x64.tgz' },
|
||||
{ resolved: 'https://registry.npmjs.org/@openai/codex/-/codex-0.156.1-linux-x64.tgz' },
|
||||
{ resolved: 'https://user@registry.npmjs.org/@openai/codex/-/codex-0.160.0-linux-x64.tgz' },
|
||||
{ inBundle: true }, { link: true }, { version: `0.156.1-${target}` },
|
||||
{ resolved: `https://example.com/@openai/codex/-/codex-0.160.0-${target}.tgz` },
|
||||
{ resolved: `https://registry.npmjs.org/@openai/codex/-/codex-0.156.1-${target}.tgz` },
|
||||
{ resolved: `https://user@registry.npmjs.org/@openai/codex/-/codex-0.160.0-${target}.tgz` },
|
||||
{ integrity: undefined }, { integrity: 'sha512-AA==' },
|
||||
]) assert.throws(() => check({ ...entry, ...change }));
|
||||
assert.throws(() => check(entry, []), /must install its host/);
|
||||
assert.throws(() => check(entry, [{ path: 'node_modules/@openai/codex-darwin-arm64', manifest }]), /only the host/);
|
||||
assert.throws(() => check(entry, [{ path, manifest: { ...manifest, name: 'untrusted' } }]), /identity/);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
test('hosted Mac lifecycle denies OS networking and writes outside owned scratch', () => {
|
||||
const profile = macPublicInstallLifecycleProfile('/Users/runner/work/_temp/owned-check');
|
||||
assert.match(profile, /\(deny network\*\)/);
|
||||
assert.match(profile, /\(deny file-write\*/);
|
||||
assert.match(profile, /subpath "\/Users\/runner\/work\/_temp\/owned-check"/);
|
||||
assert.throws(() => macPublicInstallLifecycleProfile('relative'), /absolute/);
|
||||
assert.throws(() => macPublicInstallLifecycleProfile('/tmp/owned\0bad'), /absolute/);
|
||||
const source = readFileSync(new URL('../verify-grok-npm-install.mjs', import.meta.url), 'utf8');
|
||||
assert.match(source, /process.env.GITHUB_ACTIONS, 'true'/);
|
||||
assert.match(source, /networkProbe, 'allowed'/);
|
||||
assert.match(source, /networkProbe, 'denied'/);
|
||||
assert.match(source, /error.code,'EPERM'/);
|
||||
assert.match(source, /'rebuild', '--offline', '--ignore-scripts=false', '--dangerously-allow-all-scripts'/);
|
||||
assert.match(source, /readFileSync\(sentinel, 'utf8'\), 'ok'/);
|
||||
assert.match(source, /readFileSync\(join\(consumer, 'package-lock.json'\), 'utf8'\), lock/);
|
||||
assert.match(source, /finally \{ rmSync\(root, \{ recursive: true, force: true \}\); \}/);
|
||||
});
|
||||
|
||||
test('installed daemon proof uses the production host resolver and complete source-bound manifest', () => {
|
||||
const source = installedDaemonProbeSource('/consumer/server/dist/vendor/paperclip-runner', '/consumer', 'a'.repeat(40));
|
||||
assert.match(source, /defaultCapabilityRunnerdBinary\(\)/);
|
||||
assert.match(source, /Object.keys\(manifest.platforms\).sort\(\), targets.sort\(\)/);
|
||||
assert.match(source, /manifest.sourceRevision, "a{40}"/);
|
||||
assert.match(source, /runnerBinaryTarget\(bytes\), target/);
|
||||
assert.match(source, /createHash\('sha256'\)/);
|
||||
assert.match(source, /metadata.binaryContractVersion, expected.contracts.runnerdArtifact/);
|
||||
assert.match(source, /'linux-arm64'/);
|
||||
assert.doesNotMatch(source, /PAPERCLIP_RUNNERD_BINARY|runner\/target\/debug|execFileSync\('paperclip-runnerd'/);
|
||||
});
|
||||
|
||||
test('native ARM clean consumers keep the existing container and installed-runtime boundaries', () => {
|
||||
const args = grokConsumerDockerArgs({ ...paths, platform: 'linux/arm64', command: GROK_PUBLIC_INSTALL_LIFECYCLE });
|
||||
assert.deepEqual(values(args, '--platform'), ['linux/arm64']);
|
||||
assert.deepEqual(values(args, '--network'), ['none']);
|
||||
assert.deepEqual(values(args, '--user'), ['1001:1001']);
|
||||
assert.ok(args.includes('--read-only'));
|
||||
assert.deepEqual(values(args, '--cap-drop'), ['ALL']);
|
||||
assert.deepEqual(values(args, '--security-opt'), ['no-new-privileges']);
|
||||
assert.ok(values(args, '--mount').some(value => value.endsWith('dst=/packages,readonly')));
|
||||
assert.throws(() => grokConsumerDockerArgs({ ...paths, platform: 'linux/arm/v7', command: [] }), /supported native Linux platform/);
|
||||
const source = readFileSync(new URL('../verify-grok-npm-install.mjs', import.meta.url), 'utf8');
|
||||
const arm = source.split('async function verifyLinuxArmConsumer(')[1].split("if (process.argv[2] === '--linux-arm64-consumer')")[0];
|
||||
assert.match(arm, /process.platform \+ '-' \+ process.arch, 'linux-arm64'/);
|
||||
assert.match(arm, /const platform = 'linux\/arm64'/);
|
||||
assert.match(arm, /isolated\(GROK_PUBLIC_INSTALL_LIFECYCLE\)/);
|
||||
assert.match(arm, /installedCodexProbeSource\(installed \+ '\/index.js', '\/consumer', undefined, \{ nativeCodex: true \}\)/);
|
||||
assert.match(arm, /tests\/runner-e2e\/installed-cli.ts/);
|
||||
assert.match(arm, /process.versions.node.split\('\.'\)\[0\], '24'/);
|
||||
assert.match(arm, /macInstalledStartupProbeSource\(consumer, hostServer, daemon.sha256, join\(assets, 'installed-cli.ts'\)\)/);
|
||||
assert.match(arm, /startupEnvironment: 'native host, same installed graph, isolated empty homes'/);
|
||||
assert.doesNotMatch(arm, /stage-runner|runner\/target|PAPERCLIP_RUNNER_BINARY|prerequisite:/);
|
||||
});
|
||||
|
||||
|
||||
test('Mac startup wiring normalizes only validated manifest digests and uses server-scoped tooling', () => {
|
||||
const source = macInstalledStartupProbeSource('/owned/consumer', '/owned/consumer/server', 'sha256:' + 'a'.repeat(64), '/source/tests/runner-e2e/installed-cli.ts');
|
||||
assert.match(source, /expectedRunnerSha256:"a{64}"/);
|
||||
assert.doesNotMatch(source, /expectedRunnerSha256:"sha256:/);
|
||||
for (const digest of ['a'.repeat(64), 'sha256:bad', 'sha512:' + 'a'.repeat(64), 'sha256:' + 'A'.repeat(64)]) {
|
||||
assert.throws(() => macInstalledStartupProbeSource('/owned/consumer', '/owned/consumer/server', digest, '/source/helper.ts'), /verified SHA-256/);
|
||||
}
|
||||
const verifier = readFileSync(new URL('../verify-grok-npm-install.mjs', import.meta.url), 'utf8');
|
||||
assert.match(verifier, /\['--import', 'tsx', startupFile\], join\(repo, 'server'\)/);
|
||||
const closedEnvironment = verifier.match(/const env = \{ PATH: `\$\{tools\}[\s\S]*?\};/)[0];
|
||||
assert.doesNotMatch(closedEnvironment, /NODE_PATH|NODE_OPTIONS|\.\.\.process.env/);
|
||||
});
|
||||
|
||||
test('Pi admission verifies assembled host daemon identity and preserves the flat source-package control', async () => {
|
||||
const root = realpathSync(mkdtempSync(join(tmpdir(), 'pi-installed-daemon-'))), server = join(root, 'server');
|
||||
const output = join(server, 'dist/vendor/paperclip-runner'), target = 'linux-x64', source = 'a'.repeat(40);
|
||||
const binary = join(output, 'bin', target, 'paperclip-runnerd'), manifestPath = join(output, 'bin/release-manifest.json');
|
||||
const bytes = Buffer.alloc(64); bytes.set([0x7f, 0x45, 0x4c, 0x46, 2, 1]); bytes.writeUInt16LE(62, 18);
|
||||
const sha256 = value => `sha256:${createHash('sha256').update(value).digest('hex')}`;
|
||||
const manifest = { schema: 'paperclip.runner.release-binaries.v1', sourceRevision: source,
|
||||
platforms: { [target]: { path: `${target}/paperclip-runnerd`, sha256: sha256(bytes) } } };
|
||||
const check = () => verifyPiPackagedDaemon(server, binary, target, runnerBinaryTarget);
|
||||
try {
|
||||
mkdirSync(join(output, 'bin', target), { recursive: true });
|
||||
writeFileSync(binary, bytes); writeFileSync(join(server, 'dist/build-info.json'), JSON.stringify({ commit: source }));
|
||||
writeFileSync(manifestPath, JSON.stringify(manifest));
|
||||
await check();
|
||||
for (const changed of [null, { ...manifest, schema: 'untrusted' }, { ...manifest, sourceRevision: 'b'.repeat(40) },
|
||||
{ ...manifest, platforms: { [target]: { ...manifest.platforms[target], path: '../outside' } } },
|
||||
{ ...manifest, platforms: { [target]: { ...manifest.platforms[target], sha256: 'sha256:' + '0'.repeat(64) } } }]) {
|
||||
writeFileSync(manifestPath, JSON.stringify(changed)); await assert.rejects(check());
|
||||
}
|
||||
const wrongHeader = Buffer.from(bytes); wrongHeader.writeUInt16LE(183, 18);
|
||||
writeFileSync(binary, wrongHeader);
|
||||
writeFileSync(manifestPath, JSON.stringify({ ...manifest, platforms: { [target]: { ...manifest.platforms[target], sha256: sha256(wrongHeader) } } }));
|
||||
await assert.rejects(check(), /linux-arm64/);
|
||||
writeFileSync(binary, bytes); writeFileSync(manifestPath, JSON.stringify(manifest));
|
||||
const outside = join(root, 'outside'); writeFileSync(outside, bytes);
|
||||
rmSync(binary); symlinkSync(outside, binary); await assert.rejects(check());
|
||||
rmSync(binary); writeFileSync(binary, bytes); rmSync(manifestPath);
|
||||
const flat = join(output, 'bin/paperclip-runnerd'); writeFileSync(flat, bytes);
|
||||
await verifyPiPackagedDaemon(server, flat, target, runnerBinaryTarget);
|
||||
await assert.rejects(check(), /strictly equal/);
|
||||
writeFileSync(manifestPath, 'null');
|
||||
await assert.rejects(verifyPiPackagedDaemon(server, flat, target, runnerBinaryTarget), /manifest must be an object/);
|
||||
} finally { rmSync(root, { recursive: true, force: true }); }
|
||||
});
|
||||
@@ -1,8 +1,10 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { spawnSync } from "node:child_process";
|
||||
import { copyFileSync, existsSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
||||
import { spawn, spawnSync } from "node:child_process";
|
||||
import { createHash } from "node:crypto";
|
||||
import { copyFileSync, existsSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, realpathSync, rmSync, writeFileSync } from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { tmpdir } from "node:os";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import test from "node:test";
|
||||
import { createServer } from "node:http";
|
||||
@@ -81,14 +83,14 @@ test("chaos verification isolates callers that verify the same source commit", (
|
||||
|
||||
test("canary reuses exact-source proof while stable keeps full verification", () => {
|
||||
const releaseWorkflow = readWorkflow("release.yml");
|
||||
const canary = releaseWorkflow.split(" verify_canary:\n")[1].split("\n publish_canary:")[0];
|
||||
const canary = releaseWorkflow.match(/ verify_canary:\n[\s\S]*?(?=\n [a-z_]+:)/)[0];
|
||||
assert.match(canary, /github\.repository == 'paperclipai\/paperclip' && github\.event_name == 'push' && github\.ref == 'refs\/heads\/master'/);
|
||||
assert.match(canary, /actions: read/);
|
||||
assert.match(canary, /ref: \$\{\{ github\.sha \}\}/);
|
||||
assert.match(canary, /SOURCE_SHA: \$\{\{ github\.sha \}\}/);
|
||||
assert.match(canary, /run: node scripts\/cloud-source-verification\.mjs "\$SOURCE_SHA"/);
|
||||
assert.doesNotMatch(canary, /release-verify\.yml|continue-on-error|always\(\)/);
|
||||
assert.match(releaseWorkflow, /publish_canary:\n\s+if: github\.event_name == 'push'\n\s+needs: verify_canary/);
|
||||
assert.match(releaseWorkflow, /publish_canary:\n\s+if: .*github\.event_name == 'push'.*\n\s+needs: \[verify_canary, release_runner_source, release_runner_binaries\]/);
|
||||
// The stable lane is gated on the stable channel since the nightly lane
|
||||
// was added; a `needs:` line (for example a preflight job) may sit between
|
||||
// the gate and the delegation.
|
||||
@@ -742,6 +744,399 @@ test("direct protocol concurrency override only lowers the configured ceiling",
|
||||
}
|
||||
});
|
||||
|
||||
test("release daemon production uses exact-source native hosts and no publisher or provider permissions", () => {
|
||||
const workflow = readWorkflow("release-verify.yml");
|
||||
const producer = workflow.split(" runner_binaries:\n")[1];
|
||||
const targets = workflow.split(" runner_binary_targets:\n")[1].split(" runner_binaries:\n")[0];
|
||||
for (const [target, host] of [["linux-arm64", "ubuntu-24.04-arm"], ["darwin-arm64", "macos-15"], ["darwin-x64", "macos-15-intel"]]) {
|
||||
assert.ok(targets.includes(`'${target}': '${host}'`));
|
||||
}
|
||||
assert.match(producer, /matrix: \$\{\{ fromJSON\(needs.runner_binary_targets.outputs.matrix\) \}\}/);
|
||||
assert.match(targets, /Unknown exact-source daemon platform contract/);
|
||||
assert.match(targets, /RELEASE_RUNNER_TARGETS/);
|
||||
assert.doesNotMatch(targets, /secrets\.|id-token: write|contents: write|npm publish|environment:/);
|
||||
assert.match(producer, /persist-credentials: false/);
|
||||
assert.match(producer, /contents: read/);
|
||||
assert.doesNotMatch(producer, /secrets\.|id-token: write|contents: write|npm publish|environment:/);
|
||||
assert.match(producer, /\[\[ "\$SOURCE_SHA" =~ \^\[a-f0-9\]\{40\}\$ \]\]/);
|
||||
assert.match(producer, /git rev-parse HEAD/);
|
||||
assert.match(producer, /process.platform \+ "-" \+ process.arch/);
|
||||
assert.match(producer, /\['native', \['build:binary'\]/);
|
||||
const runner = JSON.parse(readFileSync(path.join(repoRoot, 'packages/paperclip-runner/package.json'), 'utf8'));
|
||||
assert.match(runner.scripts['build:binary'], /cargo build --release --manifest-path runner\/Cargo.toml --locked/);
|
||||
assert.match(runner.scripts['build:binary'], /&& node scripts\/stage-runner-binary.mjs$/);
|
||||
assert.match(producer, /stage-release-runner-binaries.mjs record/);
|
||||
for (const job of ["runner_chaos_evals", "typecheck", "general_tests", "serialized_tests", "runner_workflow_evals", "verify_paperclip_runner", "build"]) {
|
||||
assert.ok(workflow.includes(`${job}:\n if: \${{ !inputs.runner_binaries_only }}`));
|
||||
}
|
||||
const release = readWorkflow("release.yml");
|
||||
for (const job of ["publish_canary", "publish_nightly", "publish_beta", "preview_stable", "publish_stable"]) {
|
||||
const block = release.split(` ${job}:\n`)[1].split(/\n [a-z_]+:\n/)[0];
|
||||
assert.match(block, /needs: .*release_runner_binaries/);
|
||||
assert.match(block, /pattern: \$\{\{ needs.release_runner_binaries.outputs.runner_artifact_prefix \}\}-\*/);
|
||||
assert.match(block, /PAPERCLIP_RELEASE_RUNNER_ARTIFACTS_DIR/);
|
||||
}
|
||||
const script = readFileSync(path.join(repoRoot, 'scripts/release.sh'), 'utf8');
|
||||
assert.ok(script.indexOf('assemble-daemons "$PAPERCLIP_RELEASE_RUNNER_ARTIFACTS_DIR"') < script.indexOf('scripts/build-standalone-public-packages.mjs'));
|
||||
assert.match(script, /cp -R .*paperclip-runner\/dist\/bin\/\." .*server\/dist\/vendor\/paperclip-runner\/bin\//);
|
||||
assert.match(script, /elif \[ "\$dry_run" = false \]; then[\s\S]*?release_fail "Public releases require/);
|
||||
});
|
||||
|
||||
function nativeProducer() {
|
||||
return readWorkflow('release-verify.yml').split(' runner_binaries:\n')[1];
|
||||
}
|
||||
|
||||
test('native dependency caches are target-bound and unmerged master callers cannot save them', () => {
|
||||
const producer = nativeProducer();
|
||||
const cache = producer.split(' - name: Cache native Rust dependencies\n')[1].split('\n - ')[0];
|
||||
assert.match(cache, /Swatinem\/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6/);
|
||||
assert.match(cache, /workspaces: \$\{\{ steps.native_rust_workspace.outputs.path \}\} -> target/);
|
||||
assert.match(cache, /shared-key: release-native-v2-\$\{\{ matrix.host \}\}-\$\{\{ matrix.target \}\}-\$\{\{ hashFiles\('packages\/paperclip-runner\/protocol\/\*\*', 'packages\/paperclip-runner\/src\/\*\*', 'packages\/paperclip-runner\/\*.json'\) \}\}/);
|
||||
for (const option of ['cache-workspace-crates', 'cache-bin', 'cache-on-failure']) {
|
||||
assert.match(cache, new RegExp(option + ': false'));
|
||||
}
|
||||
const expression = cache.match(/save-if: \$\{\{ (.+) \}\}/)?.[1];
|
||||
assert.ok(expression, 'cache writes need a separate source trust gate');
|
||||
const canSave = new Function('github', 'inputs', `return (${expression});`);
|
||||
const github = { repository: 'paperclipai/paperclip', repository_id: '1170821064', event_name: 'push', ref: 'refs/heads/master', sha: 'a'.repeat(40) };
|
||||
assert.equal(canSave(github, { ref: github.sha }), true);
|
||||
for (const [overrides, input] of [
|
||||
[{}, 'b'.repeat(40)], [{}, ''],
|
||||
[{ event_name: 'workflow_dispatch' }, github.sha],
|
||||
[{ event_name: 'workflow_dispatch' }, 'b'.repeat(40)],
|
||||
[{ event_name: 'pull_request', ref: 'refs/pull/123/merge' }, github.sha],
|
||||
[{ ref: 'refs/heads/topic' }, github.sha],
|
||||
[{ repository: 'fork/paperclip' }, github.sha],
|
||||
[{ repository_id: '1' }, github.sha],
|
||||
]) assert.equal(canSave({ ...github, ...overrides }, { ref: input }), false);
|
||||
assert.match(producer, /pinned="\$HOME\/paperclip-runner-rust"/);
|
||||
assert.match(producer, /ln -s "\$GITHUB_WORKSPACE\/packages\/paperclip-runner\/runner" "\$pinned"/);
|
||||
assert.match(producer, /echo "RUSTUP_TOOLCHAIN=\$toolchain" >> "\$GITHUB_ENV"/);
|
||||
assert.doesNotMatch(producer, /cache-hit|continue-on-error/);
|
||||
assert.ok(producer.indexOf('Bind immutable producer source and target') < producer.indexOf('Cache native Rust dependencies'));
|
||||
assert.ok(producer.indexOf('Build TypeScript contract and host daemon concurrently') < producer.indexOf('Record actual daemon metadata and immutable bytes'));
|
||||
});
|
||||
|
||||
function nativePreparationScript() {
|
||||
const step = nativeProducer().split(' - name: Build TypeScript contract and host daemon concurrently\n')[1].split('\n - ')[0];
|
||||
const script = step.match(/node --input-type=module <<'NODE'\n([\s\S]*?)\n NODE/)?.[1];
|
||||
assert.ok(script);
|
||||
return script.replace(/^ {10}/gm, '');
|
||||
}
|
||||
|
||||
const fixtureProcessGone = pid => {
|
||||
try { process.kill(pid, 0); } catch (error) { return error.code === 'ESRCH'; }
|
||||
try { return /^\d+ \(.*\) Z /.test(readFileSync(`/proc/${pid}/stat`, 'utf8')); } catch { return false; }
|
||||
};
|
||||
const waitForFixture = async (predicate, label) => {
|
||||
for (let attempt = 0; attempt < 200; attempt++) {
|
||||
if (predicate()) return;
|
||||
await new Promise(resolve => setTimeout(resolve, 25));
|
||||
}
|
||||
assert.fail(label);
|
||||
};
|
||||
|
||||
// Execute the workflow's actual coordinator with two tiny local Node commands.
|
||||
// Both must cross the start barrier before either can finish; no pnpm, Cargo,
|
||||
// provider, registry, or Docker build runs in these fixtures.
|
||||
async function withNativePreparationFixture(mode, check) {
|
||||
const root = realpathSync(mkdtempSync(path.join(tmpdir(), 'native-preparation-')));
|
||||
let controller;
|
||||
try {
|
||||
for (const directory of ['scripts', 'tools', 'packages/paperclip-runner', 'logs']) {
|
||||
mkdirSync(path.join(root, directory), { recursive: true });
|
||||
}
|
||||
for (const helper of ['prefetch-child.mjs', 'prefetch-supervisor.mjs']) {
|
||||
copyFileSync(path.join(repoRoot, 'scripts', helper), path.join(root, 'scripts', helper));
|
||||
}
|
||||
writeFileSync(path.join(root, 'prepare.mjs'), nativePreparationScript());
|
||||
writeFileSync(path.join(root, 'tools/pnpm'), `#!${process.execPath}
|
||||
const { spawn } = require('node:child_process');
|
||||
const { existsSync, writeFileSync } = require('node:fs');
|
||||
const { join } = require('node:path');
|
||||
const root = process.env.FIXTURE_ROOT;
|
||||
const name = process.argv.includes('build:binary') ? 'native' : 'typescript';
|
||||
writeFileSync(join(root, name + '.started'), JSON.stringify({ pid: process.pid, cwd: process.cwd(), args: process.argv.slice(2) }));
|
||||
const wait = setInterval(() => {
|
||||
if (!existsSync(join(root, 'native.started')) || !existsSync(join(root, 'typescript.started'))) return;
|
||||
clearInterval(wait);
|
||||
if (process.env.FIXTURE_MODE === 'success') {
|
||||
setTimeout(() => { writeFileSync(join(root, name + '.finished'), 'joined'); console.log(name + ' complete'); }, 150);
|
||||
return;
|
||||
}
|
||||
const child = spawn(process.execPath, ['-e', \
|
||||
"const fs=require('node:fs'); fs.writeFileSync(process.argv[1], String(process.pid)); setInterval(() => fs.writeFileSync(process.argv[2], String(Date.now())), 25)",\
|
||||
join(root, name + '.descendant'), join(root, name + '.writing')], { stdio: 'inherit' });
|
||||
child.on('exit', () => process.exit(0));
|
||||
const barrier = setInterval(() => {
|
||||
if (!existsSync(join(root, 'native.descendant')) || !existsSync(join(root, 'typescript.descendant'))) return;
|
||||
clearInterval(barrier);
|
||||
if (process.env.FIXTURE_MODE === name) { console.error(name + ' failed'); process.exit(23); }
|
||||
}, 10);
|
||||
}, 10);
|
||||
`, { mode: 0o755 });
|
||||
controller = spawn(process.execPath, ['prepare.mjs'], { cwd: root, env: {
|
||||
...process.env, PATH: `${path.join(root, 'tools')}:${process.env.PATH}`, RUNNER_TEMP: path.join(root, 'logs'), FIXTURE_ROOT: root, FIXTURE_MODE: mode,
|
||||
}, stdio: ['ignore', 'pipe', 'pipe'] });
|
||||
let stdout = '', stderr = '';
|
||||
controller.stdout.on('data', chunk => { stdout += chunk; });
|
||||
controller.stderr.on('data', chunk => { stderr += chunk; });
|
||||
const completed = new Promise((resolve, reject) => {
|
||||
controller.once('error', reject);
|
||||
controller.once('close', (status, signal) => resolve({ status, signal, stdout, stderr }));
|
||||
});
|
||||
await check({ root, controller, completed });
|
||||
} finally {
|
||||
if (controller?.exitCode === null && controller.signalCode === null) controller.kill('SIGKILL');
|
||||
for (const name of ['native', 'typescript']) {
|
||||
for (const filename of [name + '.started', name + '.descendant']) {
|
||||
const file = path.join(root, filename);
|
||||
if (!existsSync(file)) continue;
|
||||
const data = readFileSync(file, 'utf8');
|
||||
const pid = filename.endsWith('.started') ? JSON.parse(data).pid : Number(data);
|
||||
if (!fixtureProcessGone(pid)) {
|
||||
try { process.kill(pid, 'SIGKILL'); } catch (error) { if (error.code !== 'ESRCH') throw error; }
|
||||
}
|
||||
}
|
||||
}
|
||||
rmSync(root, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
|
||||
test('native preparation overlaps independent outputs and joins both before recording', async () => {
|
||||
await withNativePreparationFixture('success', async ({ root, completed }) => {
|
||||
const result = await completed;
|
||||
assert.equal(result.status, 0, result.stderr);
|
||||
for (const name of ['native', 'typescript']) assert.equal(readFileSync(path.join(root, name + '.finished'), 'utf8'), 'joined');
|
||||
assert.equal(JSON.parse(readFileSync(path.join(root, 'native.started'), 'utf8')).cwd, path.join(root, 'packages/paperclip-runner'));
|
||||
assert.deepEqual(JSON.parse(readFileSync(path.join(root, 'native.started'), 'utf8')).args, ['build:binary']);
|
||||
assert.deepEqual(JSON.parse(readFileSync(path.join(root, 'typescript.started'), 'utf8')).args, ['--filter', '@paperclipai/paperclip-runner', 'build:typescript']);
|
||||
const results = result.stdout.split('\n').filter(line => line.startsWith('{')).map(line => JSON.parse(line));
|
||||
assert.deepEqual(results.map(item => [item.build, item.status, item.exitCode]), [['typescript', 'fulfilled', 0], ['native', 'fulfilled', 0]]);
|
||||
});
|
||||
});
|
||||
|
||||
for (const name of ['typescript', 'native']) {
|
||||
test(`${name} preparation failure fails the join and retires both owned process trees`, async () => {
|
||||
await withNativePreparationFixture(name, async ({ root, completed }) => {
|
||||
const result = await completed;
|
||||
assert.equal(result.status, 1, result.stderr);
|
||||
assert.match(result.stderr, new RegExp(name + ' prefetch failed \\(exit 23\\)'));
|
||||
const results = result.stdout.split('\n').filter(line => line.startsWith('{')).map(line => JSON.parse(line));
|
||||
assert.equal(results.length, 2);
|
||||
assert.ok(results.every(item => item.status === 'rejected'));
|
||||
for (const build of ['native', 'typescript']) {
|
||||
const pid = Number(readFileSync(path.join(root, build + '.descendant'), 'utf8'));
|
||||
await waitForFixture(() => fixtureProcessGone(pid), build + ' descendant outlived failed join');
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
test('canceling native preparation retires both supervisors and their descendants', { skip: process.platform === 'win32' }, async () => {
|
||||
await withNativePreparationFixture('cancel', async ({ root, controller, completed }) => {
|
||||
await waitForFixture(() => ['native', 'typescript'].every(name => existsSync(path.join(root, name + '.descendant'))), 'both native build trees must start');
|
||||
controller.kill('SIGTERM');
|
||||
const result = await completed;
|
||||
assert.equal(result.signal, 'SIGTERM');
|
||||
for (const name of ['native', 'typescript']) {
|
||||
const pid = Number(readFileSync(path.join(root, name + '.descendant'), 'utf8'));
|
||||
await waitForFixture(() => fixtureProcessGone(pid), name + ' descendant outlived cancellation');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
test("premerge native qualification consumes the same Linux-packed npm bytes without publishing", () => {
|
||||
const smoke = readWorkflow("release-smoke.yml");
|
||||
assert.match(smoke, /smoke_service:\n\s+if: inputs.qualification_source_sha == ''/);
|
||||
assert.match(smoke, /smoke:\n\s+if: inputs.qualification_source_sha == ''/);
|
||||
const qualification = smoke.split(" qualification_daemons:\n")[1];
|
||||
assert.doesNotMatch(qualification, /secrets\.|npm publish|id-token: write|contents: write|continue-on-error/);
|
||||
assert.match(qualification, /runner_binaries_only: true/);
|
||||
assert.match(qualification, /PAPERCLIP_RUNNER_PUBLIC_PACKS_DIR/);
|
||||
assert.match(qualification, /if: \$\{\{ !cancelled\(\) && steps.pack_identity.outputs.available == 'true' \}\}/);
|
||||
assert.match(qualification, /name: \$\{\{ inputs.artifact_name \}\}-public-packages/);
|
||||
assert.match(qualification, /--mac-consumer/);
|
||||
assert.match(qualification, /--linux-arm64-consumer/);
|
||||
assert.match(qualification, /fromJSON\(needs.qualification_daemons.outputs.native_consumer_matrix\)/);
|
||||
assert.match(qualification, /pnpm install --ignore-scripts --no-frozen-lockfile/);
|
||||
});
|
||||
|
||||
test("native ARM qualification prepares its scoped distro sandbox before the existing consumer", () => {
|
||||
const smoke = readWorkflow("release-smoke.yml");
|
||||
const native = smoke.split(" qualification_native:\n")[1];
|
||||
const preparation = native.split(" - name: Prepare Linux ARM sandbox prerequisites\n")[1].split("\n - ")[0];
|
||||
assert.match(preparation, /if: \$\{\{ matrix\.target == 'linux-arm64' \}\}/);
|
||||
assert.match(preparation, /timeout-minutes: 5/);
|
||||
assert.match(preparation, /bash \.qualification-tooling\/scripts\/prepare-linux-arm64-qualification\.sh/);
|
||||
const tooling = native.split(" - name: Checkout immutable ARM qualification tooling\n")[1].split("\n - ")[0];
|
||||
assert.match(tooling, /if: \$\{\{ matrix\.target == 'linux-arm64' \}\}/);
|
||||
assert.match(tooling, /repository: \$\{\{ job\.workflow_repository \}\}/);
|
||||
assert.match(tooling, /ref: \$\{\{ job\.workflow_sha \}\}/);
|
||||
assert.match(tooling, /sparse-checkout: scripts\/prepare-linux-arm64-qualification\.sh/);
|
||||
assert.match(tooling, /sparse-checkout-cone-mode: false/);
|
||||
assert.match(tooling, /persist-credentials: false/);
|
||||
assert.doesNotMatch(tooling + preparation, /github\.workflow_sha|github\.sha|ref: (?:master|main)|ref: \$\{\{ inputs\.qualification_source_sha/);
|
||||
assert.ok(native.indexOf("Checkout immutable ARM qualification tooling") < native.indexOf("Prepare Linux ARM sandbox prerequisites"));
|
||||
assert.ok(native.indexOf("Prepare Linux ARM sandbox prerequisites") < native.indexOf("Verify host identity and actual clean npm consumer"));
|
||||
assert.match(native, /path: \|\n\s+\$\{\{ runner\.temp \}\}\/native-public-install\.json\n\s+\$\{\{ runner\.temp \}\}\/paperclip-arm-bwrap-prerequisite\//);
|
||||
assert.doesNotMatch(smoke.split(" qualification_daemons:\n")[0], /prepare-linux-arm64-qualification/,
|
||||
"ordinary service and Docker smoke keep their existing setup");
|
||||
});
|
||||
|
||||
test("ARM qualification binds separate product and reusable-workflow revisions even when the product lacks the helper", { skip: process.platform === "win32" }, () => {
|
||||
const native = readWorkflow("release-smoke.yml").split(" qualification_native:\n")[1];
|
||||
const step = native.split(" - name: Prepare Linux ARM sandbox prerequisites\n")[1].split("\n - ")[0];
|
||||
const script = step.split(" run: |\n")[1].split("\n").map(line => line.startsWith(" ") ? line.slice(10) : line).join("\n");
|
||||
const root = mkdtempSync(path.join(tmpdir(), "arm-separate-tooling-"));
|
||||
try {
|
||||
const tools = path.join(root, "tools"); mkdirSync(tools);
|
||||
const tooling = path.join(root, ".qualification-tooling", "scripts"); mkdirSync(tooling, { recursive: true });
|
||||
const helper = 'printf "%s\\n" "$TOOLING_SHA" > "$FIXTURE_PREPARED"\n';
|
||||
writeFileSync(path.join(tooling, "prepare-linux-arm64-qualification.sh"), helper);
|
||||
writeFileSync(path.join(tools, "git"), '#!/bin/bash\nif [[ "$1" == -C ]]; then printf "%s\\n" "$FIXTURE_TOOLING_HEAD"; else printf "%s\\n" "$FIXTURE_PRODUCT_HEAD"; fi\n', { mode: 0o755 });
|
||||
const productSha = "a".repeat(40), toolingSha = "b".repeat(40);
|
||||
for (const [productHead, toolingHead, expectedStatus] of [[productSha, toolingSha, 0], ["c".repeat(40), toolingSha, 1], [productSha, "c".repeat(40), 1], [productSha, "invalid", 1]]) {
|
||||
const report = path.join(root, "report"); mkdirSync(report);
|
||||
const prepared = path.join(root, "prepared");
|
||||
const result = spawnSync("/bin/bash", ["-c", script], { cwd: root, encoding: "utf8", env: {
|
||||
PATH: `${tools}:${path.dirname(process.execPath)}:/usr/bin:/bin`, RUNNER_TEMP: report,
|
||||
SOURCE_SHA: productSha, TOOLING_SHA: toolingSha, TOOLING_REPOSITORY: "paperclipai/paperclip",
|
||||
TOOLING_WORKFLOW_REF: "paperclipai/paperclip/.github/workflows/release-smoke.yml@refs/heads/qualification",
|
||||
TOOLING_WORKFLOW_PATH: ".github/workflows/release-smoke.yml",
|
||||
FIXTURE_PRODUCT_HEAD: productHead, FIXTURE_TOOLING_HEAD: toolingHead, FIXTURE_PREPARED: prepared,
|
||||
// Reusable calls retain the caller context; it must not select tooling.
|
||||
GITHUB_REPOSITORY: "different/caller", GITHUB_SHA: "d".repeat(40),
|
||||
} });
|
||||
assert.equal(result.status, expectedStatus, result.stderr);
|
||||
assert.equal(existsSync(path.join(root, "scripts", "prepare-linux-arm64-qualification.sh")), false);
|
||||
assert.equal(existsSync(prepared), expectedStatus === 0);
|
||||
if (expectedStatus === 0) {
|
||||
assert.equal(readFileSync(prepared, "utf8").trim(), toolingSha);
|
||||
const identity = JSON.parse(readFileSync(path.join(report, "paperclip-arm-bwrap-prerequisite", "qualification-identity.json"), "utf8"));
|
||||
assert.equal(identity.productSourceSha, productSha);
|
||||
assert.equal(identity.toolingSourceSha, toolingSha);
|
||||
assert.equal(identity.toolingRepository, "paperclipai/paperclip");
|
||||
assert.equal(identity.toolingWorkflowPath, ".github/workflows/release-smoke.yml");
|
||||
assert.equal(identity.helperSha256, createHash("sha256").update(helper).digest("hex"));
|
||||
}
|
||||
rmSync(report, { recursive: true, force: true });
|
||||
rmSync(prepared, { force: true });
|
||||
}
|
||||
} finally { rmSync(root, { recursive: true, force: true }); }
|
||||
});
|
||||
|
||||
test("Linux ARM prerequisite setup rejects other hosts before invoking tools and preserves scoped security", { skip: process.platform === "win32" }, () => {
|
||||
const helper = path.join(repoRoot, "scripts/prepare-linux-arm64-qualification.sh");
|
||||
const source = readFileSync(helper, "utf8");
|
||||
const root = mkdtempSync(path.join(tmpdir(), "arm-prerequisite-guard-"));
|
||||
try {
|
||||
const tools = path.join(root, "tools"); mkdirSync(tools);
|
||||
const sentinel = path.join(root, "invoked");
|
||||
for (const command of ["dpkg", "uname", "mkdir", "mktemp", "sysctl", "sudo"]) {
|
||||
writeFileSync(path.join(tools, command), '#!/bin/bash\nprintf "%s\\n" "$0" >> "$PREREQUISITE_SENTINEL"\nexit 85\n', { mode: 0o755 });
|
||||
}
|
||||
for (const [actions, runnerOs] of [["", "Linux"], ["false", "Linux"], ["true", "macOS"], ["true", "Windows"]]) {
|
||||
const result = spawnSync("/bin/bash", [helper], { encoding: "utf8", env: {
|
||||
PATH: tools, GITHUB_ACTIONS: actions, RUNNER_OS: runnerOs, RUNNER_TEMP: root, PREREQUISITE_SENTINEL: sentinel,
|
||||
} });
|
||||
assert.equal(result.status, 1);
|
||||
assert.match(result.stderr, /requires a GitHub Actions Linux runner/);
|
||||
assert.equal(existsSync(sentinel), false, "host guard must precede every external tool");
|
||||
assert.equal(existsSync(path.join(root, "paperclip-arm-bwrap-prerequisite")), false);
|
||||
}
|
||||
assert.ok(source.indexOf('"${GITHUB_ACTIONS:-}" != true') < source.indexOf(". /etc/os-release"));
|
||||
assert.match(source, /-o Acquire::AllowInsecureRepositories=false/);
|
||||
assert.match(source, /download "\$package_name=\$package_version"/);
|
||||
assert.match(source, /printf '%s %s\\n' "\$package_sha256" "\$package_file" \| sha256sum -c -/);
|
||||
assert.match(source, /dpkg-deb --fsys-tarfile "\$package_file"/);
|
||||
assert.match(source, /tar -xO \.\/usr\/bin\/bwrap/);
|
||||
assert.match(source, /11d39094f044f0cda0febb3ad517b830301da6b2ce929664af09ee9e4dd264f9/);
|
||||
assert.ok(source.indexOf('test ! -L "$target"') < source.indexOf('sudo install -o root -g root -m 0644 "$stock_profile" "$target"'));
|
||||
assert.match(source, /timeout 15s sudo "\$parser" -r "\$target"/);
|
||||
for (const setting of ["userns", "apparmor", "clone"]) assert.ok(source.includes(`test "$after_${setting}" = "$before_${setting}"`));
|
||||
const aptCommands = source.replace(/\\\n\s*/g, " ").split("\n").filter(line => /\bapt-get\b/.test(line));
|
||||
assert.equal(aptCommands.length, 2, "only signed-index update and download may invoke APT");
|
||||
assert.match(aptCommands[0], /\bupdate\b/);
|
||||
assert.match(aptCommands[1], /\bdownload\b/);
|
||||
for (const command of aptCommands) assert.doesNotMatch(command, /\b(?:install|upgrade|remove|purge)\b/);
|
||||
assert.doesNotMatch(source, /apt-get[^\n]* install|\bdpkg(?:-deb)?\s+(?:-i|--install|--unpack)|sysctl\s+(?:-w|--system)|\baa-disable\b|\bsetcap\b|systemctl|--no-sandbox/);
|
||||
} finally { rmSync(root, { recursive: true, force: true }); }
|
||||
});
|
||||
|
||||
test("exact-source daemon matrices preserve historical three-target sources and add native ARM only for new sources", () => {
|
||||
const workflow = readWorkflow("release-verify.yml");
|
||||
const step = workflow.split(' - name: Preserve the immutable source\'s platform contract')[1].split('\n runner_binaries:')[0];
|
||||
const script = step.split(' run: |\n')[1].split('\n').map(line => line.startsWith(' ') ? line.slice(10) : line).join('\n');
|
||||
const root = mkdtempSync(path.join(tmpdir(), 'release-target-contract-'));
|
||||
try {
|
||||
const tools = path.join(root, 'tools'); mkdirSync(tools);
|
||||
writeFileSync(path.join(tools, 'git'), '#!/bin/sh\nprintf "%s\\n" "$FIXTURE_HEAD"\n', { mode: 0o755 });
|
||||
const source = path.join(root, 'packages/paperclip-runner/scripts/stage-release-runner-binaries.mjs');
|
||||
mkdirSync(path.dirname(source), { recursive: true });
|
||||
const output = path.join(root, 'outputs');
|
||||
const historical = ['darwin-arm64', 'darwin-x64', 'linux-x64'];
|
||||
for (const targets of [historical, [...historical, 'linux-arm64']]) {
|
||||
writeFileSync(source, `export const RELEASE_RUNNER_TARGETS = ${JSON.stringify(targets)};`);
|
||||
writeFileSync(output, '');
|
||||
const result = spawnSync('bash', ['-euo', 'pipefail', '-c', script], { cwd: root, encoding: 'utf8', env: {
|
||||
PATH: `${tools}:${path.dirname(process.execPath)}:/usr/bin:/bin`, SOURCE_SHA: 'a'.repeat(40), FIXTURE_HEAD: 'a'.repeat(40), GITHUB_OUTPUT: output,
|
||||
} });
|
||||
assert.equal(result.status, 0, result.stderr);
|
||||
const data = Object.fromEntries(readFileSync(output, 'utf8').trim().split('\n').map(line => {
|
||||
const index = line.indexOf('='); return [line.slice(0, index), JSON.parse(line.slice(index + 1))];
|
||||
}));
|
||||
assert.deepEqual(data.matrix.include.map(row => row.target), targets);
|
||||
assert.deepEqual(data.native_consumers.include.map(row => row.target), targets.filter(target => target !== 'linux-x64'));
|
||||
assert.ok(data.matrix.include.every(row => row.target !== 'linux-arm64' || row.host === 'ubuntu-24.04-arm'));
|
||||
assert.equal(data.matrix.include.find(row => row.target === 'linux-x64').host, targets.length === 4 ? 'ubuntu-24.04' : 'ubuntu-latest',
|
||||
'new GNU libc baseline stays pinned while historical qualification keeps its producer mode');
|
||||
}
|
||||
writeFileSync(source, 'export const RELEASE_RUNNER_TARGETS = ["linux-arm64"];'); writeFileSync(output, '');
|
||||
const result = spawnSync('bash', ['-euo', 'pipefail', '-c', script], { cwd: root, encoding: 'utf8', env: {
|
||||
PATH: `${tools}:${path.dirname(process.execPath)}:/usr/bin:/bin`, SOURCE_SHA: 'a'.repeat(40), FIXTURE_HEAD: 'a'.repeat(40), GITHUB_OUTPUT: output,
|
||||
} });
|
||||
assert.equal(result.status, 1); assert.match(result.stderr, /Unknown exact-source daemon platform contract/);
|
||||
assert.equal(readFileSync(output, 'utf8'), '');
|
||||
} finally { rmSync(root, { recursive: true, force: true }); }
|
||||
});
|
||||
|
||||
|
||||
test("historical promotion sources keep their packaging path while new sources require daemon assembly", () => {
|
||||
const workflow = readWorkflow("release.yml");
|
||||
const selector = workflow.split(' - name: Read the exact source packaging contract')[1].split('\n release_runner_binaries:')[0];
|
||||
const script = selector.split(' run: |\n')[1].split('\n').map(line => line.startsWith(' ') ? line.slice(10) : line).join('\n');
|
||||
const root = mkdtempSync(path.join(tmpdir(), 'release-source-contract-'));
|
||||
try {
|
||||
const tools = path.join(root, 'tools'); mkdirSync(tools);
|
||||
writeFileSync(path.join(tools, 'git'), '#!/bin/sh\ncase "$1" in\nrev-parse) printf "%s\\n" "$FIXTURE_HEAD";;\nshow) cat "$FIXTURE_SOURCE";;\n*) exit 3;;\nesac\n', { mode: 0o755 });
|
||||
const source = path.join(root, 'release.sh'), output = path.join(root, 'outputs');
|
||||
for (const [contents, required] of [['pnpm build\n', false], ['assemble-daemons "$PAPERCLIP_RELEASE_RUNNER_ARTIFACTS_DIR" "$SOURCE_SHA"\n', true]]) {
|
||||
writeFileSync(source, contents); writeFileSync(output, '');
|
||||
const result = spawnSync('bash', ['-euo', 'pipefail', '-c', script], { encoding: 'utf8', env: {
|
||||
PATH: `${tools}:/usr/bin:/bin`, SOURCE_SHA: 'a'.repeat(40), FIXTURE_HEAD: 'a'.repeat(40), FIXTURE_SOURCE: source, GITHUB_OUTPUT: output,
|
||||
} });
|
||||
assert.equal(result.status, 0, result.stderr);
|
||||
assert.match(readFileSync(output, 'utf8'), new RegExp(`required=${required}`));
|
||||
}
|
||||
const result = spawnSync('bash', ['-euo', 'pipefail', '-c', script], { encoding: 'utf8', env: {
|
||||
PATH: `${tools}:/usr/bin:/bin`, SOURCE_SHA: 'a'.repeat(40), FIXTURE_HEAD: 'b'.repeat(40), FIXTURE_SOURCE: source, GITHUB_OUTPUT: output,
|
||||
} });
|
||||
assert.equal(result.status, 1, 'Wrong checkout cannot opt out of mandatory assembly');
|
||||
const unreadable = spawnSync('bash', ['-euo', 'pipefail', '-c', script], { encoding: 'utf8', env: {
|
||||
PATH: `${tools}:/usr/bin:/bin`, SOURCE_SHA: 'a'.repeat(40), FIXTURE_HEAD: 'a'.repeat(40), FIXTURE_SOURCE: path.join(root, 'absent'), GITHUB_OUTPUT: output,
|
||||
} });
|
||||
assert.equal(unreadable.status, 1, 'A failed immutable source read must not masquerade as a legacy source');
|
||||
|
||||
} finally { rmSync(root, { recursive: true, force: true }); }
|
||||
for (const job of ['publish_canary','publish_nightly','publish_beta','preview_stable','publish_stable']) {
|
||||
const block = workflow.split(` ${job}:\n`)[1].split(/\n [a-z_]+:\n/)[0];
|
||||
assert.match(block, /needs.release_runner_source.result == 'success'/);
|
||||
assert.match(block, /needs.release_runner_source.outputs.required == 'false' \|\| needs.release_runner_binaries.result == 'success'/);
|
||||
assert.match(block, /Download exact-source release daemon receipts\n\s+if: needs.release_runner_source.outputs.required == 'true'/);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
|
||||
test("canary startup refreshes an incomplete dependency publication, then onboards once", async () => {
|
||||
|
||||
Reference in new issue
Block a user