diff --git a/scripts/__tests__/grok-public-install-sandbox.test.mjs b/scripts/__tests__/grok-public-install-sandbox.test.mjs index 5b731c17a3f..9fcf93ecbc1 100644 --- a/scripts/__tests__/grok-public-install-sandbox.test.mjs +++ b/scripts/__tests__/grok-public-install-sandbox.test.mjs @@ -7,13 +7,30 @@ 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, assertInstalledNativeConsumerContinuity, assertNoBundledCodexPayloads, grokConsumerDockerArgs, installedCodexProbeSource, installedDaemonProbeSource, macNodeHeadersDirectory, macPublicInstallLifecycleProfile, macInstalledStartupProbeSource } from '../grok-public-install-sandbox.mjs'; +import { GROK_PUBLIC_INSTALL_IMAGE, GROK_PUBLIC_INSTALL_LIFECYCLE, assertInstalledCodexPlatformProvenance, assertInstalledNativeConsumerContinuity, assertNoBundledCodexPayloads, grokConsumerDockerArgs, installedClaudeProbeSource, 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('installed Claude admission propagates failure without emitting a passing receipt', () => { + const root = mkdtempSync(join(tmpdir(), 'installed-claude-admission-')); + try { + const fixture = join(root, 'runner.mjs'), probe = join(root, 'probe.mjs'); + writeFileSync(probe, installedClaudeProbeSource(fixture, root)); + writeFileSync(fixture, "export async function probeAcpxClaudeInstallation(model) { if (model !== 'claude-opus-5-5') throw Error('wrong model'); }\n"); + const accepted = spawnSync(process.execPath, [probe], { encoding: 'utf8', timeout: 5000 }); + assert.equal(accepted.status, 0, accepted.stderr); + assert.equal(JSON.parse(accepted.stdout).packageIntegrityAndNoFollowLease, 'passed'); + assert.equal(JSON.parse(accepted.stdout).providerCalls, 0); + writeFileSync(fixture, "export async function probeAcpxClaudeInstallation() { throw Error('integrity rejected'); }\n"); + const rejected = spawnSync(process.execPath, [probe], { encoding: 'utf8', timeout: 5000 }); + assert.notEqual(rejected.status, 0); + assert.equal(rejected.stdout, ''); + } finally { rmSync(root, { recursive: true, force: true }); } +}); + 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 })); diff --git a/scripts/grok-public-install-sandbox.mjs b/scripts/grok-public-install-sandbox.mjs index 3e674c1017b..090aefd3802 100644 --- a/scripts/grok-public-install-sandbox.mjs +++ b/scripts/grok-public-install-sandbox.mjs @@ -87,6 +87,20 @@ export function assertInstalledCodexPlatformProvenance(lock, installed, target) // Execute this in the existing offline public consumer, never in the checkout. // The public index and returned command must both come from the installed graph. +export function installedClaudeProbeSource(indexPath, serverRoot) { + return ` + import { pathToFileURL } from 'node:url'; + // Match the production sidecar's published-server package authority. + process.env.PAPERCLIP_ACPX_PROVIDER_PACKAGE_ROOT = ${JSON.stringify(serverRoot)}; + process.env.PAPERCLIP_ACPX_PROVIDER_PACKAGE_MANIFEST = ${JSON.stringify(join(serverRoot, 'package.json'))}; + const { probeAcpxClaudeInstallation } = await import(pathToFileURL(${JSON.stringify(indexPath)}).href); + await probeAcpxClaudeInstallation('claude-opus-5-5'); + console.log(JSON.stringify({ schema: 'paperclip.claude-installed-admission.v1', + platform: process.platform, architecture: process.arch, model: 'claude-opus-5-5', + packageIntegrityAndNoFollowLease: 'passed', providerCalls: 0 })); + `; +} + export function installedCodexProbeSource(indexPath, consumerRoot, expectedVersion, { nativeCodex = false } = {}) { return ` import assert from 'node:assert/strict'; diff --git a/scripts/verify-grok-npm-install.mjs b/scripts/verify-grok-npm-install.mjs index 6bd8754baf4..e06edb898ba 100644 --- a/scripts/verify-grok-npm-install.mjs +++ b/scripts/verify-grok-npm-install.mjs @@ -13,7 +13,7 @@ import { basename, dirname, join, resolve } from 'node:path'; import { fileURLToPath } from 'node:url'; import { packConcurrency, packStagedPackages, packTimings } from './pack-staged-packages.mjs'; import { materializePublishManifest } from './prepare-bundled-package.mjs'; -import { GROK_PUBLIC_INSTALL_IMAGE, GROK_PUBLIC_INSTALL_LIFECYCLE, assertInstalledNativeConsumerContinuity, assertNoBundledCodexPayloads, grokConsumerDockerArgs, installedCodexProbeSource, installedDaemonProbeSource, macNodeHeadersDirectory, macPublicInstallLifecycleProfile, macInstalledStartupProbeSource } from './grok-public-install-sandbox.mjs'; +import { GROK_PUBLIC_INSTALL_IMAGE, GROK_PUBLIC_INSTALL_LIFECYCLE, assertInstalledNativeConsumerContinuity, assertNoBundledCodexPayloads, grokConsumerDockerArgs, installedClaudeProbeSource, installedCodexProbeSource, installedDaemonProbeSource, macNodeHeadersDirectory, macPublicInstallLifecycleProfile, macInstalledStartupProbeSource } from './grok-public-install-sandbox.mjs'; import { startPrefetch, stopPrefetches } from './prefetch-child.mjs'; import { retainRunnerQualificationPackages } from './retain-runner-qualification-packages.mjs'; import { bundledInstallTimings, startBundledInstalls } from './stage-bundled-packages.mjs'; @@ -84,6 +84,7 @@ async function verifyMacConsumer(packageDirectory, sourceRevision, receiptPath) const probe = (name, source) => { const file = join(root, name); writeFileSync(file, source); return JSON.parse(run(process.execPath, [file]).trim()); }; const daemon = probe('daemon-probe.mjs', installedDaemonProbeSource(installed, consumer, sourceRevision)); const codex = probe('codex-probe.mjs', installedCodexProbeSource(join(installed, 'index.js'), consumer, undefined, { nativeCodex: true })); + const claude = probe('claude-probe.mjs', installedClaudeProbeSource(join(installed, 'index.js'), server)); const startupScript = macInstalledStartupProbeSource(consumer, server, daemon.sha256, join(repo, 'tests/runner-e2e/installed-cli.ts')); const startupFile = join(root, 'startup-probe.mts'); writeFileSync(startupFile, startupScript); const startup = JSON.parse(run(process.execPath, ['--import', 'tsx', startupFile], join(repo, 'server')).trim()); @@ -91,7 +92,7 @@ async function verifyMacConsumer(packageDirectory, sourceRevision, receiptPath) cleanNpmInstall: true, packageCount: manifest.archives.length, codexPackedPayloadsOmitted: true, consumerLockPreserved: true, lifecycleSentinelVerified: true, lifecycleNodeHeadersVerified: true, lifecycleNodeVersion: process.versions.node, lifecycleNetwork: 'sandbox-exec denied (loopback negative control)', - ...daemon, ...codex, startup, providerCalls: 0 }; + ...daemon, ...codex, claude, startup, providerCalls: 0 }; writeFileSync(receiptPath, JSON.stringify(receipt, null, 2) + '\n'); console.log(JSON.stringify(receipt)); } finally { rmSync(root, { recursive: true, force: true }); } @@ -165,6 +166,7 @@ async function verifyLinuxArmConsumer(packageDirectory, sourceRevision, receiptP const hostServer = join(consumer, 'node_modules/@paperclipai/server'), hostInstalled = join(hostServer, 'dist/vendor/paperclip-runner'); const hostDaemon = () => hostProbe('host-daemon-probe.mjs', installedDaemonProbeSource(hostInstalled, consumer, sourceRevision)); const hostCodex = () => hostProbe('host-codex-probe.mjs', installedCodexProbeSource(join(hostInstalled, 'index.js'), consumer, undefined, { nativeCodex: true })); + const claude = hostProbe('host-claude-probe.mjs', installedClaudeProbeSource(join(hostInstalled, 'index.js'), hostServer)); const digest = value => createHash('sha256').update(value).digest('hex'); const beforeCodex = hostCodex(); const before = { lockSha256: digest(lock), runnerSha256: hostDaemon().sha256.slice(7), codexCommandSha256: beforeCodex.codexCommandSha256, codexNativeExecutableSha256: beforeCodex.codexNativeExecutableSha256 }; @@ -186,7 +188,7 @@ async function verifyLinuxArmConsumer(packageDirectory, sourceRevision, receiptP startupEnvironment: 'native host, same installed graph, isolated empty homes', startupHost: { nodeVersion: process.versions.node, nodeModulesAbi: process.versions.modules, glibcVersionRuntime: hostHeader.glibcVersionRuntime, kernelRelease: hostHeader.osRelease }, installedIdentityBefore: before, installedIdentityAfter: after, - ...daemon, ...codex, startup, providerCalls: 0 }; + ...daemon, ...codex, claude, startup, providerCalls: 0 }; writeFileSync(receiptPath, JSON.stringify(receipt, null, 2) + '\n'); console.log(JSON.stringify(receipt)); } finally { rmSync(root, { recursive: true, force: true }); }