mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-07 16:11:46 +02:00
237 lines
19 KiB
Python
237 lines
19 KiB
Python
#!/usr/bin/env python3
|
|
"""Frozen Pi1 native Intel qualification evidence; no keys, prompts, or publication."""
|
|
import argparse,datetime,difflib,hashlib,json,os,platform,shutil,signal,subprocess,sys,tempfile,time
|
|
from pathlib import Path
|
|
from closed_inventory import closed_tree
|
|
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())
|
|
PIN=INPUTS['localComparison']
|
|
SOURCE=PIN['sourceRevision']
|
|
sha=source_guard.sha
|
|
require=source_guard.require
|
|
|
|
def tool_invocation(name):
|
|
found=shutil.which(name)
|
|
require(found is not None,'Required executable not found: '+name)
|
|
# Multicall CLIs dispatch on argv[0]; preserve the name returned by PATH lookup.
|
|
invocation=Path(found).absolute()
|
|
target=invocation.resolve(strict=True)
|
|
require(invocation.name==name and target.is_file() and os.access(invocation,os.X_OK),'Unexpected executable invocation')
|
|
return invocation, {'invocationPath':str(invocation),'resolvedTarget':str(target),'targetSha256':sha(target)}
|
|
|
|
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 reuse_native_daemon(archive,stage,out,scratch,run,receipt,save):
|
|
"""Retain original compiler/signature provenance; do not rebuild or re-sign."""
|
|
pin=INPUTS['nativeDaemonReuse']
|
|
require(pin['nativeInputEquivalentTo']==SOURCE and pin['claims']['freshNativeCompilation'] is False,'Wrong native reuse authority')
|
|
daemon=stage/'packages/paperclip-runner/dist/bin/paperclip-runnerd'
|
|
retained=scratch/'original-native-artifact'
|
|
reuse=import_daemon(archive,out/'source.tar',retained,daemon,pin)
|
|
require(reuse['declaredInputCount']==145,'Unexpected native input inventory scope')
|
|
refs=out/'original-native-provenance';refs.mkdir(mode=0o700)
|
|
for name in ('receipt.json','source-input-inventory.json','declared-input-equality.json'):
|
|
shutil.copy2(retained/name,refs/name)
|
|
receipt.update(nativeRecompiled=False,nativeDaemonReuse=reuse,nativeBuildSource=pin['originalBuildSource'],
|
|
originalNativeCompiler=pin['compiler'],daemonSha256=sha(daemon));save()
|
|
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')
|
|
require(sha(daemon)==pin['selectedFiles']['paperclip-runnerd']['sha256'],'Reused daemon changed during verification')
|
|
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)
|
|
scratch=Path(tempfile.mkdtemp(prefix='pc-intel-',dir='/private/tmp'));scratch.chmod(0o700)
|
|
identity=(scratch.stat().st_dev,scratch.stat().st_ino,scratch.stat().st_uid)
|
|
receipt={'status':'running','sourceRevision':SOURCE,'trustedWorkflowRevision':os.environ['GITHUB_WORKFLOW_SHA'],
|
|
'startedAt':datetime.datetime.now(datetime.timezone.utc).isoformat(),'runId':os.environ['GITHUB_RUN_ID'],'runAttempt':os.environ['GITHUB_RUN_ATTEMPT'],
|
|
'runnerLabel':'macos-15-intel','hostIsBareMetalClaim':False,'providerCalls':0,'credentialsRead':False,
|
|
'externalProviderPromptsSubmitted':0,'syntheticLoopbackModelPromptsExpected':True,'imagePublications':0,'cloudSandboxesCreated':0,'automaticRetries':0,
|
|
'model':PIN['model'],'commands':[],'helpers':{p.name:sha(p) for p in HERE.iterdir() if p.is_file()},
|
|
'originalTestSourceUnmodified':True,'cleanupObserverUsed':True,'localComparison':PIN}
|
|
def save():atomic_json(out/'receipt.json',receipt)
|
|
save()
|
|
inherited_path=os.environ['PATH']
|
|
node=Path(shutil.which('node')).resolve(strict=True)
|
|
pnpm=Path(shutil.which('pnpm')).resolve(strict=True)
|
|
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'),
|
|
'RUSTUP_TOOLCHAIN':'1.97.1','CARGO_BUILD_JOBS':'2','npm_config_audit':'false','npm_config_fund':'false',
|
|
'npm_config_update_notifier':'false','npm_config_package_import_method':'copy'}
|
|
Path(env['HOME']).mkdir(mode=0o700)
|
|
active=None;owner=None;lifecycle=Lifecycle()
|
|
def stop_signal(signum,_):raise InterruptedError('CI stopped with signal '+str(signum))
|
|
signal.signal(signal.SIGTERM,stop_signal)
|
|
def run(command,label,timeout,cwd=stage,command_env=None,owned=False):
|
|
nonlocal active,owner
|
|
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()
|
|
with (out/(label+'.log')).open('x') as log:
|
|
try:
|
|
if owned:owner=OwnedProcesses(out/(label+'-processes.json'),scratch,stage,pack/'dist/cli/acpx-runtime-sidecar.cjs')
|
|
active=subprocess.Popen(row['command'],cwd=cwd,env=command_env or env,stdout=log,stderr=log,start_new_session=True)
|
|
while active.poll() is None:
|
|
if owner:owner.observe(active.pid)
|
|
require(time.monotonic()-start<timeout,'Command deadline exceeded: '+label)
|
|
time.sleep(.1 if owner else .5)
|
|
if owner:owner.observe(active.pid)
|
|
finally:
|
|
try:
|
|
# No reliable returned handle means launch ownership is uncertain.
|
|
require(active is not None,'Launch interrupted without a reliable process handle')
|
|
if owner:
|
|
shutdown=owner.stop(active.pid);row['shutdown']=shutdown
|
|
require(shutdown['status']=='stopped' and not owner.live(),'Owned descendants remain')
|
|
elif active.poll() is None:
|
|
require(os.getpgid(active.pid)==active.pid,'Process group identity changed')
|
|
os.killpg(active.pid,signal.SIGTERM)
|
|
try:active.wait(timeout=15)
|
|
except subprocess.TimeoutExpired:os.killpg(active.pid,signal.SIGKILL)
|
|
active.wait(timeout=10)
|
|
row.update(exitCode=active.returncode,durationSeconds=round(time.monotonic()-start,3),logSha256=sha(out/(label+'.log')),status='passed' if active.returncode==0 else 'failed')
|
|
lifecycle.confirmed()
|
|
active=None;owner=None
|
|
except BaseException as error:
|
|
lifecycle.failed()
|
|
row.update(status='cleanup_uncertain',cleanupErrorType=type(error).__name__)
|
|
raise
|
|
finally:save()
|
|
require(row['exitCode']==0,'Command failed without retry: '+label)
|
|
return (out/(label+'.log')).read_text()
|
|
def source_inventory():
|
|
names=subprocess.check_output(['git','ls-files','-z'],cwd=stage).split(b'\0')
|
|
return {os.fsdecode(n):sha(stage/os.fsdecode(n)) for n in names if n and (stage/os.fsdecode(n)).is_file()}
|
|
try:
|
|
require(platform.system()=='Darwin' and platform.machine()=='x86_64','Native Intel macOS required')
|
|
translated=subprocess.run(['/usr/sbin/sysctl','-in','sysctl.proc_translated'],capture_output=True,text=True)
|
|
arm=subprocess.run(['/usr/sbin/sysctl','-in','hw.optional.arm64'],capture_output=True,text=True)
|
|
require(translated.stdout.strip() in ('','0') and arm.stdout.strip() in ('','0'),'Rosetta/ARM host rejected')
|
|
hardware=run(['/usr/sbin/sysctl','hw.machine','machdep.cpu.vendor','machdep.cpu.brand_string'],'hardware',30)
|
|
require('GenuineIntel' in hardware and 'x86_64' in hardware,'Intel hardware evidence missing')
|
|
receipt['host']={'uname':platform.uname()._asdict(),'translated':translated.stdout.strip(),'translatedExitCode':translated.returncode,'arm64':arm.stdout.strip(),'arm64ExitCode':arm.returncode,'sysctl':hardware}
|
|
require(sha(node)==PIN['nodeSha256'],'Official pinned Node bytes mismatch')
|
|
require(json.loads(run([node,'-p','JSON.stringify([process.platform,process.arch,process.version])'],'node',30))==['darwin','x64','v24.21.0'],'Node architecture/version mismatch')
|
|
require(run([pnpm,'--version'],'pnpm',30).strip()=='9.15.4','pnpm version mismatch')
|
|
npm=Path(shutil.which('npm')).resolve(strict=True)
|
|
receipt['packageManagers']={'pnpmEntrySha256':sha(pnpm),'pnpmVersion':'9.15.4','npmEntrySha256':sha(npm),'npmVersion':run([npm,'--version'],'npm-version',30).strip()}
|
|
require(subprocess.check_output(['git','rev-parse','HEAD'],cwd=stage,text=True).strip()==SOURCE,'Wrong frozen source')
|
|
require(not subprocess.check_output(['git','status','--porcelain'],cwd=stage),'Source checkout dirty')
|
|
with (out/'source.tar').open('xb') as f:subprocess.run(['git','archive',SOURCE],cwd=stage,stdout=f,check=True)
|
|
require(sha(out/'source.tar')==PIN['sourceArchiveSha256'],'Source archive mismatch')
|
|
lock=stage/'pnpm-lock.yaml';original=lock.read_text();require(sha(lock)==PIN['trackedLockSha256'],'Tracked lock mismatch')
|
|
require(original.count('xinuppsghzi2pwhozdhfegto3m')==4,'Unexpected overlay scope')
|
|
resolved=original.replace('xinuppsghzi2pwhozdhfegto3m','glhukwf7nfpmpbdvrnmtdtjn5q')
|
|
(out/'original-pnpm-lock.yaml').write_text(original);lock.write_text(resolved)
|
|
require(sha(lock)==PIN['resolvedLockSha256'],'Resolved lock mismatch')
|
|
shutil.copy2(lock,out/'resolved-pnpm-lock.yaml')
|
|
(out/'lock.diff').write_text(''.join(difflib.unified_diff(original.splitlines(True),resolved.splitlines(True),fromfile='tracked-lock',tofile='resolved-lock')))
|
|
for agent,pin in INPUTS['profiles'].items():
|
|
p=stage/pin['path'];require(sha(p)==pin['sha256'],'Profile bytes changed: '+agent)
|
|
data=json.loads(p.read_text());require(data['commandDigest']==pin['digest'],'Profile digest changed')
|
|
for rel,digest in INPUTS['additionalSourceFiles'].items():require(sha(stage/rel)==digest,'Supplementary source changed: '+rel)
|
|
receipt['sourceInputInventory']=source_inventory();atomic_json(out/'source-input-inventory.json',receipt.pop('sourceInputInventory'))
|
|
receipt['sourceInputInventorySha256']=sha(out/'source-input-inventory.json');receipt['profiles']=INPUTS['profiles'];save()
|
|
source_guard.verify_source(stage,SOURCE,PIN['resolvedLockSha256'])
|
|
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=reuse_native_daemon(args.native_archive,stage,out,scratch,run,receipt,save)
|
|
pack=scratch/'provider-pack'
|
|
run([node,'packages/paperclip-runner/scripts/build-provider-pack.mjs',pack],'pack-build',600)
|
|
verified=json.loads(run([node,HERE/'verify-pack.mjs',pack,SOURCE,'darwin','x64'],'pack-verify',180))
|
|
require(sha(pack/'node_modules/node/bin/node')==PIN['nodeSha256'],'Pack Node mismatch')
|
|
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(normalProviderSelection=INPUTS['normalProviderSelection'],qualificationOverride=False,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'
|
|
require(sha(test)=='a2fea9fb7a5d8b9282123f0b680023060438fcb1ff8f5676733e319c0ae5df30','Closed startup test changed')
|
|
receipt['testSourceSha256']={n:sha(stage/'packages/paperclip-runner/test'/n) for n in ['pi-closed-startup.test.mjs','pi-native-package-contract.test.mjs','pi-acp-package-contract.test.mjs']};save()
|
|
retained=out/'retained-test-state';retained.mkdir(mode=0o700)
|
|
testenv={'HOME':str(scratch/'home'),'TMPDIR':str(scratch),'PATH':'/usr/bin:/bin','LANG':'en_US.UTF-8',
|
|
'PI_INTEL_OWNED_TMP':str(scratch),'PI_INTEL_RETAINED_STATE':str(retained),
|
|
'PAPERCLIP_TEST_PI_STARTUP_PACKAGE_ROOT':str(pack),'PAPERCLIP_TEST_PI_STARTUP_RUNNER_BINARY':str(daemon),
|
|
'PAPERCLIP_TEST_PI_RUNTIME_ROOT':str(pack/'provider-assets/pi/darwin-x64/runtime/node_modules/@earendil-works/pi-coding-agent'),
|
|
'PAPERCLIP_TEST_PI_ACP_PACKAGE':str(pack/'provider-assets/pi/darwin-x64/runtime/node_modules/pi-acp/package.json')}
|
|
# The observer is passed only to the test process; runtime children receive no NODE_OPTIONS.
|
|
cases=[('closed-startup',[node,'--import',HERE/'retain-test-state.mjs','--test',test]),('native-extension-contracts',[node,'--test',stage/'packages/paperclip-runner/test/pi-native-package-contract.test.mjs',stage/'packages/paperclip-runner/test/pi-acp-package-contract.test.mjs'])]
|
|
failures=run_test_cases(cases,lambda command,label:run(command,label,180,command_env=testenv,owned=True),lifecycle)
|
|
source_guard.verify_source(stage,SOURCE,PIN['resolvedLockSha256'],pack,authority)
|
|
require(source_inventory()==json.loads((out/'source-input-inventory.json').read_text()),'Tracked source changed')
|
|
require(closed_tree(pack)==json.loads((out/'pack-inventory.json').read_text()),'Pack changed during tests')
|
|
receipt['retainedTestStateInventory']=closed_tree(retained)
|
|
receipt['testFailures']=failures;require(not failures,'Native Intel test failure; no retry')
|
|
receipt['status']='native_intel_no_provider_checks_passed_pending_review'
|
|
except BaseException as error:
|
|
receipt.update(status='failed_no_retry',errorType=type(error).__name__,error=str(error));raise
|
|
finally:
|
|
if scratch.exists():
|
|
st=scratch.stat();require((st.st_dev,st.st_ino,st.st_uid)==identity and st.st_uid==os.getuid(),'Scratch ownership changed')
|
|
if lifecycle.safe:shutil.rmtree(scratch);receipt['scratchCleanup']='removed_verified_owned_root'
|
|
else:receipt['scratchCleanup']='retained_unresolved_processes'
|
|
receipt['cleanupUncertain']=not lifecycle.safe
|
|
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('--native-archive',type=Path,required=True);parser.add_argument('--output',type=Path,required=True);execute(parser.parse_args())
|