mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-08 21:03:51 +02:00
ci(runner): reuse pinned Intel daemon for cold-start proof
Build current TypeScript and provider pack while admitting the exact retained native daemon through source-input equality and original compiler provenance. Keep original startup assertions and evidence bounds. Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
1 parent
2fcdaa8caf
commit
9b7a91e3fc
5 files changed
+314
-23
No files matched your search
@@ -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
|
||||
|
||||
@@ -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',[])}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
@@ -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())
|
||||
Reference in new issue
Block a user