diff --git a/.github/workflows/docker-runner-check.yml b/.github/workflows/docker-runner-check.yml index c701174598..a90335fcb5 100644 --- a/.github/workflows/docker-runner-check.yml +++ b/.github/workflows/docker-runner-check.yml @@ -68,8 +68,9 @@ on: permissions: {} concurrency: - # Publishing a newer image must not discard an earlier verification's evidence. - group: docker-runner-check-${{ github.event.pull_request.number || github.ref }}-${{ inputs.diagnose_pi_startup && 'pi-startup-diagnostic' || inputs.verify_pi_intel && 'pi-native-intel' || inputs.diagnose_ajv_pack && 'ajv-diagnostic' || inputs.verify_runner && 'runner-verification' || inputs.source_e2e_support_only && 'source-e2e-support' || inputs.source_root_tests_only && 'source-root-tests' || inputs.verify_source && 'verification' || inputs.verify_public_install && 'public-install' || inputs.build_eval_viewer && 'viewer' || 'image' }} + # Different immutable manual sources retain independent evidence; the same + # source/mode still supersedes itself. Ordinary PR grouping is unchanged. + group: docker-runner-check-${{ github.event.pull_request.number || github.ref }}-${{ inputs.diagnose_pi_startup && 'pi-startup-diagnostic' || inputs.verify_pi_intel && 'pi-native-intel' || inputs.diagnose_ajv_pack && 'ajv-diagnostic' || inputs.verify_runner && 'runner-verification' || inputs.source_e2e_support_only && 'source-e2e-support' || inputs.source_root_tests_only && 'source-root-tests' || inputs.verify_source && 'verification' || inputs.verify_public_install && 'public-install' || inputs.build_eval_viewer && 'viewer' || 'image' }}${{ github.event_name == 'workflow_dispatch' && inputs.expected_source_sha && format('-{0}', inputs.expected_source_sha) || '' }} cancel-in-progress: true jobs: @@ -176,7 +177,7 @@ jobs: test "$SOURCE_ROOT_TESTS_ONLY" != true test "$DIAGNOSE_AJV" != true test "$IMAGE_MODE" != true - test "$EXPECTED_SOURCE_SHA" = efe019a79f50440d7bd6c3bc6c75fb8f18953093 + test "$EXPECTED_SOURCE_SHA" = b9e5d6ecdb05ab7244c90976e07c950f8d09b15b test "$EXPECTED_LOCK_SHA256" = 38338a6867358440c5ab5993eaeb85fb501bc7df65d24acf52fbd63c880ee4ba # Standard hosted runner minutes are free for this public repository. # Artifact storage and unrelated resource costs are not inferred here. @@ -230,24 +231,13 @@ jobs: with: version: 9.15.4 package_json_file: candidate/package.json - - name: Download exact historical native daemon evidence - timeout-minutes: 5 - env: - GH_TOKEN: ${{ github.token }} - run: | - set -euo pipefail - gh api repos/paperclipai/paperclip/actions/artifacts/11197947432/zip \ - > "$RUNNER_TEMP/pi-native-daemon-36933598154.zip" - printf '%s %s\n' 2f9892bfa65318e3a61fd39f09fbe26374098eff6cfc3126fa1cba8df31b85c1 \ - "$RUNNER_TEMP/pi-native-daemon-36933598154.zip" | shasum -a 256 --check - name: Build frozen pack and run unchanged no-provider contracts once timeout-minutes: 55 run: | set -euo pipefail python3 -B trusted-ci/scripts/ci/pi-intel/verify.py \ --source "$GITHUB_WORKSPACE/candidate" \ - --output "$GITHUB_WORKSPACE/pi-intel-evidence" \ - --daemon-archive "$RUNNER_TEMP/pi-native-daemon-36933598154.zip" + --output "$GITHUB_WORKSPACE/pi-intel-evidence" - name: Admit complete evidence within the reserved storage bound if: always() id: pi_intel_evidence diff --git a/scripts/ci/pi-intel/profile-inputs.json b/scripts/ci/pi-intel/profile-inputs.json index d85ab7cf18..816e01e279 100644 --- a/scripts/ci/pi-intel/profile-inputs.json +++ b/scripts/ci/pi-intel/profile-inputs.json @@ -13,10 +13,10 @@ "digest": "sha256:48cecd8dc77a5533240fcf2f29d19be05380da4a79f8e5061480f94241db75a8" }, "pi": { - "path": "packages/paperclip-runner/test-fixtures/pi-acp/profile-v11-identity.json", - "sha256": "7b253e94b1672dff0e7a4f4a50f3bb2270038d14f1ef27857f8bef67b9800a7f", - "version": 11, - "digest": "sha256:5e276f48c8a87b3e6165369faac62d3925282c84b98934575b1b7b97ad50b309" + "path": "packages/paperclip-runner/test-fixtures/pi-acp/profile-v12-identity.json", + "sha256": "99a6dd48b3da1c555cfbec7f42fe0cb1c00afe437b65a78db92f6b14d7828168", + "version": 12, + "digest": "sha256:47306e6d2a9b59e8f9189f725ebb7a0a7f91826044d1739e1a35ab31f228ba1f" } }, "candidateClosureDigests": { @@ -31,26 +31,30 @@ "linux-x64": "sha256:a3d8f4367cfa1694d79e3f8b7930b6fbfe42a229c272b375b11951d631db8d07" }, "pi": { - "darwin-arm64": "sha256:a3e43c23f9603983a59fd256ca1dcf8bbd5cc2728dad095f7bcd3b3e62278597", - "darwin-x64": "sha256:8c6e7802d47da9410af56e367f6cf7af572fcbf3e361627cee026e8cffc9bdd3", - "linux-x64": "sha256:40e186c53db60d1ab5673ba34f33002e93cea518725a93395d241393b984caef" + "darwin-arm64": "sha256:e17be4d27c589b8f8b5de7d00686c39873fe6f902a3f133bdaf1a9f94dee00a2", + "darwin-x64": "sha256:03351f4a250a8db0e79411a9079b43a0ff05f72a2aff41fae17f1fc2de24bd41", + "linux-x64": "sha256:29dfc829700c57392f1d56373078dcfc80674cbd50f65674ec64fced81dffb68" } }, "additionalSourceFiles": { "packages/paperclip-runner/test/fixtures/copilot-message-identity-distributions-1.0.88.json": "7e1534d668a3db7acae15377b67d86fa80cc21ce699d021709b0b52ad314cfb2", - "patches/pi-acp@0.0.33.patch": "429184a462f9acef5bef890f3537db3ef385ee7c06d9fe0c335158590005ab0a", + "patches/pi-acp@0.0.33.patch": "93254de82ae779587611748cfb963475c3439e033f1582f06dfdcc7f7b937546", "packages/paperclip-runner/scripts/pi-distribution/package.json": "c14f4050ef27dca91d3650e7446bc9927296a926224dba50a0e02f5d9fd4d95b", "packages/paperclip-runner/scripts/pi-distribution/package-lock.json": "f4b04d51c4d91a6529dc97af2f3f84c28a220d93a54b5555826c4d9fd30f0939", - "packages/paperclip-runner/scripts/materialize-pi-distribution.mjs": "39bd8cc48791f8e54a953c7803334b965b9998943b0402008438d565ea2230be", + "packages/paperclip-runner/scripts/materialize-pi-distribution.mjs": "315e1123abdad82006caca1823e15ba2da0cb1cfe7f0f29534c3830e8bf0682b", "packages/paperclip-runner/scripts/build-node-startup-timeout.mjs": "545dbaf7062b1cd54176dafe3feb8877e5ccf19406f791172a3ad47dbfd73fc4", - "packages/paperclip-runner/src/drivers/acpx/pi-acp-runtime.ts": "a0ec66a03cd9fa32e9aaafc4ac6840115883333000fd079ccba230706b763ac0", + "packages/paperclip-runner/src/drivers/acpx/pi-acp-runtime.ts": "95f74eb44a2e498fbd9461d363fac5b4d5fdae05b9666dca1ac4a632229dfef4", "packages/paperclip-runner/src/drivers/acpx/pi-runtime-extension.ts": "f3446be10d984c368c52202e768aefe9b4a25968e09922e98e3f016c9dc0198e", "packages/paperclip-runner/src/drivers/acpx/pi-verified-runtime.ts": "dfb5f6604da0abc307c4bb514e3dd47354f4b867be28fdeb64273c3c055b68c1", "packages/paperclip-runner/src/drivers/acpx/pi-node-pins.ts": "ffc822aff64db315e461cbe44f03d704695de4a4296f643af79386c3556a1d46", - "packages/paperclip-runner/src/drivers/acpx/pi-closure-pins.ts": "0941f56ea13b8eb309a345d2a1513e0ec53d201c81c755b356565505bc1df905", - "packages/paperclip-runner/src/drivers/acpx/qualified-profiles.ts": "20185223c77ce6d5b67b270abdc0debc26166c7d3dd412a1ecea83f980429027", - "packages/paperclip-runner/package.json": "a87426b0184e114e46c6f4ddbf3f4a9727cd591cf0a5ca900efde42198190fd1", - "packages/paperclip-runner/src/drivers/acpx/pi-installation.ts": "d32de12609133c16bb78c559b98ac878fa792a1f336159d09f2c1948f887125e" + "packages/paperclip-runner/src/drivers/acpx/pi-closure-pins.ts": "9fdfcd7d7b0bdcbb75fe7d8f7e9228642f32d83bead9813d494b3ee133c2fc5e", + "packages/paperclip-runner/src/drivers/acpx/qualified-profiles.ts": "9760d924815554cb0f438c444e1f23a3608fca4a245344b25d15199b743515e5", + "packages/paperclip-runner/package.json": "b5be67eb3190ddaa9baf20a455fae62fc7722b764f89f2cdbdd02a6d6a9dcad3", + "packages/paperclip-runner/src/drivers/acpx/pi-installation.ts": "a9c4e9c3f003eebd38b35f89a112291ad0620aed57894c878ff6806593911fd7", + "packages/paperclip-runner/src/drivers/acpx/native-distribution-integrity.ts": "943ba0fb2d27d08b9b0d36778f08c7c0dbdb78893c7564babd4e67f67cf065fe", + "packages/paperclip-runner/runner/crates/runner-core/src/acpx_provider_backend.rs": "3c90ed419b96a3484756954f3c2a8af8c4c8fc4ec5aa2a7725b9df36167dcd72", + "packages/paperclip-runner/rust-toolchain.toml": "e4c6d27e48d71df0ae66d84f3bc2d5df8cbf5c3cbb3225475a7852d94361a0c5", + "packages/paperclip-runner/runner/crates/runner-core/src/bin/paperclip-runnerd.rs": "116d2641e844ba28cd7496d56b44fa405ea8def8940d9e50e9d6b30603602581" }, "permissionPolicySources": { "packages/paperclip-runner/src/drivers/acpx/copilot-permission-context.ts": { @@ -174,8 +178,8 @@ } }, "localComparison": { - "sourceRevision": "efe019a79f50440d7bd6c3bc6c75fb8f18953093", - "sourceArchiveSha256": "587c0f60a30f0ca4b43361830cb611fcaa4a83f492cb7a9fd66b7908b45f72be", + "sourceRevision": "b9e5d6ecdb05ab7244c90976e07c950f8d09b15b", + "sourceArchiveSha256": "b32d8e32639e6527804fe11bfa1aaefcefa87fca31e9155499e6b9a28f69dbd4", "trackedLockSha256": "e11d69fa8702a906c293c1cb307c71df90d3b1f1617b3c23ebf17fa9a87fec79", "resolvedLockSha256": "38338a6867358440c5ab5993eaeb85fb501bc7df65d24acf52fbd63c880ee4ba", "nodeSha256": "7abcf39bd37ab251015337ff75304d7555f0d8e88c6e0fbf04bce8ce34636f49", @@ -183,7 +187,7 @@ "piRuntimeVersion": "1.0.0", "piServerVersion": "0.0.33", "acpxVersion": "0.13.1", - "note": "Fresh W TypeScript/sidecar/provider pack; exact retained native Intel daemon from5eba after complete declared-input equality. Historical local pack and S/V outputs are not current outputs. Original Apple SDK/linker details were not retained; no equivalence or fresh-compilation claim.", + "note": "Fresh exact-source TypeScript, sidecar, native Intel Rust daemon and Pi12 provider pack. Historical local outputs are not current outputs; no old daemon import or native input-equivalence claim.", "historicalLocalOutputs": { "localPackDigest": "sha256:a7ae16b680c6fcaa9438faa46562ba31519bb0fb42e21b0a4b00e8936c462d89", "localDaemonSha256": "c0e9acdd461d5efe4a10f9fd751e8a67beceede4d43593951d7079edad657044", @@ -191,138 +195,33 @@ "sourceRevision": "5eba61ece929dcfb2b0c1761825a2d9e557c2554" } }, - "nativeDaemonReuse": { - "schema": "paperclip.native-daemon-reuse/v1", - "originalBuildSource": "5eba61ece929dcfb2b0c1761825a2d9e557c2554", - "nativeInputEquivalentTo": "efe019a79f50440d7bd6c3bc6c75fb8f18953093", - "artifactRunId": "36933598154", - "artifactId": "11197947432", - "artifactName": "pi-native-intel-36933598154", - "artifactWorkflowRevision": "b58907578ed590f11b049f794f01ce0368df2676", - "artifactZipSha256": "2f9892bfa65318e3a61fd39f09fbe26374098eff6cfc3126fa1cba8df31b85c1", - "artifactZipBytes": 1076265811, - "sourceArchiveSha256": "f751f86b19701757d30501e39d47fe7d20a102e32f9e4c4df585074393e85237", - "selectedFiles": { - "source.tar": { - "sha256": "f751f86b19701757d30501e39d47fe7d20a102e32f9e4c4df585074393e85237", - "bytes": 141516800 - }, - "source-input-inventory.json": { - "sha256": "f95181f36e02e84b309f3c7b21831bc9e6480d4431c0b57a31aa5764ec966695", - "bytes": 1032076 - }, - "receipt.json": { - "sha256": "900fdd6a89a190bd93501f650749c72c7417ea392febd0d0cb4645eae8381736", - "bytes": 26160 - }, - "paperclip-runnerd": { - "sha256": "a0c7ace7ad0be9ac5b5f9c58749d9c452cd3a1ef143a38d5120d8e4f2bf82d6a", - "bytes": 32035568 - }, - "daemon-build.log": { - "sha256": "eea04dc2986eb3f451a0b2eeb76942eab6affb8643a6cc90eace5274a1cd5bb2", - "bytes": 18358 - }, - "daemon-metadata.log": { - "sha256": "a1b957a6ee5fad916252ba629b3c2ad85af8b54c1f006d5901f1614eb700ebfe", - "bytes": 452 - }, - "daemon-sign.log": { - "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", - "bytes": 0 - }, - "rust-version-verbose.log": { - "sha256": "87c09aab5ad89ed5a2636cd4f577eb178a3afba7a9f71490b240845245725f04", - "bytes": 191 - } - }, - "sourceDeltaAllowed": [ - "doc/evals.md", - "packages/paperclip-runner/scripts/materialize-pi-distribution.test.mjs", - "packages/paperclip-runner/src/drivers/acpx/native-distribution-integrity.test.ts", - "packages/paperclip-runner/src/drivers/acpx/native-distribution-integrity.ts", - "packages/paperclip-runner/src/drivers/acpx/pi-installation.ts", - "packages/paperclip-runner/src/drivers/acpx/pi-native-admission.test.ts", - "packages/paperclip-runner/src/drivers/acpx/pi-verified-runtime.test.ts", - "packages/paperclip-runner/src/drivers/acpx/pi-verified-runtime.ts", - "tests/runner-e2e/FIXTURES.md", - "tests/runner-e2e/README.md", - "tests/runner-e2e/catalog.test.ts", - "tests/runner-e2e/catalog.ts", - "tests/runner-e2e/failure-classifier.ts", - "tests/runner-e2e/pi-controls-evidence.test.ts", - "tests/runner-e2e/pi-file-evidence.test.ts", - "tests/runner-e2e/pi-file-evidence.ts", - "tests/runner-e2e/pi-native-cases.test.ts", - "tests/runner-e2e/pi-native-cases.ts", - "tests/runner-e2e/pi-native-flow.ts", - "tests/runner-e2e/pi-native-provider-death-flow.test.ts", - "tests/runner-e2e/pi-native-provider-death-flow.ts", - "tests/runner-e2e/pi-native-restart-flow.test.ts", - "tests/runner-e2e/pi-native-restart-flow.ts", - "tests/runner-e2e/pi-provider-fault.py", - "tests/runner-e2e/pi-provider-fault.test.py", - "tests/runner-e2e/pi-provider-fault.test.ts", - "tests/runner-e2e/remote-native-fixtures.test.ts", - "tests/runner-e2e/remote-native-fixtures.ts", - "tests/runner-e2e/runner.spec.ts", - "tests/runner-e2e/server-stop.test.ts" + "expectedDaemonBuildMetadata": { + "binaryContractVersion": 2, + "binaryName": "paperclip-runnerd", + "durableSessionCapabilities": [ + "unlimited_runtime", + "connection_lease_renewal" ], - "compiler": { - "rustcSha256": "a1fd557be213adbcfe224e0e05e2903aa3f4b8d9294bed51d97a9d2a0b201e55", - "cargoSha256": "07816976dbe29da10bf127b75edcb0f5cd8b8652051b9c79709f8c0898d125d2", - "versionVerbose": "rustc 1.97.1 (8bab26f4f 2026-07-14)\nbinary: rustc\ncommit-hash: 8bab26f4f68e0e26f0bb7960be334d5b520ea452\ncommit-date: 2026-07-14\nhost: x86_64-apple-darwin\nrelease: 1.97.1\nLLVM version: 22.1.6\n", - "jobs": 2 + "harnessDriverVersion": 1, + "nativeExecutionVersion": 1, + "packageName": "@paperclipai/paperclip-runner", + "packageVersion": "0.0.0", + "prp": { + "maximumVersion": 2, + "minimumVersion": 1, + "name": "paperclip.runner" }, - "daemonBuildMetadata": { - "binaryContractVersion": 2, - "binaryName": "paperclip-runnerd", - "durableSessionCapabilities": [ - "unlimited_runtime", - "connection_lease_renewal" - ], - "harnessDriverVersion": 1, - "nativeExecutionVersion": 1, - "packageName": "@paperclipai/paperclip-runner", - "packageVersion": "0.0.0", - "prp": { - "maximumVersion": 2, - "minimumVersion": 1, - "name": "paperclip.runner" - }, - "prpTransportModes": [ - "dial_ws_loopback", - "dial_wss", - "listen_ws" - ], - "schema": "paperclip-runner/runnerd-build-metadata/v1" - }, - "buildCommandTail": [ - "build", - "--release", - "--target", - "x86_64-apple-darwin", - "--manifest-path", - "packages/paperclip-runner/runner/Cargo.toml", - "--locked", - "-p", - "paperclip-runner-core", - "--bin", - "paperclip-runnerd", - "-j", - "2" + "prpTransportModes": [ + "dial_ws_loopback", + "dial_wss", + "listen_ws" ], - "originalVerifierSha256": "7de772df5beabe6413e2d0d8dd287017ef2be8d3b3cdc76e8a23007ba547b509", - "missingOriginalToolchainEvidence": [ - "Apple SDK version", - "Apple linker/compiler identity", - "full Rust compiler sysroot inventory" - ], - "claims": { - "freshNativeCompilation": false, - "currentAppleToolchainEquivalent": false, - "completeDeclaredNativeSourceConfigProtocolEquality": true, - "originalCompilerProvenanceRetained": true - } + "schema": "paperclip-runner/runnerd-build-metadata/v1" + }, + "freshNativeBuild": { + "toolchain": "1.97.1", + "target": "x86_64-apple-darwin", + "jobs": 2, + "deadlineSeconds": 1500 } } diff --git a/scripts/ci/pi-intel/test_ci_helpers.py b/scripts/ci/pi-intel/test_ci_helpers.py index 677f7dbfe7..6d276f461c 100644 --- a/scripts/ci/pi-intel/test_ci_helpers.py +++ b/scripts/ci/pi-intel/test_ci_helpers.py @@ -6,13 +6,75 @@ import subprocess import tarfile import tempfile import unittest +import json +import re from unittest.mock import Mock, patch from lifecycle import CleanupUncertain, Lifecycle, run_test_cases from retain_pack import retain_pack, verify_archive from closed_inventory import closed_tree from source_guard import tree, sha, capture_authority, verify_authority, verify_source, ASSETS from admit_evidence import admit_evidence, regular_bytes -from verify import tool_invocation, verify_tool_invocation +from verify import tool_invocation, verify_tool_invocation, build_native_daemon, INPUTS, SOURCE + +class FreshNativeBuildRegression(unittest.TestCase): + """Mock command boundaries only: no compiler, platform tool or child launch.""" + def setUp(self): + self.temp=tempfile.TemporaryDirectory();self.addCleanup(self.temp.cleanup) + self.root=Path(self.temp.name).resolve();self.stage=self.root/'stage';self.stage.mkdir() + self.scratch=self.root/'scratch';self.scratch.mkdir();self.sdk=self.root/'sdk';self.sdk.mkdir() + (self.sdk/'SDKSettings.json').write_text('{"Version":"fixture"}') + self.tools={} + for name in ('rustup','cargo','rustc','clang','ld'): + path=self.root/name;path.write_text('fixture-only '+name);path.chmod(0o700);self.tools[name]=path + self.audit={'invocationPath':str(self.tools['rustup']),'resolvedTarget':str(self.tools['rustup']),'targetSha256':sha(self.tools['rustup'])} + self.env={'CARGO_TARGET_DIR':str(self.scratch/'target')};self.receipt={};self.calls=[];self.fail_label=None;self.bad_metadata=False + def run_command(self,command,label,timeout): + self.calls.append((label,list(map(str,command)),timeout)) + if label==self.fail_label:raise RuntimeError('injected command failure') + values={'cargo-path':str(self.tools['cargo']),'rustc-path':str(self.tools['rustc']), + 'rust-version-verbose':'rustc 1.97.1 fixture\nhost: x86_64-apple-darwin', 'cargo-version':'cargo 1.97.1 fixture', + 'sdk-path':str(self.sdk),'sdk-version':'fixture','clang-path':str(self.tools['clang']),'ld-path':str(self.tools['ld']), + 'daemon-architecture':'Mach-O 64-bit executable x86_64', + 'daemon-metadata':json.dumps({} if self.bad_metadata else INPUTS['expectedDaemonBuildMetadata'])} + if label=='daemon-build': + p=self.scratch/'target/x86_64-apple-darwin/release/paperclip-runnerd';p.parent.mkdir(parents=True);p.write_bytes(b'newly-compiled-fixture');p.chmod(0o755) + return values.get(label,'') + def build(self): + with patch('verify.tool_invocation',return_value=(self.tools['rustup'],self.audit)): + return build_native_daemon(self.stage,self.scratch,self.env,self.run_command,self.receipt,lambda:None) + def test_fresh_compiler_recipe_and_output_provenance(self): + daemon=self.build();self.assertEqual(daemon.read_bytes(),b'newly-compiled-fixture') + self.assertTrue(self.receipt['nativeRecompiled']);self.assertFalse(self.receipt['nativeDaemonReuse']);self.assertEqual(self.receipt['nativeBuildSource'],SOURCE) + build=next(c for c in self.calls if c[0]=='daemon-build') + self.assertEqual(build[2],1500);self.assertEqual(build[1][1:],['build','--release','--target','x86_64-apple-darwin','--manifest-path','packages/paperclip-runner/runner/Cargo.toml','--locked','-p','paperclip-runner-core','--bin','paperclip-runnerd','-j','2']) + self.assertEqual(self.env['RUSTC'],str(self.tools['rustc'])) + self.assertEqual(self.receipt['compiler']['sdk']['settings']['SDKSettings.json'],sha(self.sdk/'SDKSettings.json')) + labels=[c[0] for c in self.calls];self.assertLess(labels.index('daemon-sign'),labels.index('daemon-signature-verify'));self.assertLess(labels.index('daemon-signature-verify'),labels.index('daemon-metadata')) + def test_compiler_failure_cannot_produce_or_claim_fresh_daemon(self): + self.fail_label='daemon-build' + with self.assertRaisesRegex(RuntimeError,'injected'):self.build() + self.assertNotIn('nativeRecompiled',self.receipt);self.assertFalse((self.stage/'packages/paperclip-runner/dist/bin/paperclip-runnerd').exists()) + self.assertNotIn('daemon-sign',[c[0] for c in self.calls]) + def test_wrong_metadata_cannot_claim_fresh_success(self): + self.bad_metadata=True + with self.assertRaisesRegex(RuntimeError,'build metadata'):self.build() + self.assertNotIn('nativeRecompiled',self.receipt) + def test_missing_sdk_evidence_stops_before_build(self): + (self.sdk/'SDKSettings.json').unlink() + with self.assertRaisesRegex(RuntimeError,'SDK settings'):self.build() + self.assertNotIn('daemon-build',[c[0] for c in self.calls]) + def test_unreviewed_recipe_stops_before_commands(self): + with patch.dict(INPUTS,{'freshNativeBuild':dict(INPUTS['freshNativeBuild'],jobs=8)}): + with self.assertRaisesRegex(RuntimeError,'recipe'):self.build() + self.assertEqual(self.calls,[]) + + def test_pack_verifier_and_source_profile_authorities_agree(self): + verifier=(Path(__file__).parent/'verify-pack.mjs').read_text() + block=re.search(r'const pins = \{([^}]+)\};',verifier) + self.assertIsNotNone(block) + pins=dict(re.findall(r"(\w+): '([a-f0-9]{64})'",block.group(1))) + self.assertEqual({agent:'sha256:'+digest for agent,digest in pins.items()}, + {agent:profile['digest'] for agent,profile in INPUTS['profiles'].items()}) class CleanupRegression(unittest.TestCase): def test_first_cleanup_failure_prevents_second_test_and_scratch_removal(self): diff --git a/scripts/ci/pi-intel/verify-pack.mjs b/scripts/ci/pi-intel/verify-pack.mjs index e7ddb57838..4dfa3bb30d 100644 --- a/scripts/ci/pi-intel/verify-pack.mjs +++ b/scripts/ci/pi-intel/verify-pack.mjs @@ -47,7 +47,7 @@ export function verifyPack(pack, source, platform, architecture) { 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 pins = { cursor: '1df2a15b93bc3a14fa47fa3315344ba023fe2412048047cdc6f32096a6336564', copilot: '48cecd8dc77a5533240fcf2f29d19be05380da4a79f8e5061480f94241db75a8', pi: '5e276f48c8a87b3e6165369faac62d3925282c84b98934575b1b7b97ad50b309' }; + const pins = { cursor: '1df2a15b93bc3a14fa47fa3315344ba023fe2412048047cdc6f32096a6336564', copilot: '48cecd8dc77a5533240fcf2f29d19be05380da4a79f8e5061480f94241db75a8', pi: '47306e6d2a9b59e8f9189f725ebb7a0a7f91826044d1739e1a35ab31f228ba1f' }; requireTrue(Object.keys(p.candidateProviders).sort().join(',') === Object.keys(pins).sort().join(','), 'Wrong candidate set'); const closurePins = JSON.parse(readFileSync(new URL('./profile-inputs.json', import.meta.url))).candidateClosureDigests; const checks = []; diff --git a/scripts/ci/pi-intel/verify.py b/scripts/ci/pi-intel/verify.py index 175f33a23a..19a2dd96d9 100644 --- a/scripts/ci/pi-intel/verify.py +++ b/scripts/ci/pi-intel/verify.py @@ -7,7 +7,6 @@ from owned_processes import OwnedProcesses,atomic_json import source_guard from lifecycle import Lifecycle, run_test_cases from retain_pack import retain_pack -from native_daemon_reuse import import_daemon HERE=Path(__file__).resolve().parent INPUTS=json.loads((HERE/'profile-inputs.json').read_text()) @@ -29,6 +28,51 @@ def verify_tool_invocation(invocation, audit): require(str(invocation)==audit['invocationPath'] and str(invocation.resolve(strict=True))==audit['resolvedTarget'], 'Executable invocation or target changed') require(sha(invocation.resolve(strict=True))==audit['targetSha256'],'Executable target bytes changed') +def build_native_daemon(stage, scratch, env, run, receipt, save): + """Compile this source; historical artifact bytes are never an input.""" + pin=INPUTS['freshNativeBuild'] + require(pin=={'toolchain':'1.97.1','target':'x86_64-apple-darwin','jobs':2,'deadlineSeconds':1500},'Unexpected native build recipe') + rustup,audit=tool_invocation('rustup') + verify_tool_invocation(rustup,audit) + receipt['rustup']=audit;save() + run([rustup,'toolchain','install',pin['toolchain'],'--profile','minimal','--component','rustfmt'],'rust-install',600) + verify_tool_invocation(rustup,audit) + cargo=Path(run([rustup,'which','--toolchain',pin['toolchain'],'cargo'],'cargo-path',30).strip()).resolve(strict=True) + rustc=Path(run([rustup,'which','--toolchain',pin['toolchain'],'rustc'],'rustc-path',30).strip()).resolve(strict=True) + env['RUSTC']=str(rustc) + version=run([rustc,'--version','--verbose'],'rust-version-verbose',30) + require(version.startswith('rustc 1.97.1 '),'Wrong Rust') + cargo_version=run([cargo,'--version'],'cargo-version',30) + require(cargo_version.startswith('cargo 1.97.1 '),'Wrong Cargo') + sdk=Path(run(['/usr/bin/xcrun','--show-sdk-path'],'sdk-path',30).strip()).resolve(strict=True) + require(sdk.is_dir(),'Apple SDK missing') + sdk_version=run(['/usr/bin/xcrun','--show-sdk-version'],'sdk-version',30).strip() + require(sdk_version,'Apple SDK version missing') + env['SDKROOT']=str(sdk) + tools={} + for name in ('clang','ld'): + path=Path(run(['/usr/bin/xcrun','--find',name],name+'-path',30).strip()).resolve(strict=True) + require(path.is_file() and os.access(path,os.X_OK),'Apple tool missing: '+name) + tools[name]={'path':str(path),'sha256':sha(path)} + settings={p.name:sha(p) for p in (sdk/'SDKSettings.json',sdk/'SDKSettings.plist') if p.is_file()} + require(settings,'Apple SDK settings missing') + receipt['compiler']={'rustcSha256':sha(rustc),'cargoSha256':sha(cargo),'versionVerbose':version,'cargoVersion':cargo_version,'jobs':2, + 'sdk':{'path':str(sdk),'version':sdk_version,'settings':settings},'appleTools':tools, + 'target':pin['target'],'rustflags':env.get('RUSTFLAGS',''),'targetDirectory':env['CARGO_TARGET_DIR']} + save() + run([cargo,'build','--release','--target',pin['target'],'--manifest-path','packages/paperclip-runner/runner/Cargo.toml','--locked', + '-p','paperclip-runner-core','--bin','paperclip-runnerd','-j','2'],'daemon-build',pin['deadlineSeconds']) + daemon=stage/'packages/paperclip-runner/dist/bin/paperclip-runnerd';daemon.parent.mkdir(parents=True,exist_ok=True) + shutil.copy2(scratch/'target'/pin['target']/'release/paperclip-runnerd',daemon) + run(['/usr/bin/codesign','--force','--sign','-',daemon],'daemon-sign',30) + run(['/usr/bin/codesign','--verify','--strict',daemon],'daemon-signature-verify',30) + require('Mach-O 64-bit executable x86_64' in run(['/usr/bin/file',daemon],'daemon-architecture',30),'Wrong daemon architecture') + receipt['daemonBuildMetadata']=json.loads(run([daemon,'--build-metadata'],'daemon-metadata',30)) + require(receipt['daemonBuildMetadata']==INPUTS['expectedDaemonBuildMetadata'],'Wrong daemon build metadata') + receipt.update(nativeRecompiled=True,nativeDaemonReuse=False,nativeBuildSource=SOURCE,daemonSha256=sha(daemon)) + save() + return daemon + def execute(args): stage=args.source.resolve(strict=True);out=args.output.resolve() out.mkdir(mode=0o700,parents=True,exist_ok=False) @@ -45,7 +89,6 @@ def execute(args): inherited_path=os.environ['PATH'] node=Path(shutil.which('node')).resolve(strict=True) pnpm=Path(shutil.which('pnpm')).resolve(strict=True) - # Reused daemon bytes retain their original compiler provenance; no new compiler is invoked. env={'PATH':inherited_path,'HOME':str(scratch/'home'),'TMPDIR':str(scratch),'LANG':'en_US.UTF-8','CI':'true', 'PAPERCLIP_TELEMETRY_ENABLED':'false','PAPERCLIP_RUNNER_SOURCE_REVISION':SOURCE, 'CARGO_HOME':str(scratch/'cargo'),'RUSTUP_HOME':str(scratch/'rustup'),'CARGO_TARGET_DIR':str(scratch/'target'), @@ -129,13 +172,7 @@ def execute(args): run([pnpm,'install','--frozen-lockfile','--ignore-scripts','--package-import-method','copy'],'install',600) source_guard.verify_source(stage,SOURCE,PIN['resolvedLockSha256']) run([pnpm,'--filter','@paperclipai/paperclip-runner','build:typescript'],'typescript',600) - daemon=stage/'packages/paperclip-runner/dist/bin/paperclip-runnerd' - receipt['nativeDaemonReuse']=import_daemon(args.daemon_archive,out/'source.tar',out/'reused-native-evidence',daemon,INPUTS['nativeDaemonReuse']);save() - run(['/usr/bin/codesign','--verify','--strict',daemon],'daemon-signature-verify',30) - require(sha(daemon)==INPUTS['nativeDaemonReuse']['selectedFiles']['paperclip-runnerd']['sha256'],'Daemon changed during signature verification') - require('Mach-O 64-bit executable x86_64' in run(['/usr/bin/file',daemon],'daemon-architecture',30),'Wrong daemon architecture') - receipt['daemonBuildMetadata']=json.loads(run([daemon,'--build-metadata'],'daemon-metadata',30)) - require(receipt['daemonBuildMetadata']==INPUTS['nativeDaemonReuse']['daemonBuildMetadata'],'Reused daemon metadata changed') + daemon=build_native_daemon(stage,scratch,env,run,receipt,save) pack=scratch/'provider-pack' run([node,'packages/paperclip-runner/scripts/build-provider-pack.mjs',pack,'--candidate-providers=pi,copilot,cursor'],'pack-build',600) verified=json.loads(run([node,HERE/'verify-pack.mjs',pack,SOURCE,'darwin','x64'],'pack-verify',180)) @@ -143,7 +180,7 @@ def execute(args): authority=source_guard.capture_authority(stage,pack) source_guard.verify_source(stage,SOURCE,PIN['resolvedLockSha256'],pack,authority) atomic_json(out/'pack-inventory.json',closed_tree(pack));shutil.copy2(pack/'provider-pack.json',out/'provider-pack.json');shutil.copy2(daemon,out/'paperclip-runnerd') - receipt.update(packVerification=verified,packManifestSha256=sha(out/'provider-pack.json'),packInventorySha256=sha(out/'pack-inventory.json'),daemonSha256=sha(daemon),nodeSha256=sha(node),declaredNativeInputsMatchOriginal=True,outputDigestsAssumedEqual=False) + receipt.update(packVerification=verified,packManifestSha256=sha(out/'provider-pack.json'),packInventorySha256=sha(out/'pack-inventory.json'),daemonSha256=sha(daemon),nodeSha256=sha(node),outputDigestsAssumedEqual=False) receipt['providerPackArchive']=retain_pack(pack,out/'provider-pack.tar.gz',authority['pack']) save() test=stage/'packages/paperclip-runner/test/pi-closed-startup.test.mjs' @@ -175,4 +212,4 @@ def execute(args): receipt['finishedAt']=datetime.datetime.now(datetime.timezone.utc).isoformat();save() if __name__=='__main__': - parser=argparse.ArgumentParser(description=__doc__);parser.add_argument('--source',type=Path,required=True);parser.add_argument('--output',type=Path,required=True);parser.add_argument('--daemon-archive',type=Path,required=True);execute(parser.parse_args()) + parser=argparse.ArgumentParser(description=__doc__);parser.add_argument('--source',type=Path,required=True);parser.add_argument('--output',type=Path,required=True);execute(parser.parse_args())