mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-10 20:50:08 +02:00
Admit stable verified runner links in Pi provider-death fixture
This commit is contained in:
1 parent
660c6f63f6
commit
bfd0292f5e
5 files changed
+79
-6
No files matched your search
@@ -386,6 +386,17 @@ or `/proc/self/fd/7` in the wrapper argv. That form is admitted only when the
|
||||
wrapper's corresponding descriptor and executable both have the exact sealed
|
||||
snapshot Node inode. The guard and wrapper entrypoint paths remain exact.
|
||||
Missing, foreign or other descriptor numbers fail before signalling.
|
||||
Production may also stage the runner executable as a link to the image's
|
||||
verified installation. The fault helper reads the resolved regular file, checks
|
||||
that the named link remained unchanged, and still requires its pinned hash and
|
||||
exact `/proc/<runner-pid>/exe` inode. Link replacement, missing targets and a
|
||||
different executable fail admission. Linux calibration covers this installation
|
||||
form as well as a copied runner.
|
||||
|
||||
The agent-memory fixture treats its quoted JSON string as UTF-8 file content,
|
||||
including a final line-feed byte (`0x0A`). Native readback and the managed-file
|
||||
API must retain those exact bytes across a new task and controller restart.
|
||||
The prompt makes that decoding explicit; the byte graders remain unchanged.
|
||||
|
||||
The runtime itself must emit `runtime_request.expired` for the original callback
|
||||
with `provider_process_lost` and `replayAllowed:false`, followed by native turn
|
||||
|
||||
@@ -1114,7 +1114,7 @@ export const runnerSuites: readonly RunnerSuiteFixture[] = [
|
||||
groups: ["native"], profiles: extendedHarnessProfiles.filter(profile => profile.qualificationCandidate === "pi"),
|
||||
environments: runnerEnvironments, tasks: piNativeTasks, expectedMatrixSize: 10,
|
||||
excludedExecutionIds: ["pi-native.runner-acpx-pi.daytona.restrictive-denial", "pi-native.runner-acpx-pi.local.native-pending-provider-death"],
|
||||
definitionMetadata: { version: 7, qualification: "pending", scheduling: "explicit-only", profileVersion: QUALIFIED_ACPX_PROFILES.pi.agentProfileVersion, agentMemoryContent: "utf8-nonce-plus-final-lf", providerDeath: "daytona-exact-pi-child-pidfd-production-expiry", providerFaultExecutable: "snapshot-node-inode-with-held-bootstrap-fd-3-or-7", remoteBootstrapApproval: "exact-published-native-read-public-accept-once-v1", remoteDenyAll: "unsupported-native-bootstrap-read-is-denied", remoteEvidence: "owned-lease-sealed-observer", pendingControllerRestart: "same-live-native-request" },
|
||||
definitionMetadata: { version: 8, qualification: "pending", scheduling: "explicit-only", profileVersion: QUALIFIED_ACPX_PROFILES.pi.agentProfileVersion, agentMemoryContent: "utf8-nonce-plus-final-lf", providerDeath: "daytona-exact-pi-child-pidfd-production-expiry", providerFaultExecutable: "stable-preinstalled-runner-link-and-snapshot-node-inode-with-held-bootstrap-fd-3-or-7", remoteBootstrapApproval: "exact-published-native-read-public-accept-once-v1", remoteDenyAll: "unsupported-native-bootstrap-read-is-denied", remoteEvidence: "owned-lease-sealed-observer", pendingControllerRestart: "same-live-native-request" },
|
||||
},
|
||||
{
|
||||
id: "native-active-stop", label: "Stop an unanswered native permission", manualOnly: true,
|
||||
|
||||
@@ -72,6 +72,20 @@ 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.
|
||||
@@ -111,7 +125,7 @@ def inspect(config):
|
||||
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_file(runner, 256 * 1024 * 1024)
|
||||
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)
|
||||
|
||||
@@ -98,6 +98,7 @@ class IdentityTests(unittest.TestCase):
|
||||
self.args = {21: [RUNTIME + '/bin/paperclip-runnerd', '--run-id', 'run', '--environment-lease-id', 'workspace-id', '--lifecycle-mode', 'per_turn', '--state-dir', RUNTIME + '/sessions/' + 'a' * 64 + '/runner'],
|
||||
22: [DIST + '/' + fault.NODE, '--require', DIST + '/' + fault.GUARD, DIST + '/pi-entry.cjs'], 23: ['pi']}
|
||||
self.patches = [patch.object(fault, 'proc', side_effect=lambda pid, boot: self.table[pid]), patch.object(fault, 'argv', side_effect=lambda pid: self.args[pid]),
|
||||
patch.object(fault, 'checked_runner_executable', side_effect=lambda p: self.files[p]),
|
||||
patch.object(fault, 'checked_file', side_effect=lambda p, *a: self.files[p]), patch.object(Path, 'read_text', return_value=BOOT),
|
||||
patch.object(os, 'listdir', return_value=['21', '22', '23']), patch.object(os, 'getpgid', return_value=21),
|
||||
patch.object(os, 'lstat', return_value=SimpleNamespace(st_mode=0o40500)), patch.object(os.path, 'realpath', side_effect=lambda p: p),
|
||||
@@ -158,6 +159,43 @@ class IdentityTests(unittest.TestCase):
|
||||
with patch.object(os, 'listdir', return_value=['21', '22', '23', '24']), self.assertRaisesRegex(RuntimeError, 'unique_pi_child'): fault.inspect(self.config)
|
||||
|
||||
|
||||
class RunnerExecutableTests(unittest.TestCase):
|
||||
def test_regular_and_preinstalled_link_resolve_to_the_same_inode(self):
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
executable = Path(tmp).resolve() / 'installed-runner'
|
||||
executable.write_bytes(b'pinned runner')
|
||||
link = executable.with_name('runtime-runner')
|
||||
link.symlink_to(executable)
|
||||
content, inode = fault.checked_runner_executable(str(link))
|
||||
self.assertEqual(content, b'pinned runner')
|
||||
self.assertEqual((content, inode), fault.checked_runner_executable(str(executable)))
|
||||
|
||||
def test_retargeted_link_is_rejected_after_read(self):
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
executable = Path(tmp).resolve() / 'installed-runner'
|
||||
executable.write_bytes(b'pinned runner')
|
||||
foreign = executable.with_name('foreign-runner')
|
||||
foreign.write_bytes(b'foreign')
|
||||
link = executable.with_name('runtime-runner')
|
||||
link.symlink_to(executable)
|
||||
original = fault.checked_file
|
||||
def replace_during_read(*args):
|
||||
result = original(*args)
|
||||
link.unlink()
|
||||
link.symlink_to(foreign)
|
||||
return result
|
||||
with patch.object(fault, 'checked_file', side_effect=replace_during_read), self.assertRaisesRegex(RuntimeError, 'runner_link_changed'):
|
||||
fault.checked_runner_executable(str(link))
|
||||
|
||||
def test_missing_link_and_directory_never_admit_an_executable(self):
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
root = Path(tmp).resolve()
|
||||
link = root / 'runtime-runner'
|
||||
link.symlink_to(root / 'missing')
|
||||
with self.assertRaises(FileNotFoundError): fault.checked_runner_executable(str(link))
|
||||
with self.assertRaisesRegex(RuntimeError, 'runner_file_shape'): fault.checked_runner_executable(str(root))
|
||||
|
||||
|
||||
class PidfdTests(unittest.TestCase):
|
||||
def test_changed_or_retired_pidfd_never_signals_and_always_closes(self):
|
||||
before = {'target': TARGET}
|
||||
@@ -186,7 +224,11 @@ class PidfdTests(unittest.TestCase):
|
||||
for descriptor in [3, 7]:
|
||||
with self.subTest(descriptor=descriptor): self.calibrate_real_child(descriptor)
|
||||
|
||||
def calibrate_real_child(self, descriptor=None):
|
||||
@unittest.skipUnless(sys.platform == 'linux' and hasattr(os, 'pidfd_open'), 'Hosted native Linux preinstalled-link calibration required')
|
||||
def test_real_preinstalled_runner_link_then_exact_child_pidfd(self):
|
||||
self.calibrate_real_child(7, runner_link=True)
|
||||
|
||||
def calibrate_real_child(self, descriptor=None, runner_link=False):
|
||||
node = shutil.which('node'); self.assertIsNotNone(node)
|
||||
base = Path(tempfile.mkdtemp(prefix='pi-fault-calibration-', dir='/tmp'))
|
||||
snapshot = Path(tempfile.mkdtemp(prefix='paperclip-acpx-native-', dir='/tmp'))
|
||||
@@ -199,7 +241,13 @@ class PidfdTests(unittest.TestCase):
|
||||
for name in NAMES + [fault.GUARD]:
|
||||
p = distribution / name; p.parent.mkdir(parents=True, exist_ok=True); p.write_text('// fixture\n')
|
||||
shutil.copyfile(node, distribution / fault.NODE); (distribution / fault.NODE).chmod(0o500)
|
||||
shutil.copyfile(node, runtime / 'bin/paperclip-runnerd'); (runtime / 'bin/paperclip-runnerd').chmod(0o500)
|
||||
runner = runtime / 'bin/paperclip-runnerd'
|
||||
if runner_link:
|
||||
installed = base / 'preinstalled-runner'
|
||||
shutil.copyfile(node, installed); installed.chmod(0o500)
|
||||
runner.symlink_to(installed)
|
||||
else:
|
||||
shutil.copyfile(node, runner); runner.chmod(0o500)
|
||||
ready = workspace / 'child.json'; exited = workspace / 'exit.json'
|
||||
(distribution / fault.ENTRY).write_text('process.title="pi"; require("node:fs").writeFileSync(' + json.dumps(str(ready)) + ',JSON.stringify({pid:process.pid}));setInterval(()=>{},1000);')
|
||||
(distribution / 'pi-entry.cjs').write_text('const p=require("node:child_process").spawn(process.execPath,["--require",' + json.dumps(str(distribution / fault.GUARD)) + ',' + json.dumps(str(distribution / fault.ENTRY)) + '],{stdio:"ignore"});p.on("exit",(code,signal)=>require("node:fs").writeFileSync(' + json.dumps(str(exited)) + ',JSON.stringify({code,signal})));process.on("SIGTERM",()=>{if(p.exitCode!==null||p.signalCode!==null)process.exit(0);p.once("exit",()=>process.exit(0));p.kill("SIGTERM")});setInterval(()=>{},1000);')
|
||||
|
||||
@@ -15,8 +15,8 @@ it("calibrates the exact Pi fault helper with metadata negatives and native Linu
|
||||
expect(result.error).toBeUndefined();
|
||||
expect(result.signal).toBeNull();
|
||||
expect(result.status, result.stderr).toBe(0);
|
||||
expect(result.stderr).toContain("Ran 17 tests");
|
||||
expect(result.stderr).toContain("Ran 21 tests");
|
||||
if (process.platform === "linux") expect(result.stderr).not.toContain("skipped");
|
||||
else expect(result.stderr).toContain("skipped=2");
|
||||
else expect(result.stderr).toContain("skipped=3");
|
||||
console.info(result.stderr.trim());
|
||||
}, 30_000);
|
||||
Reference in new issue
Block a user