diff --git a/.github/workflows/docker-runner-check.yml b/.github/workflows/docker-runner-check.yml index 31b2cd21b3..6763d352bb 100644 --- a/.github/workflows/docker-runner-check.yml +++ b/.github/workflows/docker-runner-check.yml @@ -170,7 +170,7 @@ jobs: test "$SOURCE_ROOT_TESTS_ONLY" != true test "$DIAGNOSE_AJV" != true test "$IMAGE_MODE" != true - test "$EXPECTED_SOURCE_SHA" = 5eba61ece929dcfb2b0c1761825a2d9e557c2554 + test "$EXPECTED_SOURCE_SHA" = 586af4d7f3b07aa683338aedb4e5182603b70401 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. @@ -189,6 +189,7 @@ jobs: timeout-minutes: 60 permissions: contents: read + actions: read steps: - name: Checkout trusted CI helpers independently of candidate source uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 @@ -211,13 +212,24 @@ 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" + --output "$GITHUB_WORKSPACE/pi-intel-evidence" \ + --daemon-archive "$RUNNER_TEMP/pi-native-daemon-36933598154.zip" - name: Admit complete evidence within the reserved storage bound if: always() id: pi_intel_evidence diff --git a/scripts/ci/pi-intel/native_daemon_reuse.py b/scripts/ci/pi-intel/native_daemon_reuse.py new file mode 100644 index 0000000000..6ec7fc248e --- /dev/null +++ b/scripts/ci/pi-intel/native_daemon_reuse.py @@ -0,0 +1,100 @@ +"""Import one pinned native daemon; never execute an archive or infer rebuild provenance.""" +import hashlib,json,os,shutil,stat,tarfile,zipfile +from pathlib import Path,PurePosixPath +from source_guard import require,sha + +def safe_name(name): + p=PurePosixPath(name) + require(bool(name) and not p.is_absolute() and '..' not in p.parts and '\\' not in name,'Unsafe archive path') + require(name.rstrip('/')==p.as_posix(),'Noncanonical archive path') + return p + +def extract_selected(archive,destination,pin): + archive=Path(archive);destination=Path(destination) + require(archive.is_file() and not archive.is_symlink(),'Missing or linked trusted artifact ZIP') + require(archive.stat().st_size==pin['artifactZipBytes'] and sha(archive)==pin['artifactZipSha256'],'Trusted artifact ZIP mismatch') + destination.mkdir(mode=0o700,parents=True,exist_ok=False) + with zipfile.ZipFile(archive) as z: + entries=z.infolist();names=[e.filename for e in entries] + require(len(names)==len(set(names)) and len(entries)<10000,'Duplicate or excessive ZIP entries') + require(sum(e.file_size for e in entries)<=16*1024**3,'ZIP expanded size exceeds evidence bound') + for e in entries: + safe_name(e.filename);kind=stat.S_IFMT(e.external_attr>>16) + require(kind in (0,stat.S_IFREG,stat.S_IFDIR),'Linked or special ZIP entry') + for name,expected in pin['selectedFiles'].items(): + require(safe_name(name).name==name,'Selected member must be top-level') + item=z.getinfo(name) + require(not item.is_dir() and item.file_size==expected['bytes'],'Selected member shape mismatch') + target=destination/name + with z.open(item) as source,target.open('xb') as output:shutil.copyfileobj(source,output,1024*1024) + target.chmod(0o600) + require(sha(target)==expected['sha256'],'Selected member digest mismatch') + return destination + +def archive_inventory(archive): + result={} + with tarfile.open(archive,'r:') as tar: + for item in tar: + safe_name(item.name) + require(item.name not in result,'Duplicate source archive entry') + if item.isdir():continue + if item.isfile(): + stream=tar.extractfile(item);h=hashlib.sha256() + for chunk in iter(lambda:stream.read(1024*1024),b''):h.update(chunk) + row={'kind':'file','sha256':h.hexdigest(),'mode':item.mode,'size':item.size} + else: + require(item.issym(),'Unsupported source archive member') + row={'kind':'symlink','target':item.linkname,'mode':item.mode} + result[item.name]=row + return result + +def native_path(name): + p=PurePosixPath(name) + return (name.startswith(('packages/paperclip-runner/runner/','packages/paperclip-runner/protocol/')) + or p.suffix=='.rs' or p.name in ('Cargo.toml','Cargo.lock','rust-toolchain','rust-toolchain.toml') + or '.cargo' in p.parts) + +def compare_declared_inputs(original,current,allowed_delta): + old_native={p:v for p,v in original.items() if native_path(p)} + new_native={p:v for p,v in current.items() if native_path(p)} + require(old_native and old_native==new_native,'Declared native source/config/protocol inputs differ') + require(all(row['kind']=='file' for row in old_native.values()),'Native input symlink refused') + # Also reject ANY other source delta, including otherwise unrecognized native inputs. + delta=sorted(p for p in set(original)|set(current) if original.get(p)!=current.get(p)) + require(delta==sorted(allowed_delta),'Unexpected full source delta') + require(not any(native_path(p) for p in allowed_delta),'Native input cannot be excepted') + return {'nativeInputs':old_native,'sourceDelta':{p:{'original':original.get(p),'candidate':current.get(p)} for p in delta}} + +def import_daemon(archive,current_archive,destination,daemon,pin): + retained=extract_selected(archive,destination,pin) + require(sha(retained/'source.tar')==pin['sourceArchiveSha256'],'Original source archive mismatch') + original=json.loads((retained/'receipt.json').read_text()) + require(original['sourceRevision']==pin['originalBuildSource'] and original['runId']==pin['artifactRunId'] and original['runAttempt']=='1','Original build identity mismatch') + require(original['trustedWorkflowRevision']==pin['artifactWorkflowRevision'],'Original workflow mismatch') + require(original['helpers']['verify.py']==pin['originalVerifierSha256'],'Original build recipe mismatch') + require(original['compiler']==pin['compiler'],'Original compiler provenance mismatch') + require(original['daemonSha256']==pin['selectedFiles']['paperclip-runnerd']['sha256'],'Original daemon binding mismatch') + commands={r['label']:r for r in original['commands']} + require(len(commands)==len(original['commands']),'Duplicate original command labels') + for name in ('rust-install','rust-version-verbose','daemon-build','daemon-sign','daemon-architecture','daemon-metadata'): + require(commands[name]['exitCode']==0 and commands[name]['status']=='passed','Original native production step did not pass') + require(commands['daemon-build']['command'][1:]==pin['buildCommandTail'],'Original build flags mismatch') + require(original['daemonBuildMetadata']==pin['daemonBuildMetadata'],'Original daemon metadata mismatch') + require(original['cleanupUncertain'] is False,'Original process cleanup uncertain') + old=archive_inventory(retained/'source.tar');current=archive_inventory(current_archive) + equality=compare_declared_inputs(old,current,pin['sourceDeltaAllowed']) + inventory=json.loads((retained/'source-input-inventory.json').read_text()) + require(original['sourceInputInventorySha256']==sha(retained/'source-input-inventory.json'),'Original source inventory binding mismatch') + require(all(inventory[p]==row['sha256'] for p,row in equality['nativeInputs'].items()),'Original native input inventory mismatch') + (retained/'declared-input-equality.json').write_text(json.dumps(equality,indent=2)+'\n') + daemon=Path(daemon);daemon.parent.mkdir(parents=True,exist_ok=True) + require(not os.path.lexists(daemon),'Refuse overwriting generated native daemon') + shutil.copyfile(retained/'paperclip-runnerd',daemon);daemon.chmod(0o755) + require(sha(daemon)==pin['selectedFiles']['paperclip-runnerd']['sha256'],'Imported daemon changed') + return {'schema':pin['schema'],'originalBuildSource':pin['originalBuildSource'],'nativeInputEquivalentTo':pin['nativeInputEquivalentTo'], + 'artifactRunId':pin['artifactRunId'],'artifactId':pin['artifactId'],'artifactZipSha256':sha(archive), + 'selectedFiles':pin['selectedFiles'],'compiler':pin['compiler'],'buildCommandTail':pin['buildCommandTail'], + 'declaredInputCount':len(equality['nativeInputs']),'declaredInputEqualitySha256':sha(retained/'declared-input-equality.json'), + 'currentSourceArchiveSha256':sha(current_archive),'daemonSha256':sha(daemon), + 'missingOriginalToolchainEvidence':pin['missingOriginalToolchainEvidence'],'claims':pin['claims'], + 'historicalQualificationStatus':original['status'],'historicalTestFailures':original.get('testFailures',[])} diff --git a/scripts/ci/pi-intel/profile-inputs.json b/scripts/ci/pi-intel/profile-inputs.json index 5e17a6f21a..de6183eaef 100644 --- a/scripts/ci/pi-intel/profile-inputs.json +++ b/scripts/ci/pi-intel/profile-inputs.json @@ -45,7 +45,7 @@ "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-runtime-extension.ts": "f3446be10d984c368c52202e768aefe9b4a25968e09922e98e3f016c9dc0198e", - "packages/paperclip-runner/src/drivers/acpx/pi-verified-runtime.ts": "6a08420964ac3f8c91a248b90098cd025a47612ed4060de43db30d1ef80be296", + "packages/paperclip-runner/src/drivers/acpx/pi-verified-runtime.ts": "1641cbc6d86f07a80e5db78ee4567af8677a6823ddc60a17ca5020cedfb53a6c", "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", @@ -173,8 +173,8 @@ } }, "localComparison": { - "sourceRevision": "5eba61ece929dcfb2b0c1761825a2d9e557c2554", - "sourceArchiveSha256": "f751f86b19701757d30501e39d47fe7d20a102e32f9e4c4df585074393e85237", + "sourceRevision": "586af4d7f3b07aa683338aedb4e5182603b70401", + "sourceArchiveSha256": "87f48d5c0df9c4e9c30c880c5b1da862b063d9ba4c12afc4788ee5fd6adcfc6e", "trackedLockSha256": "e11d69fa8702a906c293c1cb307c71df90d3b1f1617b3c23ebf17fa9a87fec79", "resolvedLockSha256": "38338a6867358440c5ab5993eaeb85fb501bc7df65d24acf52fbd63c880ee4ba", "nodeSha256": "7abcf39bd37ab251015337ff75304d7555f0d8e88c6e0fbf04bce8ce34636f49", @@ -182,9 +182,135 @@ "piRuntimeVersion": "1.0.0", "piServerVersion": "0.0.33", "acpxVersion": "0.13.1", - "localPackDigest": "sha256:a7ae16b680c6fcaa9438faa46562ba31519bb0fb42e21b0a4b00e8936c462d89", - "localDaemonSha256": "c0e9acdd461d5efe4a10f9fd751e8a67beceede4d43593951d7079edad657044", - "localBuildReceiptSha256": "f9bdef008c11d30e89da20588e750df7689d4f5f7400ce3723fb467d82c53581", - "note": "CI pack/daemon are fresh outputs; compare frozen inputs and native closure, not local output digests." + "note": "Fresh S TypeScript/sidecar/provider pack; exact retained native Intel daemon from5eba after declared-input equality. Historical local pack is not a current output. Original Apple SDK/linker details were not retained; no equivalence or fresh-compilation claim.", + "historicalLocalOutputs": { + "localPackDigest": "sha256:a7ae16b680c6fcaa9438faa46562ba31519bb0fb42e21b0a4b00e8936c462d89", + "localDaemonSha256": "c0e9acdd461d5efe4a10f9fd751e8a67beceede4d43593951d7079edad657044", + "localBuildReceiptSha256": "f9bdef008c11d30e89da20588e750df7689d4f5f7400ce3723fb467d82c53581", + "sourceRevision": "5eba61ece929dcfb2b0c1761825a2d9e557c2554" + } + }, + "nativeDaemonReuse": { + "schema": "paperclip.native-daemon-reuse/v1", + "originalBuildSource": "5eba61ece929dcfb2b0c1761825a2d9e557c2554", + "nativeInputEquivalentTo": "586af4d7f3b07aa683338aedb4e5182603b70401", + "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/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-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/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-restart-flow.test.ts", + "tests/runner-e2e/pi-native-restart-flow.ts", + "tests/runner-e2e/runner.spec.ts", + "tests/runner-e2e/server-stop.test.ts" + ], + "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 + }, + "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" + ], + "originalVerifierSha256": "7de772df5beabe6413e2d0d8dd287017ef2be8d3b3cdc76e8a23007ba547b509", + "missingOriginalToolchainEvidence": [ + "Apple SDK version", + "Apple linker/compiler identity", + "full Rust compiler sysroot inventory" + ], + "claims": { + "freshNativeCompilation": false, + "currentAppleToolchainEquivalent": false, + "completeDeclaredNativeSourceConfigProtocolEquality": true, + "originalCompilerProvenanceRetained": true + } } } diff --git a/scripts/ci/pi-intel/test_native_daemon_reuse.py b/scripts/ci/pi-intel/test_native_daemon_reuse.py new file mode 100644 index 0000000000..c218d9785d --- /dev/null +++ b/scripts/ci/pi-intel/test_native_daemon_reuse.py @@ -0,0 +1,58 @@ +"""Synthetic archive admission; no compiler/runtime/network execution.""" +import hashlib,io,json,stat,tarfile,tempfile,unittest,zipfile +from pathlib import Path +from native_daemon_reuse import archive_inventory,compare_declared_inputs,extract_selected,import_daemon +from source_guard import sha +class NativeDaemonReuseTests(unittest.TestCase): + def setUp(self): + self.temp=tempfile.TemporaryDirectory();self.addCleanup(self.temp.cleanup);self.root=Path(self.temp.name) + self.old={'packages/paperclip-runner/runner/Cargo.toml':b'[workspace]','packages/paperclip-runner/runner/crates/lib.rs':b'fn main(){}','packages/paperclip-runner/protocol/schema.json':b'{}','packages/paperclip-runner/rust-toolchain.toml':b'channel="1.97.1"','src/runtime.ts':b'old'} + def tar(files): + stream=io.BytesIO() + with tarfile.open(fileobj=stream,mode='w') as archive: + for name,content in files.items(): + item=tarfile.TarInfo(name);item.size=len(content);item.mode=0o644;archive.addfile(item,io.BytesIO(content)) + return stream.getvalue() + self.current=self.root/'source.tar';self.current.write_bytes(tar(self.old|{'src/runtime.ts':b'new'})) + commands=[{'label':n,'exitCode':0,'status':'passed','command':['cargo','build','--locked']} for n in ('rust-install','rust-version-verbose','daemon-build','daemon-sign','daemon-architecture','daemon-metadata')] + inventory={p:hashlib.sha256(data).hexdigest() for p,data in self.old.items()} + self.members={'source.tar':tar(self.old),'source-input-inventory.json':json.dumps(inventory).encode(),'paperclip-runnerd':b'fake-test-daemon-not-executable'} + receipt={'sourceRevision':'old','runId':'123','runAttempt':'1','trustedWorkflowRevision':'workflow','helpers':{'verify.py':'recipe'},'compiler':{'version':'pinned'},'daemonSha256':hashlib.sha256(self.members['paperclip-runnerd']).hexdigest(),'commands':commands,'daemonBuildMetadata':{'schema':'fake'},'cleanupUncertain':False,'sourceInputInventorySha256':hashlib.sha256(self.members['source-input-inventory.json']).hexdigest(),'status':'historical_failure'} + self.members['receipt.json']=json.dumps(receipt).encode();self.zip=self.root/'input.zip' + with zipfile.ZipFile(self.zip,'w') as z: + for name,data in self.members.items():z.writestr(name,data) + self.pin={'schema':'test','originalBuildSource':'old','nativeInputEquivalentTo':'new','artifactRunId':'123','artifactId':'456','artifactWorkflowRevision':'workflow','artifactZipSha256':sha(self.zip),'artifactZipBytes':self.zip.stat().st_size,'selectedFiles':{p:{'sha256':hashlib.sha256(b).hexdigest(),'bytes':len(b)} for p,b in self.members.items()},'sourceArchiveSha256':hashlib.sha256(self.members['source.tar']).hexdigest(),'compiler':{'version':'pinned'},'buildCommandTail':['build','--locked'],'daemonBuildMetadata':{'schema':'fake'},'sourceDeltaAllowed':['src/runtime.ts'],'originalVerifierSha256':'recipe','missingOriginalToolchainEvidence':['Apple SDK'],'claims':{'freshNativeCompilation':False}} + def run_import(self,pin=None):return import_daemon(self.zip,self.current,self.root/'retained',self.root/'dist/bin/daemon',pin or self.pin) + def test_exact_import_retains_original_provenance_and_current_equality(self): + result=self.run_import();self.assertEqual(result['declaredInputCount'],4);self.assertEqual(result['originalBuildSource'],'old');self.assertEqual(result['nativeInputEquivalentTo'],'new');self.assertFalse(result['claims']['freshNativeCompilation']);self.assertEqual((self.root/'dist/bin/daemon').read_bytes(),self.members['paperclip-runnerd']);self.assertEqual(result['historicalQualificationStatus'],'historical_failure') + def test_wrong_whole_zip_hash_rejected_before_extraction(self): + self.pin['artifactZipSha256']='0'*64 + with self.assertRaisesRegex(RuntimeError,'ZIP mismatch'):self.run_import() + self.assertFalse((self.root/'retained').exists()) + def test_selected_daemon_digest_mismatch_rejected(self): + self.pin['selectedFiles']['paperclip-runnerd']['sha256']='0'*64 + with self.assertRaisesRegex(RuntimeError,'member digest'):self.run_import() + def test_archive_traversal_and_symlink_rejected(self): + for name,mode in [('../escape',stat.S_IFREG|0o644),('evil',stat.S_IFLNK|0o777)]: + with self.subTest(name=name),tempfile.TemporaryDirectory() as td: + p=Path(td)/'bad.zip' + with zipfile.ZipFile(p,'w') as z: + item=zipfile.ZipInfo(name);item.external_attr=mode<<16;z.writestr(item,b'x') + pin=self.pin|{'artifactZipBytes':p.stat().st_size,'artifactZipSha256':sha(p)} + with self.assertRaises(RuntimeError):extract_selected(p,Path(td)/'out',pin) + def test_native_change_cannot_hide_in_delta_allowlist(self): + old={'runner/lib.rs':{'kind':'file','sha256':'old'}};new={'runner/lib.rs':{'kind':'file','sha256':'new'}} + with self.assertRaisesRegex(RuntimeError,'native source'):compare_declared_inputs(old,new,['runner/lib.rs']) + def test_new_native_config_or_unlisted_source_delta_rejected(self): + old=archive_inventory(self.current) + for name in ['.cargo/config.toml','unlisted/file.txt']: + with self.subTest(name=name),self.assertRaises(RuntimeError):compare_declared_inputs(old,old|{name:{'kind':'file','sha256':'new'}},[]) + def test_original_compiler_or_flags_mismatch_rejected(self): + for key,value in [('compiler',{'version':'different'}),('buildCommandTail',['build','--unlocked'])]: + with self.subTest(key=key),tempfile.TemporaryDirectory() as td: + with self.assertRaises(RuntimeError):import_daemon(self.zip,self.current,Path(td)/'retained',Path(td)/'daemon',self.pin|{key:value}) + def test_existing_daemon_not_overwritten(self): + target=self.root/'dist/bin/daemon';target.parent.mkdir(parents=True);target.write_bytes(b'existing') + with self.assertRaisesRegex(RuntimeError,'overwriting'):self.run_import() + self.assertEqual(target.read_bytes(),b'existing') +if __name__=='__main__':unittest.main() diff --git a/scripts/ci/pi-intel/verify.py b/scripts/ci/pi-intel/verify.py index b7671dfbfd..175f33a23a 100644 --- a/scripts/ci/pi-intel/verify.py +++ b/scripts/ci/pi-intel/verify.py @@ -7,6 +7,7 @@ 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()) @@ -44,8 +45,7 @@ def execute(args): inherited_path=os.environ['PATH'] node=Path(shutil.which('node')).resolve(strict=True) pnpm=Path(shutil.which('pnpm')).resolve(strict=True) - rustup,rustup_audit=tool_invocation('rustup') - receipt['rustupTool']=rustup_audit;save() + # 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'), @@ -57,7 +57,6 @@ def execute(args): signal.signal(signal.SIGTERM,stop_signal) def run(command,label,timeout,cwd=stage,command_env=None,owned=False): nonlocal active,owner - if str(command[0])==str(rustup):verify_tool_invocation(rustup,rustup_audit) lifecycle.begin() row={'label':label,'startedAt':datetime.datetime.now(datetime.timezone.utc).isoformat(),'command':list(map(str,command)),'deadlineSeconds':timeout,'status':'running'} receipt['commands'].append(row);save();start=time.monotonic() @@ -130,17 +129,13 @@ 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) - run([rustup,'toolchain','install','1.97.1','--profile','minimal','--component','rustfmt'],'rust-install',600) - cargo=run([rustup,'which','--toolchain','1.97.1','cargo'],'cargo-path',30).strip() - rustc=run([rustup,'which','--toolchain','1.97.1','rustc'],'rustc-path',30).strip();env['RUSTC']=rustc - require(run([rustc,'--version'],'rust-version',30).startswith('rustc 1.97.1 '),'Wrong Rust') - receipt['compiler']={'rustcSha256':sha(rustc),'cargoSha256':sha(cargo),'versionVerbose':run([rustc,'--version','--verbose'],'rust-version-verbose',30),'jobs':2} - run([cargo,'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'],'daemon-build',1500) - daemon=stage/'packages/paperclip-runner/dist/bin/paperclip-runnerd';daemon.parent.mkdir(parents=True,exist_ok=True) - shutil.copy2(scratch/'target/x86_64-apple-darwin/release/paperclip-runnerd',daemon) - run(['/usr/bin/codesign','--force','--sign','-',daemon],'daemon-sign',30) + 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') 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)) @@ -148,7 +143,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),buildInputsMatchLocal=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),declaredNativeInputsMatchOriginal=True,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' @@ -180,4 +175,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);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);parser.add_argument('--daemon-archive',type=Path,required=True);execute(parser.parse_args())