Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
11 changes: 7 additions & 4 deletions core/src/sandbox/srt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down Expand Up @@ -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)]
{
Expand All @@ -628,7 +627,7 @@ fn compose_srt_process_env(
}

#[cfg(not(windows))]
fn compose_wrapper_env(workspace: &Path) -> HashMap<OsString, OsString> {
fn compose_wrapper_env(workspace: &Path, scratch: &Path) -> HashMap<OsString, OsString> {
const SAFE_KEYS: &[&str] = &[
"HOME",
"USER",
Expand Down Expand Up @@ -656,6 +655,10 @@ fn compose_wrapper_env(workspace: &Path) -> HashMap<OsString, OsString> {
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
}
Expand Down
101 changes: 100 additions & 1 deletion core/src/sandbox/srt/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -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"] {
Expand Down Expand Up @@ -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"]
Expand Down
Loading