"""One-shot Linux Pi-child fault. No environment reads; never targets a worker. The caller is the nonce-bound remote observer. Its already admitted lease and run root are rechecked here, then pidfd pins the exact child across PID reuse. This helper does not relax provider admission or manufacture runtime events. """ import base64 import hashlib import json import os from pathlib import Path import re import signal import stat import sys PACK = '/opt/paperclip-runner/provider-pack' ENTRY = 'node_modules/@earendil-works/pi-coding-agent/dist/cli.js' NODE = 'node/bin/node' EXTENSION = 'extensions/paperclip.js' GUARD = '.paperclip-native-module-guard.cjs' def require(value, code): if not value: raise RuntimeError('pi_provider_fault:' + code) def proc(pid, boot): raw = Path(f'/proc/{pid}/stat').read_text() require(raw.startswith(f'{pid} ('), 'stat_pid') fields = raw.rsplit(') ', 1)[1].split() require(fields[0] != 'Z', 'dead_process') return {'pid': pid, 'ppid': int(fields[1]), 'startTicks': fields[19], 'bootId': boot} def argv(pid): raw = Path(f'/proc/{pid}/cmdline').read_bytes() require(0 < len(raw) <= 32768, 'argv_bound') return raw.decode().rstrip('\0').split('\0') def flag(args, name): require(args.count(name) == 1 and args.index(name) + 1 < len(args), 'flag_' + name) return args[args.index(name) + 1] def checked_file(path, maximum, sealed=False): require(os.path.realpath(path) == path, 'file_realpath') fd = os.open(path, os.O_RDONLY | os.O_NOFOLLOW) try: before = os.fstat(fd) require(stat.S_ISREG(before.st_mode) and before.st_nlink == 1 and 0 < before.st_size <= maximum, 'file_shape') if sealed: require(before.st_mode & 0o222 == 0, 'file_writable') chunks = [] while True: chunk = os.read(fd, min(1024 * 1024, maximum + 1)) if not chunk: break chunks.append(chunk) require(sum(map(len, chunks)) <= maximum, 'file_bound') after, named = os.fstat(fd), os.lstat(path) identity = lambda s: (s.st_dev, s.st_ino, s.st_size, s.st_mtime_ns, s.st_ctime_ns, s.st_mode, s.st_nlink) require(identity(before) == identity(after) == identity(named), 'file_changed') return b''.join(chunks), (before.st_dev, before.st_ino) finally: os.close(fd) def digest(data): return hashlib.sha256(data).hexdigest() def checked_runner_executable(path): # Production stages a verified preinstalled runner with ln -sfn. Admit # that named link only while its identity and resolved regular file remain # stable; inspect still requires both the pinned hash and /proc/exe inode. before = os.lstat(path) require(stat.S_ISREG(before.st_mode) or stat.S_ISLNK(before.st_mode), 'runner_file_shape') resolved = os.path.realpath(path) content, inode = checked_file(resolved, 256 * 1024 * 1024) after = os.lstat(path) identity = lambda s: (s.st_dev, s.st_ino, s.st_mode, s.st_mtime_ns, s.st_ctime_ns) require(identity(before) == identity(after) and os.path.realpath(path) == resolved, 'runner_link_changed') return content, inode def parse_closure_metadata(content, expected_pin): # Match production parseNativeAcpxDistributionEntries: the profile pins # canonical entries, not the formatting or outer JSON file bytes. manifest = json.loads(content) entries = manifest.get('entries') if isinstance(manifest, dict) else None require(isinstance(entries, list) and 0 < len(entries) <= 30000, 'closure_inventory') normalized, previous, total = [], '', 0 for entry in entries: require(isinstance(entry, dict) and set(entry) == {'path', 'sha256', 'size', 'executable'}, 'closure_entry_shape') path = entry['path'] require(isinstance(path, str) and 0 < len(path) <= 4096 and not os.path.isabs(path) and not re.search(r'[\x00-\x1f\x7f\\]', path) and all(part not in ('', '.', '..') for part in path.split('/')) and path > previous and path != 'manifest.json' and not path.startswith('.paperclip-'), 'closure_entry_path') require(isinstance(entry['sha256'], str) and re.fullmatch(r'[a-f0-9]{64}', entry['sha256']) and type(entry['size']) is int and 0 <= entry['size'] <= 384 * 1024 * 1024 and type(entry['executable']) is bool, 'closure_entry_metadata') total += entry['size']; require(total <= 1024 * 1024 * 1024, 'closure_tree_bound') previous = path normalized.append({key: entry[key] for key in ('path', 'sha256', 'size', 'executable')}) canonical = json.dumps(normalized, separators=(',', ':'), ensure_ascii=False).encode('utf8') require(re.fullmatch(r'[a-f0-9]{64}', expected_pin) and digest(canonical) == expected_pin, 'closure_pin') return normalized def inspect(config): require(set(config) == {'root', 'binding', 'runtimeEnvironmentLeaseId', 'runnerdSha256', 'closureSha256'}, 'config_keys') binding, expected = config['binding'], config['root'] boot = Path('/proc/sys/kernel/random/boot_id').read_text().strip() require(re.fullmatch(r'[a-f0-9-]{36}', boot) and boot == expected['bootId'], 'boot_changed') root = proc(expected['pid'], boot) require(root == expected and os.getpgid(root['pid']) == root['pid'], 'root_changed') root_args = argv(root['pid']) runtime_root = binding['remoteCwd'] + '/.paperclip-runtime/paperclip-runner' runner = runtime_root + '/bin/paperclip-runnerd' require(root_args[0] == runner and flag(root_args, '--run-id') == binding['runId'] and flag(root_args, '--environment-lease-id') == config['runtimeEnvironmentLeaseId'] and flag(root_args, '--lifecycle-mode') == 'per_turn', 'root_binding') require(re.fullmatch(re.escape(runtime_root) + r'/sessions/[a-f0-9]{64}/runner', flag(root_args, '--state-dir')), 'root_session') runner_bytes, runner_inode = checked_runner_executable(runner) executable = os.stat(f'/proc/{root["pid"]}/exe') require((executable.st_dev, executable.st_ino) == runner_inode and 'sha256:' + digest(runner_bytes) == config['runnerdSha256'], 'runner_executable') closure_bytes, _ = checked_file(PACK + '/provider-assets/pi/linux-x64/native-closure.json', 4 * 1024 * 1024) admitted_entries = parse_closure_metadata(closure_bytes, config['closureSha256']) entries = {e['path']: e for e in admitted_entries} for name in (NODE, ENTRY, EXTENSION, 'pi-entry.cjs', 'node_modules/pi-acp/dist/index.js', 'node_modules/pi-acp/dist/paperclip-runtime.js'): require(name in entries and re.fullmatch(r'[a-f0-9]{64}', entries[name]['sha256']), 'closure_entry') pids = [int(p) for p in os.listdir('/proc') if p.isdigit() and int(p) > 1] require(len(pids) <= 4096, 'process_bound') table = {} for pid in pids: try: table[pid] = proc(pid, boot) except (FileNotFoundError, ProcessLookupError): continue except RuntimeError as error: if str(error).endswith(':dead_process'): continue raise def ancestry(pid): chain = [] while pid != root['pid']: require(pid in table and len(chain) < 64 and pid not in [p['pid'] for p in chain], 'ancestry') chain.append(table[pid]); pid = table[pid]['ppid'] return chain + [root] matches = [] for pid in table: try: chain = ancestry(pid) except RuntimeError: continue parent = table[pid]['ppid'] if parent not in table: continue parent_args = argv(parent) if len(parent_args) != 4 or not parent_args[3].endswith('/distribution/pi-entry.cjs'): continue distribution = parent_args[3][:-len('/pi-entry.cjs')] require(re.fullmatch(r'/tmp/paperclip-acpx-native-[A-Za-z0-9_-]+/distribution', distribution), 'snapshot_path') require(parent_args[1:] == ['--require', distribution + '/' + GUARD, distribution + '/pi-entry.cjs'], 'wrapper_parent') descriptor = None if parent_args[0] != distribution + '/' + NODE: # Production nativeBootstrap executes the held Node through child # FD 3 (unfenced) or 7 (guardian/credential fences). The child's # cmdline retains /proc/self/fd/N; it does not name the source path. match = re.fullmatch(r'/proc/self/fd/(3|7)', parent_args[0]) require(match is not None, 'wrapper_parent') descriptor = f'/proc/{parent}/fd/{match.group(1)}' # Pi sets process.title and overwrites Linux argv. The exact pinned # parent's launch code attests the entrypoint; title alone never selects. require(argv(pid) == ['pi'], 'pi_process_title') for directory in (distribution, os.path.dirname(distribution)): st = os.lstat(directory) require(stat.S_ISDIR(st.st_mode) and not st.st_mode & 0o222 and os.path.realpath(directory) == directory, 'snapshot_seal') require(os.readlink(f'/proc/{pid}/cwd') == binding['remoteCwd'], 'cwd') identities = {} for name in (NODE, ENTRY, EXTENSION, 'pi-entry.cjs', 'node_modules/pi-acp/dist/index.js', 'node_modules/pi-acp/dist/paperclip-runtime.js'): content, inode = checked_file(distribution + '/' + name, 256 * 1024 * 1024, True) require(len(content) == entries[name]['size'] and digest(content) == entries[name]['sha256'], 'snapshot_digest') identities[name] = inode exe = os.stat(f'/proc/{pid}/exe') require((exe.st_dev, exe.st_ino) == identities[NODE], 'pi_executable') parent_exe = os.stat(f'/proc/{parent}/exe') require((parent_exe.st_dev, parent_exe.st_ino) == identities[NODE], 'wrapper_executable') if descriptor is not None: held_exe = os.stat(descriptor) require((held_exe.st_dev, held_exe.st_ino) == identities[NODE], 'wrapper_descriptor') for identity in chain: require(proc(identity['pid'], boot) == identity, 'ancestry_changed') matches.append({'target': table[pid], 'ancestry': chain, 'nodeSha256': 'sha256:' + entries[NODE]['sha256'], 'entrypointSha256': 'sha256:' + entries[ENTRY]['sha256'], 'closureSha256': config['closureSha256'], 'entrypointAttribution': 'pinned_wrapper_parent', 'originalChildArgvAvailable': False, 'observedChildTitle': 'pi'}) require(len(matches) == 1, 'unique_pi_child') return matches[0] def terminate(config): require(sys.platform == 'linux' and hasattr(os, 'pidfd_open') and hasattr(signal, 'pidfd_send_signal'), 'pidfd_required') before = inspect(config) pid = before['target']['pid'] require(pid != os.getpid() and pid != config['root']['pid'], 'not_worker') fd = os.pidfd_open(pid) try: require(inspect(config) == before, 'identity_changed_after_pidfd') fields = dict(line.split(':', 1) for line in Path(f'/proc/self/fdinfo/{fd}').read_text().splitlines() if ':' in line) require(int(fields.get('Pid', '-1').strip()) == pid, 'pidfd_retired') signal.pidfd_send_signal(fd, signal.SIGKILL, None, 0) return {'schema': 'paperclip.e2e.pi-provider-death.v1', 'binding': config['binding'], 'runtimeEnvironmentLeaseId': config['runtimeEnvironmentLeaseId'], **before, 'signalled': True, 'signal': 'SIGKILL', 'targetKind': 'pi_native_child', 'workerSignalled': False} finally: os.close(fd) if __name__ == '__main__': try: require(len(sys.argv) == 2 and len(sys.argv[1]) <= 8192, 'input_bound') print(json.dumps(terminate(json.loads(base64.b64decode(sys.argv[1], validate=True))), separators=(',', ':'))) except Exception: # Never print process arguments, filesystem paths, or inherited context. print(json.dumps({'ok': False, 'code': 'pi_provider_identity_or_signal_failed'})) sys.exit(1)