diff --git a/packages/paperclip-runner/runner/crates/runner-core/src/bin/paperclip-runnerd.rs b/packages/paperclip-runner/runner/crates/runner-core/src/bin/paperclip-runnerd.rs
index 1241f1c5843..6d5ffa68fbf 100644
--- a/packages/paperclip-runner/runner/crates/runner-core/src/bin/paperclip-runnerd.rs
+++ b/packages/paperclip-runner/runner/crates/runner-core/src/bin/paperclip-runnerd.rs
@@ -559,6 +559,23 @@ fn apply_oom_score_adj(args: &[String]) -> Result<(), LocalRunnerError> {
Ok(())
}
+/// `--trusted-launch-root
`: the host asserts that `` is an
+/// immutable, digest-pinned image (the agent computer's Runner ROM). Launch
+/// artifacts the root admits run in place instead of from a sealed copy. A root
+/// that fails its startup checks only loses that speedup, never protection.
+fn apply_trusted_launch_root(args: &[String]) -> Result<(), LocalRunnerError> {
+ let Some(path) = optional_value(args, "--trusted-launch-root")? else {
+ return Ok(());
+ };
+ match paperclip_runner_core::qualified_launch::TrustedLaunchRoot::open(Path::new(&path)) {
+ Ok(root) => paperclip_runner_core::qualified_launch::set_trusted_launch_root(root),
+ Err(reason) => eprintln!(
+ "paperclip-runnerd: ignoring --trusted-launch-root {path}: {reason}; launch artifacts keep sealed snapshots"
+ ),
+ }
+ Ok(())
+}
+
fn apply_task_memory_cgroup(args: &[String]) -> Result<(), LocalRunnerError> {
if let Some(path) = optional_value(args, "--task-memory-cgroup")? {
paperclip_runner_core::task_memory::set_task_cgroup(PathBuf::from(path));
@@ -640,6 +657,7 @@ fn run(args: &[String]) -> Result<(), LocalRunnerError> {
}
apply_oom_score_adj(args)?;
apply_task_memory_cgroup(args)?;
+ apply_trusted_launch_root(args)?;
if args.iter().any(|argument| argument == "--connect-url")
|| args.iter().any(|argument| argument == "--listen-address")
|| args.iter().any(|argument| argument == "--listen-port")
diff --git a/packages/paperclip-runner/runner/crates/runner-core/src/process_supervisor.rs b/packages/paperclip-runner/runner/crates/runner-core/src/process_supervisor.rs
index 64d27043b29..e479e5247e9 100644
--- a/packages/paperclip-runner/runner/crates/runner-core/src/process_supervisor.rs
+++ b/packages/paperclip-runner/runner/crates/runner-core/src/process_supervisor.rs
@@ -102,6 +102,26 @@ impl VerifiedProcessArtifact {
#[cfg(not(target_os = "macos"))]
Self::snapshot_verified(display_path, file, expected_sha256)
}
+
+ /// Hold a launch artifact open in place instead of snapshotting it.
+ ///
+ /// Only `qualified_launch` calls this, and only after the trusted launch
+ /// root proved that this open descriptor itself lies on an immutable
+ /// mount under that root. The process then executes this descriptor
+ /// through `/proc/self/fd/N`, so a later path swap cannot change what
+ /// runs and nested launches reopen the same file.
+ #[cfg(target_os = "linux")]
+ pub(crate) fn in_place_on_trusted_root(display_path: PathBuf, file: File) -> Self {
+ Self {
+ display_path,
+ file: Arc::new(file),
+ }
+ }
+
+ #[cfg(test)]
+ pub(crate) fn held_file(&self) -> &File {
+ &self.file
+ }
}
#[cfg(target_os = "macos")]
@@ -2013,6 +2033,52 @@ mod tests {
fs::remove_dir_all(directory).unwrap();
}
+ #[cfg(target_os = "linux")]
+ #[test]
+ fn trusted_root_artifacts_execute_the_held_file_and_reopen_it_for_nested_launches() {
+ let nonce = SystemTime::now()
+ .duration_since(UNIX_EPOCH)
+ .unwrap()
+ .as_nanos();
+ let directory = std::env::temp_dir().join(format!(
+ "paperclip-trusted-root-launch-{}-{nonce}",
+ std::process::id()
+ ));
+ fs::create_dir_all(&directory).unwrap();
+ let shell = fs::canonicalize(directory.join(".")).unwrap().join("sh");
+ fs::copy(fs::canonicalize("/bin/sh").unwrap(), &shell).unwrap();
+ let program = VerifiedProcessArtifact::in_place_on_trusted_root(
+ shell.clone(),
+ File::open(&shell).unwrap(),
+ );
+ let launch = VerifiedProcessLaunch::new(
+ program,
+ vec![
+ VerifiedProcessArgument::Literal("-c".to_owned()),
+ VerifiedProcessArgument::Literal(format!(
+ "readlink /proc/$$/exe; \"${VERIFIED_RUNTIME_EXECUTABLE_ENV}\" -c 'readlink /proc/$$/exe'"
+ )),
+ ],
+ );
+
+ let inherited = launch.inherited_command().unwrap();
+ assert!(inherited.program.starts_with("/proc/self/fd/"));
+ let output = Command::new(&inherited.program)
+ .args(&inherited.args)
+ .env(VERIFIED_RUNTIME_EXECUTABLE_ENV, &inherited.program)
+ .output()
+ .unwrap();
+ drop(inherited);
+
+ assert!(output.status.success(), "{output:?}");
+ let expected = format!("{}\n", shell.display());
+ assert_eq!(
+ String::from_utf8(output.stdout).unwrap(),
+ expected.repeat(2)
+ );
+ fs::remove_dir_all(directory).unwrap();
+ }
+
#[cfg(target_os = "macos")]
#[test]
fn executable_arguments_use_the_private_nested_launch_contract() {
diff --git a/packages/paperclip-runner/runner/crates/runner-core/src/qualified_launch.rs b/packages/paperclip-runner/runner/crates/runner-core/src/qualified_launch.rs
index 570080ff877..c65f07b3379 100644
--- a/packages/paperclip-runner/runner/crates/runner-core/src/qualified_launch.rs
+++ b/packages/paperclip-runner/runner/crates/runner-core/src/qualified_launch.rs
@@ -1,4 +1,6 @@
use std::fs::{self, File};
+use std::path::{Path, PathBuf};
+use std::sync::OnceLock;
#[cfg(unix)]
use std::os::unix::fs::{MetadataExt, PermissionsExt};
@@ -6,24 +8,210 @@ use std::os::unix::fs::{MetadataExt, PermissionsExt};
use crate::durable::{DurableRunnerError, QualifiedLaunchArtifact};
use crate::process_supervisor::VerifiedProcessArtifact;
+static TRUSTED_LAUNCH_ROOT: OnceLock = OnceLock::new();
+
+/// Set once at startup from `--trusted-launch-root`. Without it, every launch
+/// artifact is copied into a sealed snapshot and hashed, as before.
+pub fn set_trusted_launch_root(root: TrustedLaunchRoot) {
+ let _ = TRUSTED_LAUNCH_ROOT.set(root);
+}
+
+/// A directory the host asserts is an immutable, digest-pinned image, such as
+/// the agent computer's Runner ROM. Runner-owned launch artifacts whose open
+/// descriptor lies on that image are executed in place: no sealed copy and no
+/// per-launch digest. Anything the root does not admit keeps the sealed copy.
+///
+/// The assertion is explicit and path-scoped. Admission still proves, from the
+/// open descriptor, that the file is inside the root and on a read-only,
+/// exec-permitted mount of a filesystem that cannot be written at all (erofs or
+/// squashfs). A misconfigured root therefore only loses the speedup.
+#[derive(Debug)]
+pub struct TrustedLaunchRoot {
+ path: PathBuf,
+ #[cfg(target_os = "linux")]
+ device: u64,
+ #[cfg(target_os = "linux")]
+ immutable_mount: fn(&File) -> std::io::Result,
+}
+
+impl TrustedLaunchRoot {
+ /// Accept a root only when it is absolute, already canonical (no symlink
+ /// or `..` component) and itself on an immutable mount.
+ pub fn open(path: &Path) -> Result {
+ #[cfg(target_os = "linux")]
+ {
+ Self::open_with_mount_check(path, immutable_exec_mount)
+ }
+ #[cfg(not(target_os = "linux"))]
+ {
+ let _ = path;
+ Err("trusted launch roots are supported only on Linux".to_owned())
+ }
+ }
+
+ #[cfg(target_os = "linux")]
+ fn open_with_mount_check(
+ path: &Path,
+ immutable_mount: fn(&File) -> std::io::Result,
+ ) -> Result {
+ if !path.is_absolute() {
+ return Err("trusted launch root must be an absolute path".to_owned());
+ }
+ let canonical = fs::canonicalize(path)
+ .map_err(|error| format!("trusted launch root cannot be resolved: {error}"))?;
+ if canonical != path {
+ return Err("trusted launch root must not contain symlinks or `..`".to_owned());
+ }
+ let directory = File::open(&canonical)
+ .map_err(|error| format!("trusted launch root cannot be opened: {error}"))?;
+ let metadata = directory
+ .metadata()
+ .map_err(|error| format!("trusted launch root cannot be inspected: {error}"))?;
+ if !metadata.is_dir() {
+ return Err("trusted launch root must be a directory".to_owned());
+ }
+ if !immutable_mount(&directory).unwrap_or(false) {
+ return Err(
+ "trusted launch root is not on a read-only, exec-permitted erofs or squashfs mount"
+ .to_owned(),
+ );
+ }
+ Ok(Self {
+ path: canonical,
+ device: metadata.dev(),
+ immutable_mount,
+ })
+ }
+
+ pub fn path(&self) -> &Path {
+ &self.path
+ }
+
+ /// Decide from the open descriptor alone, never from the name it was
+ /// opened by: the kernel's path for the descriptor must lie under the
+ /// root, on the root's filesystem, and that mount must be immutable.
+ #[cfg(target_os = "linux")]
+ fn admits(&self, file: &File) -> Result<(), &'static str> {
+ use std::os::fd::AsRawFd;
+
+ let metadata = file.metadata().map_err(|_| "cannot inspect descriptor")?;
+ if !metadata.is_file() {
+ return Err("not a regular file");
+ }
+ if metadata.dev() != self.device {
+ return Err("not on the trusted root's filesystem");
+ }
+ let held = fs::read_link(format!("/proc/self/fd/{}", file.as_raw_fd()))
+ .map_err(|_| "cannot resolve descriptor")?;
+ if !held.is_absolute()
+ || held
+ .components()
+ .any(|component| matches!(component, std::path::Component::ParentDir))
+ || !held.starts_with(&self.path)
+ || held == self.path
+ {
+ return Err("outside the trusted root");
+ }
+ if !(self.immutable_mount)(file).unwrap_or(false) {
+ return Err("not on a read-only, exec-permitted erofs or squashfs mount");
+ }
+ Ok(())
+ }
+}
+
+/// erofs and squashfs have no write path at all, so a read-only mount of one
+/// cannot change underneath an open descriptor, not even through another
+/// mount of the same superblock.
+#[cfg(target_os = "linux")]
+fn immutable_exec_mount(file: &File) -> std::io::Result {
+ use rustix::fs::StatVfsMountFlags;
+ const EROFS_SUPER_MAGIC_V1: u32 = 0xE0F5_E1E2;
+ const SQUASHFS_MAGIC: u32 = 0x7371_7368;
+
+ let mount = rustix::fs::fstatvfs(file)?;
+ if !mount.f_flag.contains(StatVfsMountFlags::RDONLY)
+ || mount.f_flag.contains(StatVfsMountFlags::NOEXEC)
+ {
+ return Ok(false);
+ }
+ // `f_type` is signed or unsigned, 32 or 64 bits, by architecture. The
+ // magic numbers are 32-bit, so compare the low word.
+ #[allow(clippy::unnecessary_cast)]
+ let filesystem = rustix::fs::fstatfs(file)?.f_type as u32;
+ Ok(matches!(filesystem, EROFS_SUPER_MAGIC_V1 | SQUASHFS_MAGIC))
+}
+
+/// Who chose an artifact. Only Runner-owned launch-profile artifacts may use
+/// the trusted launch root.
+#[derive(Clone, Copy)]
+enum ArtifactOwner {
+ Runner,
+ HostAssigned,
+}
+
+/// A Runner-owned artifact (provider runtime, sidecar or proxy) pinned by the
+/// launch profile. It may run in place from the trusted launch root.
pub fn verify_launch_artifact(
artifact: &QualifiedLaunchArtifact,
label: &str,
) -> Result {
- verify_launch_artifact_with_role(artifact, label, false)
+ verify_owned(
+ artifact,
+ label,
+ false,
+ ArtifactOwner::Runner,
+ TRUSTED_LAUNCH_ROOT.get(),
+ )
}
+/// The executable counterpart of [`verify_launch_artifact`].
pub fn verify_executable_launch_artifact(
artifact: &QualifiedLaunchArtifact,
label: &str,
) -> Result {
- verify_launch_artifact_with_role(artifact, label, true)
+ verify_owned(
+ artifact,
+ label,
+ true,
+ ArtifactOwner::Runner,
+ TRUSTED_LAUNCH_ROOT.get(),
+ )
+}
+
+/// A host-assigned executable, such as a stdio MCP server. It is always
+/// snapshotted and hashed, whatever the trusted launch root.
+pub fn verify_assigned_executable_launch_artifact(
+ artifact: &QualifiedLaunchArtifact,
+ label: &str,
+) -> Result {
+ verify_owned(
+ artifact,
+ label,
+ true,
+ ArtifactOwner::HostAssigned,
+ TRUSTED_LAUNCH_ROOT.get(),
+ )
+}
+
+fn verify_owned(
+ artifact: &QualifiedLaunchArtifact,
+ label: &str,
+ executable: bool,
+ owner: ArtifactOwner,
+ configured_root: Option<&TrustedLaunchRoot>,
+) -> Result {
+ let trusted_root = match owner {
+ ArtifactOwner::Runner => configured_root,
+ ArtifactOwner::HostAssigned => None,
+ };
+ verify_launch_artifact_with_role(artifact, label, executable, trusted_root)
}
fn verify_launch_artifact_with_role(
artifact: &QualifiedLaunchArtifact,
label: &str,
executable: bool,
+ trusted_root: Option<&TrustedLaunchRoot>,
) -> Result {
let source_metadata = fs::symlink_metadata(&artifact.path).map_err(|error| {
DurableRunnerError::invalid(format!("failed to inspect qualified {label}: {error}"))
@@ -64,6 +252,24 @@ fn verify_launch_artifact_with_role(
"qualified {label} changed while it was opened"
)));
}
+ #[cfg(target_os = "linux")]
+ if let Some(root) = trusted_root {
+ match root.admits(&file) {
+ // The image is immutable and pinned by digest where it is
+ // deployed, so re-hashing it here could only repeat that check.
+ Ok(()) => {
+ return Ok(VerifiedProcessArtifact::in_place_on_trusted_root(
+ canonical, file,
+ ))
+ }
+ Err(reason) => eprintln!(
+ "paperclip-runnerd: qualified {label} is not admitted by trusted launch root {} ({reason}); using a sealed snapshot",
+ root.path.display()
+ ),
+ }
+ }
+ #[cfg(not(target_os = "linux"))]
+ let _ = trusted_root;
let snapshot = if executable {
VerifiedProcessArtifact::snapshot_verified_executable(canonical, file, &artifact.sha256)
} else {
@@ -87,3 +293,257 @@ fn same_file(left: &fs::Metadata, right: &fs::Metadata) -> bool {
&& left.modified().ok() == right.modified().ok()
&& left.is_file() == right.is_file()
}
+
+#[cfg(all(test, target_os = "linux"))]
+mod tests {
+ use super::*;
+ use sha2::{Digest, Sha256};
+ use std::time::{SystemTime, UNIX_EPOCH};
+
+ // A writable temporary directory stands in for the ROM. These checks
+ // replace only the mount-type probe; path, device and descriptor checks
+ // stay real.
+ fn immutable(_: &File) -> std::io::Result {
+ Ok(true)
+ }
+
+ fn writable(_: &File) -> std::io::Result {
+ Ok(false)
+ }
+
+ struct Fixture {
+ root: PathBuf,
+ }
+
+ impl Fixture {
+ fn new(name: &str) -> Self {
+ let root = std::env::temp_dir().join(format!(
+ "paperclip-trusted-launch-root-{name}-{}-{}",
+ std::process::id(),
+ SystemTime::now()
+ .duration_since(UNIX_EPOCH)
+ .unwrap()
+ .as_nanos()
+ ));
+ fs::create_dir_all(root.join("rom/libexec")).unwrap();
+ fs::create_dir_all(root.join("outside")).unwrap();
+ Self {
+ root: fs::canonicalize(root).unwrap(),
+ }
+ }
+
+ fn rom(&self) -> PathBuf {
+ self.root.join("rom")
+ }
+
+ fn artifact(&self, path: PathBuf, bytes: &[u8]) -> QualifiedLaunchArtifact {
+ fs::write(&path, bytes).unwrap();
+ fs::set_permissions(&path, fs::Permissions::from_mode(0o755)).unwrap();
+ QualifiedLaunchArtifact {
+ path,
+ sha256: format!("sha256:{:x}", Sha256::digest(bytes)),
+ }
+ }
+ }
+
+ impl Drop for Fixture {
+ fn drop(&mut self) {
+ let _ = fs::remove_dir_all(&self.root);
+ }
+ }
+
+ fn sealed(artifact: &VerifiedProcessArtifact) -> bool {
+ let file = artifact.held_file();
+ let link = fs::read_link(format!(
+ "/proc/self/fd/{}",
+ std::os::fd::AsRawFd::as_raw_fd(file)
+ ))
+ .unwrap();
+ file.metadata().unwrap().nlink() == 0
+ && link
+ .to_string_lossy()
+ .starts_with("/memfd:paperclip-verified-launch")
+ }
+
+ fn same_inode(artifact: &VerifiedProcessArtifact, path: &Path) -> bool {
+ let held = artifact.held_file().metadata().unwrap();
+ let named = fs::metadata(path).unwrap();
+ held.dev() == named.dev() && held.ino() == named.ino()
+ }
+
+ #[test]
+ fn an_artifact_under_an_immutable_trusted_root_runs_in_place_without_a_digest() {
+ let fixture = Fixture::new("hit");
+ let root = TrustedLaunchRoot::open_with_mount_check(&fixture.rom(), immutable).unwrap();
+ let mut artifact = fixture.artifact(fixture.rom().join("libexec/node"), b"node");
+ // The trusted path does not re-hash: a wrong expected digest still
+ // admits, because the image itself is the pinned authority.
+ artifact.sha256 = "sha256:not-rehashed".to_owned();
+
+ let verified =
+ verify_launch_artifact_with_role(&artifact, "test", true, Some(&root)).unwrap();
+
+ assert!(!sealed(&verified));
+ assert!(same_inode(&verified, &artifact.path));
+ }
+
+ #[test]
+ fn an_artifact_outside_the_trusted_root_keeps_the_sealed_snapshot() {
+ let fixture = Fixture::new("outside");
+ let root = TrustedLaunchRoot::open_with_mount_check(&fixture.rom(), immutable).unwrap();
+ let artifact = fixture.artifact(fixture.root.join("outside/node"), b"node");
+
+ let verified =
+ verify_launch_artifact_with_role(&artifact, "test", true, Some(&root)).unwrap();
+ assert!(sealed(&verified));
+
+ let mut tampered = artifact.clone();
+ tampered.sha256 = "sha256:wrong".to_owned();
+ assert!(verify_launch_artifact_with_role(&tampered, "test", true, Some(&root)).is_err());
+ }
+
+ #[test]
+ fn a_sibling_with_the_root_as_a_name_prefix_is_outside_the_root() {
+ let fixture = Fixture::new("prefix");
+ fs::create_dir_all(fixture.root.join("rom2")).unwrap();
+ let root = TrustedLaunchRoot::open_with_mount_check(&fixture.rom(), immutable).unwrap();
+ let artifact = fixture.artifact(fixture.root.join("rom2/node"), b"node");
+
+ let verified =
+ verify_launch_artifact_with_role(&artifact, "test", false, Some(&root)).unwrap();
+ assert!(sealed(&verified));
+ }
+
+ #[test]
+ fn a_root_that_is_not_read_only_falls_back_to_the_sealed_snapshot() {
+ let fixture = Fixture::new("writable");
+ // The real probe rejects a writable temporary directory at startup.
+ assert!(TrustedLaunchRoot::open(&fixture.rom()).is_err());
+
+ // A mount that stops being immutable after startup is rejected per
+ // artifact, from the open descriptor.
+ let mut root = TrustedLaunchRoot::open_with_mount_check(&fixture.rom(), immutable).unwrap();
+ root.immutable_mount = writable;
+ let artifact = fixture.artifact(fixture.rom().join("libexec/node"), b"node");
+ let verified =
+ verify_launch_artifact_with_role(&artifact, "test", true, Some(&root)).unwrap();
+ assert!(sealed(&verified));
+
+ let mut tampered = artifact.clone();
+ tampered.sha256 = "sha256:wrong".to_owned();
+ assert!(verify_launch_artifact_with_role(&tampered, "test", true, Some(&root)).is_err());
+ }
+
+ #[test]
+ fn the_real_mount_probe_rejects_a_writable_filesystem() {
+ let fixture = Fixture::new("probe");
+ let file = File::open(fixture.artifact(fixture.rom().join("node"), b"node").path).unwrap();
+ assert!(!immutable_exec_mount(&file).unwrap());
+ }
+
+ #[test]
+ fn symlink_and_parent_escapes_from_the_root_fall_back() {
+ let fixture = Fixture::new("escape");
+ let root = TrustedLaunchRoot::open_with_mount_check(&fixture.rom(), immutable).unwrap();
+ let outside = fixture.artifact(fixture.root.join("outside/node"), b"node");
+
+ // A directory symlink inside the root that points outside it.
+ std::os::unix::fs::symlink(fixture.root.join("outside"), fixture.rom().join("escape"))
+ .unwrap();
+ let through_link = QualifiedLaunchArtifact {
+ path: fixture.rom().join("escape/node"),
+ sha256: outside.sha256.clone(),
+ };
+ let verified =
+ verify_launch_artifact_with_role(&through_link, "test", true, Some(&root)).unwrap();
+ assert!(sealed(&verified));
+
+ // A `..` path that starts under the root and leaves it.
+ let dotted = QualifiedLaunchArtifact {
+ path: fixture.rom().join("libexec/../../outside/node"),
+ sha256: outside.sha256.clone(),
+ };
+ let verified =
+ verify_launch_artifact_with_role(&dotted, "test", true, Some(&root)).unwrap();
+ assert!(sealed(&verified));
+
+ // A final-component symlink is still refused outright.
+ std::os::unix::fs::symlink(&outside.path, fixture.rom().join("libexec/link")).unwrap();
+ let linked = QualifiedLaunchArtifact {
+ path: fixture.rom().join("libexec/link"),
+ sha256: outside.sha256,
+ };
+ assert!(verify_launch_artifact_with_role(&linked, "test", true, Some(&root)).is_err());
+ }
+
+ #[test]
+ fn a_root_must_be_absolute_canonical_and_a_directory() {
+ let fixture = Fixture::new("root-shape");
+ std::os::unix::fs::symlink(fixture.rom(), fixture.root.join("alias")).unwrap();
+ for candidate in [
+ PathBuf::from("relative/rom"),
+ fixture.root.join("alias"),
+ fixture.rom().join("libexec/.."),
+ fixture.root.join("missing"),
+ ] {
+ assert!(
+ TrustedLaunchRoot::open_with_mount_check(&candidate, immutable).is_err(),
+ "{}",
+ candidate.display()
+ );
+ }
+ let file = fixture
+ .artifact(fixture.rom().join("libexec/node"), b"node")
+ .path;
+ assert!(TrustedLaunchRoot::open_with_mount_check(&file, immutable).is_err());
+ }
+
+ #[test]
+ fn without_a_trusted_root_launch_artifacts_are_sealed_and_hashed() {
+ let fixture = Fixture::new("absent");
+ let artifact = fixture.artifact(fixture.rom().join("libexec/node"), b"node");
+
+ assert!(TRUSTED_LAUNCH_ROOT.get().is_none());
+ assert!(sealed(&verify_launch_artifact(&artifact, "test").unwrap()));
+ assert!(sealed(
+ &verify_executable_launch_artifact(&artifact, "test").unwrap()
+ ));
+
+ let mut tampered = artifact;
+ tampered.sha256 = "sha256:wrong".to_owned();
+ assert!(verify_launch_artifact(&tampered, "test").is_err());
+ }
+
+ #[test]
+ fn host_assigned_executables_ignore_the_trusted_root() {
+ let fixture = Fixture::new("assigned");
+ let root = TrustedLaunchRoot::open_with_mount_check(&fixture.rom(), immutable).unwrap();
+ let artifact = fixture.artifact(fixture.rom().join("libexec/mcp"), b"mcp");
+
+ // The same configured root admits this file for a Runner-owned launch.
+ let runner =
+ verify_owned(&artifact, "test", true, ArtifactOwner::Runner, Some(&root)).unwrap();
+ assert!(!sealed(&runner));
+
+ // A host-assigned executable at the same path stays sealed and hashed.
+ let assigned = verify_owned(
+ &artifact,
+ "test",
+ true,
+ ArtifactOwner::HostAssigned,
+ Some(&root),
+ )
+ .unwrap();
+ assert!(sealed(&assigned));
+ let mut tampered = artifact;
+ tampered.sha256 = "sha256:wrong".to_owned();
+ assert!(verify_owned(
+ &tampered,
+ "test",
+ true,
+ ArtifactOwner::HostAssigned,
+ Some(&root),
+ )
+ .is_err());
+ }
+}
diff --git a/packages/paperclip-runner/runner/crates/runner-core/src/stdio_mcp.rs b/packages/paperclip-runner/runner/crates/runner-core/src/stdio_mcp.rs
index 7f580ea20a3..c530f6a82c8 100644
--- a/packages/paperclip-runner/runner/crates/runner-core/src/stdio_mcp.rs
+++ b/packages/paperclip-runner/runner/crates/runner-core/src/stdio_mcp.rs
@@ -15,7 +15,7 @@ use sha2::{Digest, Sha256};
use crate::durable::QualifiedLaunchArtifact;
use crate::local_runner::LocalRunnerError;
use crate::process_supervisor::{VerifiedProcessArgument, VerifiedProcessLaunch};
-use crate::qualified_launch::verify_executable_launch_artifact;
+use crate::qualified_launch::verify_assigned_executable_launch_artifact;
pub const BINDINGS_ENV: &str = "PAPERCLIP_NATIVE_MCP_STDIO";
pub const PROXY_ENV: &str = "PAPERCLIP_NATIVE_MCP_PROXY";
@@ -67,7 +67,7 @@ impl StdioMcpBinding {
{
return Err(invalid("invalid assigned stdio MCP binding"));
}
- verify_executable_launch_artifact(
+ verify_assigned_executable_launch_artifact(
&QualifiedLaunchArtifact {
path: self.command.clone(),
sha256: self.sha256.clone(),
@@ -271,7 +271,7 @@ pub fn run_proxy(path: &Path, content_images: bool) -> McpResult<()> {
.map_err(|_| invalid("invalid stdio MCP configuration"))?;
binding.validate()?;
let (mut lock, resource_identity) = resource_lock(&binding.serial_resource)?;
- let program = verify_executable_launch_artifact(
+ let program = verify_assigned_executable_launch_artifact(
&QualifiedLaunchArtifact {
path: binding.command.clone(),
sha256: binding.sha256.clone(),
diff --git a/tests/runner-e2e/SECURITY.md b/tests/runner-e2e/SECURITY.md
index d186c8855de..709101ef64a 100644
--- a/tests/runner-e2e/SECURITY.md
+++ b/tests/runner-e2e/SECURITY.md
@@ -588,6 +588,16 @@ foreign inodes fail before any signal. Native Linux calibration exercises both
descriptor launches with synthetic owned processes and no provider credentials;
it does not qualify paid Pi behavior or change previous failed grades.
+These fixtures run without `paperclip-runnerd --trusted-launch-root`, so the
+Runner still hands providers a sealed memfd Node and the memfd assertions above
+are unchanged. Under a trusted launch root (the agent computer's read-only Runner
+ROM) the Runner instead holds the ROM's own Node file. Kimi admission then takes
+its existing linked-descriptor branch: one link, the same inode as the parent's
+`/proc//exe`, and the complete bytes hashed against `nodeSha256`. It never
+accepts that file as a memfd. Pi admission binds to the native distribution
+snapshot's Node, which the ACPX closure copies whether or not a trusted root is
+set.
+
The private resource-admission marker and raw cleanup results control recovery-state retention independently of evidence packaging. A worker crash after admission keeps the owner-only recovery database; a confirmed pre-allocation bootstrap failure does not. Neither the marker nor the database enters published evidence.
## Task workspace fixture source