Skip to content

Commit 2636193

Browse files
authored
Merge pull request #23 from A3S-Lab/fix/srt-nested-write-denies-6.3.1
fix(sandbox): avoid nested deny mount failures
2 parents 142b534 + 578593a commit 2636193

20 files changed

Lines changed: 106 additions & 46 deletions

File tree

CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
## [6.3.1] - 2026-07-22
11+
12+
### Fixed
13+
14+
- Removed redundant nested SRT write-deny mounts when a protected workspace
15+
ancestor already blocks writes, while retaining credential read denial and
16+
standalone sensitive-file write protection.
17+
1018
## [6.3.0] - 2026-07-22
1119

1220
### Added

Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

README.md

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -317,10 +317,12 @@ language/bootstrap injection variables. It keeps stdout and stderr distinct
317317
under one global capture limit. Its deadline covers output draining and the
318318
child's complete lifetime, including a process that closes both streams before
319319
it exits; timeout or cancellation terminates the Unix process group. It never
320-
searches `PATH` for a sandbox runtime and never falls back to the host runner
321-
when its explicitly provisioned runtime is missing or fails. The embedding host
322-
remains responsible for choosing whether an unavailable sandbox causes an
323-
interactive escalation or a deterministic denial.
320+
searches `PATH` for a sandbox runtime and never falls back to the host runner
321+
when its explicitly provisioned runtime is missing or fails. Write-deny paths
322+
already covered by a protected ancestor are collapsed before SRT startup,
323+
while more-specific credential read denies remain intact. The embedding host
324+
remains responsible for choosing whether an unavailable sandbox causes an
325+
interactive escalation or a deterministic denial.
324326

325327
Shell isolation does not automatically govern in-process workspace tools.
326328
Interactive hosts should construct `LocalWorkspaceBackend` or

core/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "a3s-code-core"
3-
version = "6.3.0"
3+
version = "6.3.1"
44
edition = "2021"
55
authors = ["A3S Lab Team"]
66
license = "MIT"

core/src/sandbox/srt.rs

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,7 @@ impl SrtBashSandbox {
132132
deny_read.extend(read_denied_roots());
133133
let mut allow_read = readable_tool_paths(&self.workspace, scratch);
134134
deny_write.extend(sensitive_paths.iter().cloned());
135-
deduplicate_paths(&mut deny_write);
135+
remove_redundant_deny_write_descendants(&mut deny_write);
136136
deduplicate_paths(&mut sensitive_paths);
137137
deduplicate_paths(&mut deny_read);
138138
deduplicate_paths(&mut allow_read);
@@ -1047,6 +1047,16 @@ fn deduplicate_paths(paths: &mut Vec<PathBuf>) {
10471047
paths.dedup();
10481048
}
10491049

1050+
fn remove_redundant_deny_write_descendants(paths: &mut Vec<PathBuf>) {
1051+
deduplicate_paths(paths);
1052+
let candidates = paths.clone();
1053+
paths.retain(|path| {
1054+
!candidates
1055+
.iter()
1056+
.any(|ancestor| ancestor != path && path.starts_with(ancestor))
1057+
});
1058+
}
1059+
10501060
fn path_strings<'a>(paths: impl IntoIterator<Item = &'a Path>) -> Vec<String> {
10511061
paths
10521062
.into_iter()

core/src/sandbox/srt/tests.rs

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,22 @@ async fn adapter_passes_argv_workspace_timeout_env_and_settings() {
9898
.as_array()
9999
.unwrap()
100100
.iter()
101-
.any(|path| path.as_str().unwrap().ends_with("/.codex/auth.json")));
101+
.any(|path| path
102+
== &json!(workspace
103+
.path()
104+
.canonicalize()
105+
.unwrap()
106+
.join(".codex/auth.json"))));
107+
assert!(settings["filesystem"]["denyRead"]
108+
.as_array()
109+
.unwrap()
110+
.iter()
111+
.any(|path| path
112+
== &json!(workspace
113+
.path()
114+
.canonicalize()
115+
.unwrap()
116+
.join(".a3s/os-auth.json"))));
102117
assert!(settings["filesystem"]["denyRead"]
103118
.as_array()
104119
.unwrap()
@@ -114,11 +129,29 @@ async fn adapter_passes_argv_workspace_timeout_env_and_settings() {
114129
.canonicalize()
115130
.unwrap()
116131
.join("services/api/.env"))));
132+
assert!(
133+
!settings["filesystem"]["denyWrite"]
134+
.as_array()
135+
.unwrap()
136+
.iter()
137+
.any(|path| path
138+
== &json!(workspace
139+
.path()
140+
.canonicalize()
141+
.unwrap()
142+
.join(".a3s/os-auth.json"))),
143+
"a protected parent directory must cover sensitive descendants without a nested mount"
144+
);
117145
assert!(settings["filesystem"]["denyWrite"]
118146
.as_array()
119147
.unwrap()
120148
.iter()
121-
.any(|path| path.as_str().unwrap().ends_with("/.codex/auth.json")));
149+
.any(|path| path
150+
== &json!(workspace
151+
.path()
152+
.canonicalize()
153+
.unwrap()
154+
.join("services/api/.env"))));
122155
assert_eq!(settings["enableWeakerNestedSandbox"], false);
123156
assert_eq!(settings["allowAppleEvents"], false);
124157
}

core/src/tools/builtin/bash.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -200,10 +200,10 @@ impl Tool for BashTool {
200200
}
201201
};
202202
if require_escalated
203-
&& !args
203+
&& args
204204
.get("justification")
205205
.and_then(serde_json::Value::as_str)
206-
.is_some_and(|value| !value.trim().is_empty())
206+
.is_none_or(|value| value.trim().is_empty())
207207
{
208208
return Ok(ToolOutput::error(
209209
"justification is required when sandbox_permissions is require_escalated",

sdk/node/Cargo.lock

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

sdk/node/Cargo.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "a3s-code-node"
3-
version = "6.3.0"
3+
version = "6.3.1"
44
edition = "2021"
55
authors = ["A3S Lab Team"]
66
license = "MIT"
@@ -11,7 +11,7 @@ description = "A3S Code Node.js bindings - Native addon via napi-rs"
1111
crate-type = ["cdylib"]
1212

1313
[dependencies]
14-
a3s-code-core = { version = "6.3.0", path = "../../core", features = ["s3", "serve"] }
14+
a3s-code-core = { version = "6.3.1", path = "../../core", features = ["s3", "serve"] }
1515
napi = { version = "2", features = ["async", "napi6", "serde-json"] }
1616
napi-derive = "2"
1717
tokio = { version = "1.35", features = ["full"] }

sdk/node/examples/package-lock.json

Lines changed: 7 additions & 7 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)