From 68cf9671d95acc3372c9afa24a7df399b31464dd Mon Sep 17 00:00:00 2001 From: Dotta Date: Tue, 3 Nov 2026 17:41:34 -0600 Subject: [PATCH] test(runner): keep native sandbox regressions in the test directory Co-Authored-By: Paperclip --- .../runner-core/src/runtime_notifications.rs | 159 +----------------- .../tests/codex_sandbox.rs | 157 +++++++++++++++++ 2 files changed, 158 insertions(+), 158 deletions(-) create mode 100644 packages/paperclip-runner/runner/crates/runner-core/src/runtime_notifications/tests/codex_sandbox.rs diff --git a/packages/paperclip-runner/runner/crates/runner-core/src/runtime_notifications.rs b/packages/paperclip-runner/runner/crates/runner-core/src/runtime_notifications.rs index eab22606958..975fca64ba7 100644 --- a/packages/paperclip-runner/runner/crates/runner-core/src/runtime_notifications.rs +++ b/packages/paperclip-runner/runner/crates/runner-core/src/runtime_notifications.rs @@ -666,164 +666,7 @@ mod tests { assert_eq!(fs::read(nested.join("protected")).unwrap(), b"unchanged"); fs::remove_dir_all(pipe.parent().unwrap()).unwrap(); } - #[test] - fn codex_activity_grant_uses_only_its_own_private_channel_directory() { - let (_directory, config, _store, _state, server) = setup(); - let (_other_directory, _other_config, _other_store, _other_state, other) = setup(); - let channel = server.pipe.0.parent().unwrap(); - let other_channel = other.pipe.0.parent().unwrap(); - assert_ne!(channel, other_channel); - verify_private_directory(channel).unwrap(); - assert!(fs::symlink_metadata(&server.pipe.0) - .unwrap() - .file_type() - .is_fifo()); - let paths = [ - PathBuf::from("/runtime/runnerd"), - context_path(&config.state_dir), - server.pipe.0.with_extension("lock"), - server.pipe.0.clone(), - ]; - let profile = "paperclip-runner-workspace-read-only"; - let args = vec![ - "shell_environment_policy.include_only=[]".into(), - format!("permissions.{profile}.filesystem={{\":root\"=\"none\"}}"), - ]; - let projected = - crate::codex_provider::runtime_notification_codex_args(&args, profile, &paths); - let prefix = format!("permissions.{profile}.filesystem="); - let value = projected - .iter() - .find_map(|arg| arg.strip_prefix(&prefix)) - .unwrap(); - let config = format!("filesystem={value}") - .parse::() - .unwrap(); - let filesystem = config["filesystem"].as_inline_table().unwrap(); - assert_eq!( - filesystem - .get(channel.to_str().unwrap()) - .and_then(toml_edit::Value::as_str), - Some("write") - ); - assert!(filesystem.get(other_channel.to_str().unwrap()).is_none()); - assert!(filesystem - .get(paths[1].parent().unwrap().to_str().unwrap()) - .is_none()); - } - - #[test] - #[ignore = "requires source-pinned Linux CODEX_NOTIFICATION_SANDBOX_COMMAND or CODEX_NOTIFICATION_FIXTURE_EXPORT"] - fn read_only_codex_opens_with_runtime_activity_without_widening_file_access() { - use std::process::Command; - - let directory = TestDirectory::create(&std::env::temp_dir()).unwrap(); - let config = config(directory.directory.join("state")); - fs::DirBuilder::new() - .mode(0o700) - .create(&config.state_dir) - .unwrap(); - let server = RuntimeNotifications::bind(&config.state_dir).unwrap(); - let workspace = directory.directory.join("workspace"); - let home = directory.directory.join("home"); - fs::create_dir(&workspace).unwrap(); - fs::create_dir(&home).unwrap(); - fs::write( - home.join("credential-fixture"), - b"synthetic private credential", - ) - .unwrap(); - fs::write( - workspace.join("AGENTS.md"), - b"Read-only fixture instructions.\n", - ) - .unwrap(); - fs::write( - config.state_dir.join("private-state"), - b"must remain inaccessible", - ) - .unwrap(); - let helper = config.state_dir.join("helper"); - fs::write(&helper, b"synthetic executable resource\n").unwrap(); - let paths = [ - helper, - context_path(&config.state_dir), - server.pipe.0.with_extension("lock"), - server.pipe.0.clone(), - ]; - let profile = "paperclip-runner-workspace-read-only"; - let resources = std::env::var("CODEX_NOTIFICATION_SANDBOX_RESOURCES") - .expect("set the source-pinned Codex executable resource directory"); - let resources = serde_json::to_string(&resources).unwrap(); - let args = vec![ - "-c".into(), - "shell_environment_policy.include_only=[\"PATH\"]".into(), - "-c".into(), - format!("permissions.{profile}.filesystem={{\":root\"=\"none\",\":minimal\"=\"read\",\":tmpdir\"=\"none\",{resources}=\"read\",\"{}\"=\"none\",\"{}\"=\"read\"}}", home.display(), workspace.display()), - "-c".into(), - format!("permissions.{profile}.network.enabled=false"), - ]; - let extra = crate::codex_provider::runtime_notification_codex_args(&args, profile, &paths); - let script = r#" -set -eu -test -z "${UNRELATED_SECRET:-}" -test "$(cat "$4/AGENTS.md")" = 'Read-only fixture instructions.' -if touch "$4/forbidden" 2>/dev/null; then exit 71; fi -if cat "$1/private-state" >/dev/null 2>&1; then exit 72; fi -if sh -c ': >> "$1"' ignored "$2" 2>/dev/null; then exit 73; fi -if sh -c ': >> "$1"' ignored "$3" 2>/dev/null; then exit 74; fi -if rm "$3" 2>/dev/null; then exit 75; fi -if cat "$6/credential-fixture" >/dev/null 2>&1; then exit 76; fi -cat "$2" >/dev/null -cat "$3" >/dev/null -printf 'synthetic notification frame\n' > "$5" -"#; - let mut args = args; - args.extend(extra); - args.extend(["sandbox", "-P", profile, "-C"].map(str::to_owned)); - args.push(workspace.to_string_lossy().into_owned()); - args.extend(["--", "/bin/sh", "-c", script, "notification-sandbox"].map(str::to_owned)); - args.extend( - [ - &config.state_dir, - &paths[1], - &paths[2], - &workspace, - &paths[3], - &home, - ] - .map(|path| path.to_string_lossy().into_owned()), - ); - if let Some(path) = std::env::var_os("CODEX_NOTIFICATION_FIXTURE_EXPORT") { - fs::write( - path, - serde_json::to_vec_pretty(&json!({ - "args": args, "stateDirectory": config.state_dir, - "pipe": server.pipe.0, "home": home, "workspace": workspace, - "descriptor": fs::read_to_string(&paths[1]).unwrap(), - })) - .unwrap(), - ) - .unwrap(); - return; - } - let command = std::env::var_os("CODEX_NOTIFICATION_SANDBOX_COMMAND").unwrap(); - let output = Command::new(command) - .args(args) - .current_dir(&workspace) - .env_clear() - .env("PATH", std::env::var_os("PATH").unwrap()) - .env("HOME", &home) - .env("CODEX_HOME", &home) - .env("UNRELATED_SECRET", "must-not-reach-command") - .output() - .unwrap(); - assert!( - output.status.success(), - "source-pinned sandbox failed: {}", - String::from_utf8_lossy(&output.stderr) - ); - } + include!("runtime_notifications/tests/codex_sandbox.rs"); #[test] #[ignore = "requires CODEX_NOTIFICATION_SANDBOX_COMMAND and PAPERCLIP_RUNTIME_NOTIFICATION_HELPER"] diff --git a/packages/paperclip-runner/runner/crates/runner-core/src/runtime_notifications/tests/codex_sandbox.rs b/packages/paperclip-runner/runner/crates/runner-core/src/runtime_notifications/tests/codex_sandbox.rs new file mode 100644 index 00000000000..1638b7d84c5 --- /dev/null +++ b/packages/paperclip-runner/runner/crates/runner-core/src/runtime_notifications/tests/codex_sandbox.rs @@ -0,0 +1,157 @@ +#[test] +fn codex_activity_grant_uses_only_its_own_private_channel_directory() { + let (_directory, config, _store, _state, server) = setup(); + let (_other_directory, _other_config, _other_store, _other_state, other) = setup(); + let channel = server.pipe.0.parent().unwrap(); + let other_channel = other.pipe.0.parent().unwrap(); + assert_ne!(channel, other_channel); + verify_private_directory(channel).unwrap(); + assert!(fs::symlink_metadata(&server.pipe.0) + .unwrap() + .file_type() + .is_fifo()); + let paths = [ + PathBuf::from("/runtime/runnerd"), + context_path(&config.state_dir), + server.pipe.0.with_extension("lock"), + server.pipe.0.clone(), + ]; + let profile = "paperclip-runner-workspace-read-only"; + let args = vec![ + "shell_environment_policy.include_only=[]".into(), + format!("permissions.{profile}.filesystem={{\":root\"=\"none\"}}"), + ]; + let projected = crate::codex_provider::runtime_notification_codex_args(&args, profile, &paths); + let prefix = format!("permissions.{profile}.filesystem="); + let value = projected + .iter() + .find_map(|arg| arg.strip_prefix(&prefix)) + .unwrap(); + let config = format!("filesystem={value}") + .parse::() + .unwrap(); + let filesystem = config["filesystem"].as_inline_table().unwrap(); + assert_eq!( + filesystem + .get(channel.to_str().unwrap()) + .and_then(toml_edit::Value::as_str), + Some("write") + ); + assert!(filesystem.get(other_channel.to_str().unwrap()).is_none()); + assert!(filesystem + .get(paths[1].parent().unwrap().to_str().unwrap()) + .is_none()); +} + +#[test] +#[ignore = "requires source-pinned Linux CODEX_NOTIFICATION_SANDBOX_COMMAND or CODEX_NOTIFICATION_FIXTURE_EXPORT"] +fn read_only_codex_opens_with_runtime_activity_without_widening_file_access() { + use std::process::Command; + + let directory = TestDirectory::create(&std::env::temp_dir()).unwrap(); + let config = config(directory.directory.join("state")); + fs::DirBuilder::new() + .mode(0o700) + .create(&config.state_dir) + .unwrap(); + let server = RuntimeNotifications::bind(&config.state_dir).unwrap(); + let workspace = directory.directory.join("workspace"); + let home = directory.directory.join("home"); + fs::create_dir(&workspace).unwrap(); + fs::create_dir(&home).unwrap(); + fs::write( + home.join("credential-fixture"), + b"synthetic private credential", + ) + .unwrap(); + fs::write( + workspace.join("AGENTS.md"), + b"Read-only fixture instructions.\n", + ) + .unwrap(); + fs::write( + config.state_dir.join("private-state"), + b"must remain inaccessible", + ) + .unwrap(); + let helper = config.state_dir.join("helper"); + fs::write(&helper, b"synthetic executable resource\n").unwrap(); + let paths = [ + helper, + context_path(&config.state_dir), + server.pipe.0.with_extension("lock"), + server.pipe.0.clone(), + ]; + let profile = "paperclip-runner-workspace-read-only"; + let resources = std::env::var("CODEX_NOTIFICATION_SANDBOX_RESOURCES") + .expect("set the source-pinned Codex executable resource directory"); + let resources = serde_json::to_string(&resources).unwrap(); + let args = vec![ + "-c".into(), + "shell_environment_policy.include_only=[\"PATH\"]".into(), + "-c".into(), + format!("permissions.{profile}.filesystem={{\":root\"=\"none\",\":minimal\"=\"read\",\":tmpdir\"=\"none\",{resources}=\"read\",\"{}\"=\"none\",\"{}\"=\"read\"}}", home.display(), workspace.display()), + "-c".into(), + format!("permissions.{profile}.network.enabled=false"), + ]; + let extra = crate::codex_provider::runtime_notification_codex_args(&args, profile, &paths); + let script = r#" +set -eu +test -z "${UNRELATED_SECRET:-}" +test "$(cat "$4/AGENTS.md")" = 'Read-only fixture instructions.' +if touch "$4/forbidden" 2>/dev/null; then exit 71; fi +if cat "$1/private-state" >/dev/null 2>&1; then exit 72; fi +if sh -c ': >> "$1"' ignored "$2" 2>/dev/null; then exit 73; fi +if sh -c ': >> "$1"' ignored "$3" 2>/dev/null; then exit 74; fi +if rm "$3" 2>/dev/null; then exit 75; fi +if cat "$6/credential-fixture" >/dev/null 2>&1; then exit 76; fi +cat "$2" >/dev/null +cat "$3" >/dev/null +printf 'synthetic notification frame\n' > "$5" +"#; + let mut args = args; + args.extend(extra); + args.extend(["sandbox", "-P", profile, "-C"].map(str::to_owned)); + args.push(workspace.to_string_lossy().into_owned()); + args.extend(["--", "/bin/sh", "-c", script, "notification-sandbox"].map(str::to_owned)); + args.extend( + [ + &config.state_dir, + &paths[1], + &paths[2], + &workspace, + &paths[3], + &home, + ] + .map(|path| path.to_string_lossy().into_owned()), + ); + if let Some(path) = std::env::var_os("CODEX_NOTIFICATION_FIXTURE_EXPORT") { + fs::write( + path, + serde_json::to_vec_pretty(&json!({ + "args": args, "stateDirectory": config.state_dir, + "pipe": server.pipe.0, "home": home, "workspace": workspace, + "descriptor": fs::read_to_string(&paths[1]).unwrap(), + })) + .unwrap(), + ) + .unwrap(); + return; + } + let command = std::env::var_os("CODEX_NOTIFICATION_SANDBOX_COMMAND").unwrap(); + let output = Command::new(command) + .args(args) + .current_dir(&workspace) + .env_clear() + .env("PATH", std::env::var_os("PATH").unwrap()) + .env("HOME", &home) + .env("CODEX_HOME", &home) + .env("UNRELATED_SECRET", "must-not-reach-command") + .output() + .unwrap(); + assert!( + output.status.success(), + "source-pinned sandbox failed: {}", + String::from_utf8_lossy(&output.stderr) + ); +}