mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-09 06:15:21 +02:00
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>
179 lines
15 KiB
Python
179 lines
15 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 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,
|
|
'providerPromptsSubmitted':0,'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)
|
|
# 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'),
|
|
'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=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))
|
|
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(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'
|
|
require(sha(test)=='8967cf9c8cd130b68bf9d64abef8cb8d352af00646e2288b341d8c6ae758b47a','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('--output',type=Path,required=True);parser.add_argument('--daemon-archive',type=Path,required=True);execute(parser.parse_args())
|