diff --git a/CHANGELOG.md b/CHANGELOG.md index ad9153db..9d60bf9d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Changed + +- Selected sandbox-runtime 0.0.67 for managed hosts. + +### Fixed + +- Pinned Unix managed-SRT temporary files to the private per-command scratch + directory so hosts can pass large macOS Seatbelt profiles by file without + exceeding the operating system argument-size limit. + ## [6.4.1] - 2026-07-23 ### Fixed diff --git a/README.md b/README.md index 2a690d08..cf0092ee 100644 --- a/README.md +++ b/README.md @@ -326,7 +326,9 @@ when its explicitly provisioned runtime is missing or fails. Write-deny paths already covered by a protected ancestor are collapsed before SRT startup, while more-specific credential read denies remain intact. Workspace policy scans treat an entry removed concurrently after enumeration as absent, but -permission and other I/O failures remain fatal. The embedding host remains +permission and other I/O failures remain fatal. Unix wrapper temporary files +are pinned to the same private per-run scratch directory so a lifecycle host +can pass large native sandbox policies by file. The embedding host remains responsible for choosing whether an unavailable sandbox causes an interactive escalation or a deterministic denial. diff --git a/core/src/sandbox/srt.rs b/core/src/sandbox/srt.rs index 116c3986..58119103 100644 --- a/core/src/sandbox/srt.rs +++ b/core/src/sandbox/srt.rs @@ -28,7 +28,7 @@ pub const SRT_NPM_PACKAGE_NAME: &str = "@anthropic-ai/sandbox-runtime"; /// Core accepts the tested compatibility range below so a host can roll a /// compatible patch independently. The CLI deliberately installs one exact /// version until an A3S-signed component artifact replaces registry bootstrap. -pub const MANAGED_SRT_VERSION: &str = "0.0.66"; +pub const MANAGED_SRT_VERSION: &str = "0.0.67"; const MINIMUM_SRT_VERSION: (u64, u64, u64) = (0, 0, 66); const MAXIMUM_SRT_VERSION_EXCLUSIVE: (u64, u64, u64) = (0, 1, 0); @@ -611,8 +611,7 @@ fn compose_srt_process_env( #[cfg(not(windows))] { let _ = explicit; - let _ = scratch; - Ok(compose_wrapper_env(workspace)) + Ok(compose_wrapper_env(workspace, scratch)) } #[cfg(windows)] { @@ -628,7 +627,7 @@ fn compose_srt_process_env( } #[cfg(not(windows))] -fn compose_wrapper_env(workspace: &Path) -> HashMap { +fn compose_wrapper_env(workspace: &Path, scratch: &Path) -> HashMap { const SAFE_KEYS: &[&str] = &[ "HOME", "USER", @@ -656,6 +655,10 @@ fn compose_wrapper_env(workspace: &Path) -> HashMap { if let Some(path) = trusted_wrapper_path(workspace) { environment.insert(OsString::from("PATH"), path); } + let scratch = scratch.as_os_str().to_os_string(); + environment.insert(OsString::from("TMPDIR"), scratch.clone()); + environment.insert(OsString::from("TMP"), scratch.clone()); + environment.insert(OsString::from("TEMP"), scratch); remove_bootstrap_injection_variables(&mut environment); environment } diff --git a/core/src/sandbox/srt/tests.rs b/core/src/sandbox/srt/tests.rs index 34ed626b..9704ef83 100644 --- a/core/src/sandbox/srt/tests.rs +++ b/core/src/sandbox/srt/tests.rs @@ -270,6 +270,22 @@ fn child_environment_drops_ambient_secrets_and_pins_scratch_paths() { ); } +#[cfg(unix)] +#[test] +fn wrapper_environment_pins_profile_files_to_the_private_scratch_directory() { + let workspace = tempfile::tempdir().unwrap(); + let scratch = tempfile::tempdir().unwrap(); + let environment = compose_srt_process_env(None, scratch.path(), workspace.path()).unwrap(); + + for key in ["TMPDIR", "TMP", "TEMP"] { + assert_eq!( + environment.get(OsStr::new(key)), + Some(&scratch.path().as_os_str().to_os_string()), + "{key} must keep managed SRT profile files inside the per-run scratch directory" + ); + } +} + #[test] fn child_environment_rejects_explicit_bootstrap_injection_variables() { let scratch = tempfile::tempdir().unwrap(); @@ -314,7 +330,7 @@ fn child_environment_rejects_explicit_bootstrap_injection_variables() { #[test] fn supported_srt_version_range_is_explicit() { - for version in ["0.0.66", "v0.0.66", "0.0.99-beta.1"] { + for version in ["0.0.66", "0.0.67", "v0.0.67", "0.0.99-beta.1"] { ensure_supported_srt_version(version).unwrap(); } for version in ["0.0.65", "0.1.0", "1.0.0", "unknown"] { @@ -517,6 +533,89 @@ async fn real_srt_probe_survives_concurrent_workspace_churn() { result.unwrap(); } +/// Run explicitly with `A3S_TEST_SRT_BIN=/absolute/path/to/cli.js` and +/// `A3S_TEST_SRT_NODE=/absolute/path/to/node`. +#[tokio::test] +#[ignore = "requires an installed A3S-patched srt runtime and Node.js"] +async fn real_srt_probe_handles_many_nested_sensitive_paths_without_e2big() { + let binary = std::env::var_os("A3S_TEST_SRT_BIN") + .map(PathBuf::from) + .expect("set A3S_TEST_SRT_BIN"); + let node = std::env::var_os("A3S_TEST_SRT_NODE") + .map(PathBuf::from) + .expect("set A3S_TEST_SRT_NODE"); + let workspace = tempfile::tempdir().unwrap(); + + for directory in 0..128 { + let nested = workspace + .path() + .join(format!("service-{directory:03}/config")); + std::fs::create_dir_all(&nested).unwrap(); + for variant in 0..4 { + std::fs::write( + nested.join(format!(".env.variant-{variant}")), + b"SECRET=hidden", + ) + .unwrap(); + } + } + + let sandbox = + SrtBashSandbox::from_verified_npm_with_node(&binary, &node, workspace.path()).unwrap(); + let output = sandbox + .exec_command("printf a3s-managed-srt-ready", "/workspace") + .await + .unwrap(); + + assert_eq!( + output.exit_code, 0, + "large managed SRT profile failed: {}{}", + output.stdout, output.stderr + ); + assert_eq!(output.stdout, "a3s-managed-srt-ready"); +} + +/// Run explicitly with `A3S_TEST_SRT_BIN=/absolute/path/to/cli.js` and +/// `A3S_TEST_SRT_NODE=/absolute/path/to/node`. +#[cfg(unix)] +#[tokio::test] +#[ignore = "requires an installed A3S-patched srt runtime and Node.js"] +async fn real_srt_probe_handles_a_large_hardlink_profile_without_e2big() { + let binary = std::env::var_os("A3S_TEST_SRT_BIN") + .map(PathBuf::from) + .expect("set A3S_TEST_SRT_BIN"); + let node = std::env::var_os("A3S_TEST_SRT_NODE") + .map(PathBuf::from) + .expect("set A3S_TEST_SRT_NODE"); + let root = tempfile::tempdir().unwrap(); + let workspace = root.path().join("workspace"); + std::fs::create_dir_all(&workspace).unwrap(); + let outside = root.path().join("outside-secret"); + std::fs::write(&outside, "outside-secret").unwrap(); + for index in 0..1_024 { + std::fs::hard_link( + &outside, + workspace.join(format!( + "source-tree-hardlink-alias-with-a-deliberately-long-name-{index:04}.txt" + )), + ) + .unwrap(); + } + + let sandbox = SrtBashSandbox::from_verified_npm_with_node(&binary, &node, &workspace).unwrap(); + let output = sandbox + .exec_command("printf a3s-managed-srt-ready", "/workspace") + .await + .unwrap(); + + assert_eq!( + output.exit_code, 0, + "large managed SRT profile failed: {}{}", + output.stdout, output.stderr + ); + assert_eq!(output.stdout, "a3s-managed-srt-ready"); +} + /// Run explicitly with `A3S_TEST_SRT_BIN=/absolute/path/to/srt`. #[tokio::test] #[ignore = "requires an installed srt runtime"]