Skip to content

Commit 12d2829

Browse files
committed
fix writable roots
1 parent 74a8a21 commit 12d2829

File tree

4 files changed

+126
-34
lines changed

4 files changed

+126
-34
lines changed

codex-rs/app-server/tests/suite/send_message.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ use codex_protocol::protocol::SandboxPolicy;
2121
use core_test_support::responses;
2222
use pretty_assertions::assert_eq;
2323
use std::path::Path;
24+
use std::path::PathBuf;
2425
use tempfile::TempDir;
2526
use tokio::time::timeout;
2627

@@ -354,6 +355,7 @@ fn assert_permissions_message(item: &ResponseItem) {
354355
let expected = DeveloperInstructions::from_policy(
355356
&SandboxPolicy::DangerFullAccess,
356357
AskForApproval::Never,
358+
&PathBuf::from("/tmp"),
357359
)
358360
.into_text();
359361
assert_eq!(

codex-rs/core/src/codex.rs

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1021,7 +1021,14 @@ impl Session {
10211021
return None;
10221022
}
10231023

1024-
Some(DeveloperInstructions::from_policy(&next.sandbox_policy, next.approval_policy).into())
1024+
Some(
1025+
DeveloperInstructions::from_policy(
1026+
&next.sandbox_policy,
1027+
next.approval_policy,
1028+
&next.cwd,
1029+
)
1030+
.into(),
1031+
)
10251032
}
10261033

10271034
/// Persist the event to rollout and send it to clients.
@@ -1361,6 +1368,7 @@ impl Session {
13611368
DeveloperInstructions::from_policy(
13621369
&turn_context.sandbox_policy,
13631370
turn_context.approval_policy,
1371+
&turn_context.cwd,
13641372
)
13651373
.into(),
13661374
);

codex-rs/core/tests/suite/permissions_messages.rs

Lines changed: 97 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,10 @@ use codex_core::config::Constrained;
33
use codex_core::protocol::AskForApproval;
44
use codex_core::protocol::EventMsg;
55
use codex_core::protocol::Op;
6+
use codex_core::protocol::SandboxPolicy;
7+
use codex_protocol::models::DeveloperInstructions;
68
use codex_protocol::user_input::UserInput;
9+
use codex_utils_absolute_path::AbsolutePathBuf;
710
use core_test_support::responses::ev_completed;
811
use core_test_support::responses::ev_response_created;
912
use core_test_support::responses::mount_sse_once;
@@ -14,6 +17,8 @@ use core_test_support::test_codex::test_codex;
1417
use core_test_support::wait_for_event;
1518
use pretty_assertions::assert_eq;
1619
use std::collections::HashSet;
20+
use std::path::PathBuf;
21+
use tempfile::TempDir;
1722

1823
fn permissions_texts(input: &[serde_json::Value]) -> Vec<String> {
1924
input
@@ -42,15 +47,35 @@ fn sse_completed(id: &str) -> String {
4247
sse(vec![ev_response_created(id), ev_completed(id)])
4348
}
4449

50+
fn workspace_write_policy(root: AbsolutePathBuf) -> SandboxPolicy {
51+
SandboxPolicy::WorkspaceWrite {
52+
writable_roots: vec![root],
53+
network_access: false,
54+
exclude_tmpdir_env_var: true,
55+
exclude_slash_tmp: true,
56+
}
57+
}
58+
59+
fn expected_permissions_message(
60+
sandbox_policy: &SandboxPolicy,
61+
approval_policy: AskForApproval,
62+
cwd: &PathBuf,
63+
) -> String {
64+
DeveloperInstructions::from_policy(sandbox_policy, approval_policy, cwd).into_text()
65+
}
66+
4567
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
4668
async fn permissions_message_sent_once_on_start() -> Result<()> {
4769
skip_if_no_network!(Ok(()));
4870

4971
let server = start_mock_server().await;
5072
let req = mount_sse_once(&server, sse_completed("resp-1")).await;
5173

74+
let writable = TempDir::new()?;
75+
let sandbox_policy = workspace_write_policy(AbsolutePathBuf::try_from(writable.path())?);
5276
let mut builder = test_codex().with_config(|config| {
5377
config.approval_policy = Constrained::allow_any(AskForApproval::OnRequest);
78+
config.sandbox_policy = Constrained::allow_any(sandbox_policy.clone());
5479
});
5580
let test = builder.build(&server).await?;
5681

@@ -68,7 +93,9 @@ async fn permissions_message_sent_once_on_start() -> Result<()> {
6893
let body = request.body_json();
6994
let input = body["input"].as_array().expect("input array");
7095
let permissions = permissions_texts(input);
71-
assert_eq!(permissions.len(), 1);
96+
let expected =
97+
expected_permissions_message(&sandbox_policy, AskForApproval::OnRequest, &test.config.cwd);
98+
assert_eq!(permissions, vec![expected]);
7299

73100
Ok(())
74101
}
@@ -81,10 +108,17 @@ async fn permissions_message_added_on_override_change() -> Result<()> {
81108
let req1 = mount_sse_once(&server, sse_completed("resp-1")).await;
82109
let req2 = mount_sse_once(&server, sse_completed("resp-2")).await;
83110

111+
let writable = TempDir::new()?;
112+
let sandbox_policy = workspace_write_policy(AbsolutePathBuf::try_from(writable.path())?);
84113
let mut builder = test_codex().with_config(|config| {
85114
config.approval_policy = Constrained::allow_any(AskForApproval::OnRequest);
115+
config.sandbox_policy = Constrained::allow_any(sandbox_policy.clone());
86116
});
87117
let test = builder.build(&server).await?;
118+
let expected_on_request =
119+
expected_permissions_message(&sandbox_policy, AskForApproval::OnRequest, &test.config.cwd);
120+
let expected_never =
121+
expected_permissions_message(&sandbox_policy, AskForApproval::Never, &test.config.cwd);
88122

89123
test.codex
90124
.submit(Op::UserInput {
@@ -124,8 +158,11 @@ async fn permissions_message_added_on_override_change() -> Result<()> {
124158
let permissions_1 = permissions_texts(input1);
125159
let permissions_2 = permissions_texts(input2);
126160

127-
assert_eq!(permissions_1.len(), 1);
128-
assert_eq!(permissions_2.len(), 2);
161+
assert_eq!(permissions_1, vec![expected_on_request.clone()]);
162+
assert_eq!(
163+
permissions_2,
164+
vec![expected_on_request, expected_never.clone()]
165+
);
129166
let unique = permissions_2.into_iter().collect::<HashSet<String>>();
130167
assert_eq!(unique.len(), 2);
131168

@@ -140,10 +177,15 @@ async fn permissions_message_not_added_when_no_change() -> Result<()> {
140177
let req1 = mount_sse_once(&server, sse_completed("resp-1")).await;
141178
let req2 = mount_sse_once(&server, sse_completed("resp-2")).await;
142179

180+
let writable = TempDir::new()?;
181+
let sandbox_policy = workspace_write_policy(AbsolutePathBuf::try_from(writable.path())?);
143182
let mut builder = test_codex().with_config(|config| {
144183
config.approval_policy = Constrained::allow_any(AskForApproval::OnRequest);
184+
config.sandbox_policy = Constrained::allow_any(sandbox_policy.clone());
145185
});
146186
let test = builder.build(&server).await?;
187+
let expected =
188+
expected_permissions_message(&sandbox_policy, AskForApproval::OnRequest, &test.config.cwd);
147189

148190
test.codex
149191
.submit(Op::UserInput {
@@ -172,9 +214,8 @@ async fn permissions_message_not_added_when_no_change() -> Result<()> {
172214
let permissions_1 = permissions_texts(input1);
173215
let permissions_2 = permissions_texts(input2);
174216

175-
assert_eq!(permissions_1.len(), 1);
176-
assert_eq!(permissions_2.len(), 1);
177-
assert_eq!(permissions_1, permissions_2);
217+
assert_eq!(permissions_1, vec![expected.clone()]);
218+
assert_eq!(permissions_2, vec![expected]);
178219

179220
Ok(())
180221
}
@@ -188,12 +229,22 @@ async fn resume_replays_permissions_messages() -> Result<()> {
188229
let _req2 = mount_sse_once(&server, sse_completed("resp-2")).await;
189230
let req3 = mount_sse_once(&server, sse_completed("resp-3")).await;
190231

232+
let writable = TempDir::new()?;
233+
let sandbox_policy = workspace_write_policy(AbsolutePathBuf::try_from(writable.path())?);
191234
let mut builder = test_codex().with_config(|config| {
192235
config.approval_policy = Constrained::allow_any(AskForApproval::OnRequest);
236+
config.sandbox_policy = Constrained::allow_any(sandbox_policy.clone());
193237
});
194238
let initial = builder.build(&server).await?;
195239
let rollout_path = initial.session_configured.rollout_path.clone();
196240
let home = initial.home.clone();
241+
let expected_on_request = expected_permissions_message(
242+
&sandbox_policy,
243+
AskForApproval::OnRequest,
244+
&initial.config.cwd,
245+
);
246+
let expected_never =
247+
expected_permissions_message(&sandbox_policy, AskForApproval::Never, &initial.config.cwd);
197248

198249
initial
199250
.codex
@@ -244,7 +295,14 @@ async fn resume_replays_permissions_messages() -> Result<()> {
244295
let body3 = req3.single_request().body_json();
245296
let input = body3["input"].as_array().expect("input array");
246297
let permissions = permissions_texts(input);
247-
assert_eq!(permissions.len(), 3);
298+
assert_eq!(
299+
permissions,
300+
vec![
301+
expected_on_request.clone(),
302+
expected_never,
303+
expected_on_request
304+
]
305+
);
248306
let unique = permissions.into_iter().collect::<HashSet<String>>();
249307
assert_eq!(unique.len(), 2);
250308

@@ -261,12 +319,27 @@ async fn resume_and_fork_append_permissions_messages() -> Result<()> {
261319
let req3 = mount_sse_once(&server, sse_completed("resp-3")).await;
262320
let req4 = mount_sse_once(&server, sse_completed("resp-4")).await;
263321

322+
let writable = TempDir::new()?;
323+
let sandbox_policy = workspace_write_policy(AbsolutePathBuf::try_from(writable.path())?);
264324
let mut builder = test_codex().with_config(|config| {
265325
config.approval_policy = Constrained::allow_any(AskForApproval::OnRequest);
326+
config.sandbox_policy = Constrained::allow_any(sandbox_policy.clone());
266327
});
267328
let initial = builder.build(&server).await?;
268329
let rollout_path = initial.session_configured.rollout_path.clone();
269330
let home = initial.home.clone();
331+
let expected_on_request = expected_permissions_message(
332+
&sandbox_policy,
333+
AskForApproval::OnRequest,
334+
&initial.config.cwd,
335+
);
336+
let expected_never =
337+
expected_permissions_message(&sandbox_policy, AskForApproval::Never, &initial.config.cwd);
338+
let expected_unless_trusted = expected_permissions_message(
339+
&sandbox_policy,
340+
AskForApproval::UnlessTrusted,
341+
&initial.config.cwd,
342+
);
270343

271344
initial
272345
.codex
@@ -305,7 +378,10 @@ async fn resume_and_fork_append_permissions_messages() -> Result<()> {
305378
let body2 = req2.single_request().body_json();
306379
let input2 = body2["input"].as_array().expect("input array");
307380
let permissions_base = permissions_texts(input2);
308-
assert_eq!(permissions_base.len(), 2);
381+
assert_eq!(
382+
permissions_base,
383+
vec![expected_on_request.clone(), expected_never.clone()]
384+
);
309385

310386
builder = builder.with_config(|config| {
311387
config.approval_policy = Constrained::allow_any(AskForApproval::UnlessTrusted);
@@ -325,12 +401,14 @@ async fn resume_and_fork_append_permissions_messages() -> Result<()> {
325401
let body3 = req3.single_request().body_json();
326402
let input3 = body3["input"].as_array().expect("input array");
327403
let permissions_resume = permissions_texts(input3);
328-
assert_eq!(permissions_resume.len(), permissions_base.len() + 1);
329404
assert_eq!(
330-
&permissions_resume[..permissions_base.len()],
331-
permissions_base.as_slice()
405+
permissions_resume,
406+
vec![
407+
expected_on_request.clone(),
408+
expected_never.clone(),
409+
expected_unless_trusted.clone()
410+
]
332411
);
333-
assert!(!permissions_base.contains(permissions_resume.last().expect("new permissions")));
334412

335413
let mut fork_config = initial.config.clone();
336414
fork_config.approval_policy = Constrained::allow_any(AskForApproval::UnlessTrusted);
@@ -352,15 +430,15 @@ async fn resume_and_fork_append_permissions_messages() -> Result<()> {
352430
let body4 = req4.single_request().body_json();
353431
let input4 = body4["input"].as_array().expect("input array");
354432
let permissions_fork = permissions_texts(input4);
355-
assert_eq!(permissions_fork.len(), permissions_base.len() + 2);
356433
assert_eq!(
357-
&permissions_fork[..permissions_base.len()],
358-
permissions_base.as_slice()
434+
permissions_fork,
435+
vec![
436+
expected_on_request,
437+
expected_never,
438+
expected_unless_trusted.clone(),
439+
expected_unless_trusted
440+
]
359441
);
360-
let new_permissions = &permissions_fork[permissions_base.len()..];
361-
assert_eq!(new_permissions.len(), 2);
362-
assert_eq!(new_permissions[0], new_permissions[1]);
363-
assert!(!permissions_base.contains(&new_permissions[0]));
364442

365443
Ok(())
366444
}

codex-rs/protocol/src/models.rs

Lines changed: 18 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
use std::collections::HashMap;
2+
use std::path::PathBuf;
23

34
use codex_utils_image::load_and_resize_to_fit;
45
use mcp_types::CallToolResult;
@@ -13,9 +14,9 @@ use crate::config_types::SandboxMode;
1314
use crate::protocol::AskForApproval;
1415
use crate::protocol::NetworkAccess;
1516
use crate::protocol::SandboxPolicy;
17+
use crate::protocol::WritableRoot;
1618
use crate::user_input::UserInput;
1719
use codex_git::GhostCommit;
18-
use codex_utils_absolute_path::AbsolutePathBuf;
1920
use codex_utils_image::error::ImageProcessingError;
2021
use schemars::JsonSchema;
2122

@@ -200,7 +201,11 @@ impl DeveloperInstructions {
200201
Self { text }
201202
}
202203

203-
pub fn from_policy(sandbox_policy: &SandboxPolicy, approval_policy: AskForApproval) -> Self {
204+
pub fn from_policy(
205+
sandbox_policy: &SandboxPolicy,
206+
approval_policy: AskForApproval,
207+
cwd: &PathBuf,
208+
) -> Self {
204209
let network_access = if sandbox_policy.has_full_network_access() {
205210
NetworkAccess::Enabled
206211
} else {
@@ -211,13 +216,9 @@ impl DeveloperInstructions {
211216
SandboxPolicy::DangerFullAccess => (SandboxMode::DangerFullAccess, None),
212217
SandboxPolicy::ReadOnly => (SandboxMode::ReadOnly, None),
213218
SandboxPolicy::ExternalSandbox { .. } => (SandboxMode::DangerFullAccess, None),
214-
SandboxPolicy::WorkspaceWrite { writable_roots, .. } => {
215-
let roots = if writable_roots.is_empty() {
216-
None
217-
} else {
218-
Some(writable_roots.clone())
219-
};
220-
(SandboxMode::WorkspaceWrite, roots)
219+
SandboxPolicy::WorkspaceWrite { .. } => {
220+
let roots = sandbox_policy.get_writable_roots_with_cwd(cwd);
221+
(SandboxMode::WorkspaceWrite, Some(roots))
221222
}
222223
};
223224

@@ -233,7 +234,7 @@ impl DeveloperInstructions {
233234
sandbox_mode: SandboxMode,
234235
network_access: NetworkAccess,
235236
approval_policy: AskForApproval,
236-
writable_roots: Option<Vec<AbsolutePathBuf>>,
237+
writable_roots: Option<Vec<WritableRoot>>,
237238
) -> Self {
238239
let start_tag = DeveloperInstructions::new("<permissions instructions>");
239240
let end_tag = DeveloperInstructions::new("</permissions instructions>");
@@ -247,7 +248,7 @@ impl DeveloperInstructions {
247248
.concat(end_tag)
248249
}
249250

250-
fn from_writable_roots(writable_roots: Option<Vec<AbsolutePathBuf>>) -> Self {
251+
fn from_writable_roots(writable_roots: Option<Vec<WritableRoot>>) -> Self {
251252
let Some(roots) = writable_roots else {
252253
return DeveloperInstructions::new("");
253254
};
@@ -258,7 +259,7 @@ impl DeveloperInstructions {
258259

259260
let roots_list: Vec<String> = roots
260261
.iter()
261-
.map(|r| format!("`{}`", r.to_string_lossy()))
262+
.map(|r| format!("`{}`", r.root.to_string_lossy()))
262263
.collect();
263264
let text = if roots_list.len() == 1 {
264265
format!(" The writable root is {}.", roots_list[0])
@@ -762,8 +763,11 @@ mod tests {
762763
exclude_slash_tmp: false,
763764
};
764765

765-
let instructions =
766-
DeveloperInstructions::from_policy(&policy, AskForApproval::UnlessTrusted);
766+
let instructions = DeveloperInstructions::from_policy(
767+
&policy,
768+
AskForApproval::UnlessTrusted,
769+
&PathBuf::from("/tmp"),
770+
);
767771
let text = instructions.into_text();
768772
assert!(text.contains("Network access is enabled."));
769773
assert!(text.contains("`approval_policy` is `unless-trusted`"));

0 commit comments

Comments
 (0)