Skip to content

Commit d5ec93f

Browse files
authored
Move memories root setup out of core config (#24758)
## Why Config loading should not create or write-authorize the memories root just because memory support exists. Memory startup is the code path that actually materializes that tree. ## What - Stop creating the memories root during Config load and remove it from legacy workspace-write projections. - Grant the memories root read access only when the memories feature and use_memories are enabled. - Create the memories root inside memories startup before seeding extension instructions. - Update config and startup tests around the ownership boundary. ## Tests - just fmt - just fix -p codex-core - just fix -p codex-memories-write - just test -p codex-core memory_tool_makes_memories_root_readable_without_creating_or_widening_writes workspace_write_includes_configured_writable_root_once_without_memories_root permission_profile_override_keeps_memories_root_out_of_legacy_projection permissions_profiles_allow_direct_write_roots_outside_workspace_root default_permissions_profile_populates_runtime_sandbox_policy - just test -p codex-memories-write memories_startup_creates_memory_root Note: a broader just test -p codex-core run is not clean in this sandbox; it hit missing test_stdio_server plus seatbelt, realtime, and environment-sensitive failures. The changed config tests above pass.
1 parent 46946bb commit d5ec93f

5 files changed

Lines changed: 114 additions & 99 deletions

File tree

codex-rs/app-server/tests/suite/v2/remote_thread_store.rs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -228,7 +228,6 @@ fn assert_no_local_persistence_artifacts(codex_home: &Path) -> Result<()> {
228228
BTreeSet::from([
229229
"config.toml".to_string(),
230230
"installation_id".to_string(),
231-
"memories".to_string(),
232231
"skills".to_string(),
233232
]),
234233
"non-local thread persistence should not create unexpected files in codex_home"

codex-rs/core/src/config/config_tests.rs

Lines changed: 70 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1554,7 +1554,6 @@ async fn default_permissions_profile_populates_runtime_sandbox_policy() -> std::
15541554
.await?;
15551555

15561556
let cwd_root = cwd.path().abs();
1557-
let memories_root = codex_home.path().join("memories").abs();
15581557
assert_eq!(
15591558
config.permissions.file_system_sandbox_policy(),
15601559
FileSystemSandboxPolicy::restricted(vec![
@@ -1576,18 +1575,12 @@ async fn default_permissions_profile_populates_runtime_sandbox_policy() -> std::
15761575
},
15771576
access: FileSystemAccessMode::Read,
15781577
},
1579-
FileSystemSandboxEntry {
1580-
path: FileSystemPath::Path {
1581-
path: memories_root.clone(),
1582-
},
1583-
access: FileSystemAccessMode::Write,
1584-
},
15851578
]),
15861579
);
15871580
assert_eq!(
15881581
&config.legacy_sandbox_policy(),
15891582
&SandboxPolicy::WorkspaceWrite {
1590-
writable_roots: vec![memories_root],
1583+
writable_roots: vec![],
15911584
network_access: false,
15921585
exclude_tmpdir_env_var: true,
15931586
exclude_slash_tmp: true,
@@ -1816,7 +1809,7 @@ async fn managed_unrestricted_permission_profile_still_enables_network_requireme
18161809
}
18171810

18181811
#[tokio::test]
1819-
async fn permission_profile_override_applies_runtime_roots_to_legacy_projection()
1812+
async fn permission_profile_override_keeps_memories_root_out_of_legacy_projection()
18201813
-> std::io::Result<()> {
18211814
let codex_home = TempDir::new()?;
18221815
let cwd = TempDir::new()?;
@@ -1851,15 +1844,15 @@ async fn permission_profile_override_applies_runtime_roots_to_legacy_projection(
18511844

18521845
let memories_root = codex_home.path().join("memories").abs();
18531846
assert!(
1854-
config
1847+
!config
18551848
.permissions
18561849
.file_system_sandbox_policy()
18571850
.can_write_path_with_cwd(memories_root.as_path(), cwd.path())
18581851
);
18591852
assert_eq!(
18601853
&config.legacy_sandbox_policy(),
18611854
&SandboxPolicy::WorkspaceWrite {
1862-
writable_roots: vec![memories_root],
1855+
writable_roots: vec![],
18631856
network_access: false,
18641857
exclude_tmpdir_env_var: true,
18651858
exclude_slash_tmp: true,
@@ -2757,9 +2750,6 @@ async fn permissions_profiles_allow_direct_write_roots_outside_workspace_root()
27572750
description: Some("Workspace access.".to_string()),
27582751
}]
27592752
);
2760-
let memories_root = AbsolutePathBuf::from_absolute_path(std::fs::canonicalize(
2761-
codex_home.path().join("memories"),
2762-
)?)?;
27632753
assert!(
27642754
config
27652755
.permissions
@@ -2769,7 +2759,7 @@ async fn permissions_profiles_allow_direct_write_roots_outside_workspace_root()
27692759
assert_eq!(
27702760
&config.legacy_sandbox_policy(),
27712761
&SandboxPolicy::WorkspaceWrite {
2772-
writable_roots: vec![external_write_path, memories_root],
2762+
writable_roots: vec![external_write_path],
27732763
network_access: false,
27742764
exclude_tmpdir_env_var: true,
27752765
exclude_slash_tmp: true,
@@ -4514,13 +4504,15 @@ async fn sqlite_home_defaults_to_codex_home_for_workspace_write() -> std::io::Re
45144504
}
45154505

45164506
#[tokio::test]
4517-
async fn workspace_write_always_includes_memories_root_once() -> std::io::Result<()> {
4507+
async fn workspace_write_includes_configured_writable_root_once_without_memories_root()
4508+
-> std::io::Result<()> {
45184509
let codex_home = TempDir::new()?;
45194510
let memories_root = codex_home.path().join("memories");
4511+
let writable_root = codex_home.path().join("writable").abs();
45204512
let config = Config::load_from_base_config_with_overrides(
45214513
ConfigToml {
45224514
sandbox_workspace_write: Some(SandboxWorkspaceWrite {
4523-
writable_roots: vec![memories_root.abs()],
4515+
writable_roots: vec![writable_root.clone(), writable_root.clone()],
45244516
..Default::default()
45254517
}),
45264518
..Default::default()
@@ -4540,21 +4532,22 @@ async fn workspace_write_always_includes_memories_root_once() -> std::io::Result
45404532
}
45414533
} else {
45424534
assert!(
4543-
memories_root.is_dir(),
4544-
"expected memories root directory to exist at {}",
4535+
!memories_root.exists(),
4536+
"expected config load not to create memories root at {}",
45454537
memories_root.display()
45464538
);
45474539
let expected_memories_root = memories_root.abs();
45484540
match &config.legacy_sandbox_policy() {
45494541
SandboxPolicy::WorkspaceWrite { writable_roots, .. } => {
4542+
assert!(!writable_roots.contains(&expected_memories_root));
45504543
assert_eq!(
45514544
writable_roots
45524545
.iter()
4553-
.filter(|root| **root == expected_memories_root)
4546+
.filter(|root| **root == writable_root)
45544547
.count(),
45554548
1,
45564549
"expected single writable root entry for {}",
4557-
expected_memories_root.display()
4550+
writable_root.display()
45584551
);
45594552
}
45604553
other => panic!("expected workspace-write policy, got {other:?}"),
@@ -4564,6 +4557,62 @@ async fn workspace_write_always_includes_memories_root_once() -> std::io::Result
45644557
Ok(())
45654558
}
45664559

4560+
#[tokio::test]
4561+
async fn memory_tool_makes_memories_root_readable_without_creating_or_widening_writes()
4562+
-> std::io::Result<()> {
4563+
let codex_home = TempDir::new()?;
4564+
let cwd = TempDir::new()?;
4565+
let memories_root = codex_home.path().join("memories");
4566+
let memories_root_abs = memories_root.abs();
4567+
4568+
let config = Config::load_from_base_config_with_overrides(
4569+
ConfigToml {
4570+
features: Some(FeaturesToml::from(BTreeMap::from([(
4571+
"memories".to_string(),
4572+
true,
4573+
)]))),
4574+
sandbox_workspace_write: Some(SandboxWorkspaceWrite {
4575+
exclude_tmpdir_env_var: true,
4576+
exclude_slash_tmp: true,
4577+
..Default::default()
4578+
}),
4579+
..Default::default()
4580+
},
4581+
ConfigOverrides {
4582+
cwd: Some(cwd.path().to_path_buf()),
4583+
sandbox_mode: Some(SandboxMode::WorkspaceWrite),
4584+
..Default::default()
4585+
},
4586+
codex_home.abs(),
4587+
)
4588+
.await?;
4589+
4590+
assert!(
4591+
!memories_root.exists(),
4592+
"expected config load not to create memories root at {}",
4593+
memories_root.display()
4594+
);
4595+
let file_system_policy = config.permissions.file_system_sandbox_policy();
4596+
assert!(file_system_policy.can_read_path_with_cwd(memories_root_abs.as_path(), cwd.path()));
4597+
assert!(!file_system_policy.can_write_path_with_cwd(memories_root_abs.as_path(), cwd.path()));
4598+
4599+
if cfg!(target_os = "windows") {
4600+
match &config.legacy_sandbox_policy() {
4601+
SandboxPolicy::ReadOnly { .. } => {}
4602+
other => panic!("expected read-only policy on Windows, got {other:?}"),
4603+
}
4604+
} else {
4605+
match &config.legacy_sandbox_policy() {
4606+
SandboxPolicy::WorkspaceWrite { writable_roots, .. } => {
4607+
assert!(!writable_roots.contains(&memories_root_abs));
4608+
}
4609+
other => panic!("expected workspace-write policy, got {other:?}"),
4610+
}
4611+
}
4612+
4613+
Ok(())
4614+
}
4615+
45674616
#[tokio::test]
45684617
async fn config_defaults_to_file_cli_auth_store_mode() -> std::io::Result<()> {
45694618
let codex_home = TempDir::new()?;

codex-rs/core/src/config/mod.rs

Lines changed: 10 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -2631,9 +2631,8 @@ impl Config {
26312631
Some(WindowsSandboxModeToml::Unelevated) => WindowsSandboxLevel::RestrictedToken,
26322632
None => WindowsSandboxLevel::from_features(&features),
26332633
};
2634+
let memories_config: MemoriesConfig = cfg.memories.clone().unwrap_or_default().into();
26342635
let memories_root = memory_root(&codex_home);
2635-
std::fs::create_dir_all(&memories_root)?;
2636-
let internal_writable_roots = vec![memories_root];
26372636

26382637
let profiles_are_active = effective_permission_selection.profiles_are_active(
26392638
default_permissions_override.as_deref(),
@@ -2701,8 +2700,8 @@ impl Config {
27012700
file_system_sandbox_policy,
27022701
mut active_permission_profile,
27032702
mut profile_workspace_roots,
2704-
) = if let Some(mut permission_profile) = permission_profile {
2705-
let (mut file_system_sandbox_policy, network_sandbox_policy) =
2703+
) = if let Some(permission_profile) = permission_profile {
2704+
let (file_system_sandbox_policy, _network_sandbox_policy) =
27062705
permission_profile.to_runtime_permissions();
27072706
let configured_network_proxy_config =
27082707
if profile_allows_configured_network_proxy(&permission_profile)
@@ -2726,30 +2725,6 @@ impl Config {
27262725
} else {
27272726
NetworkProxyConfig::default()
27282727
};
2729-
let materialized_file_system_sandbox_policy = file_system_sandbox_policy
2730-
.clone()
2731-
.materialize_project_roots_with_workspace_roots(&workspace_roots);
2732-
let materialized_permission_profile =
2733-
PermissionProfile::from_runtime_permissions_with_enforcement(
2734-
permission_profile.enforcement(),
2735-
&materialized_file_system_sandbox_policy,
2736-
network_sandbox_policy,
2737-
);
2738-
let sandbox_policy = compatibility_sandbox_policy_for_permission_profile(
2739-
&materialized_permission_profile,
2740-
&materialized_file_system_sandbox_policy,
2741-
network_sandbox_policy,
2742-
resolved_cwd.as_path(),
2743-
);
2744-
if matches!(sandbox_policy, SandboxPolicy::WorkspaceWrite { .. }) {
2745-
file_system_sandbox_policy = file_system_sandbox_policy
2746-
.with_additional_legacy_workspace_writable_roots(&internal_writable_roots);
2747-
permission_profile = PermissionProfile::from_runtime_permissions_with_enforcement(
2748-
permission_profile.enforcement(),
2749-
&file_system_sandbox_policy,
2750-
network_sandbox_policy,
2751-
);
2752-
}
27532728
(
27542729
configured_network_proxy_config,
27552730
permission_profile,
@@ -2794,7 +2769,7 @@ impl Config {
27942769
dedupe_absolute_paths(&mut configured_workspace_roots);
27952770
file_system_sandbox_policy = file_system_sandbox_policy
27962771
.with_materialized_project_roots_for_workspace_roots(&configured_workspace_roots);
2797-
let mut permission_profile = if let Some(permission_profile) =
2772+
let permission_profile = if let Some(permission_profile) =
27982773
builtin_permission_profile(default_permissions, builtin_workspace_write_settings)
27992774
{
28002775
permission_profile
@@ -2804,30 +2779,6 @@ impl Config {
28042779
network_sandbox_policy,
28052780
)
28062781
};
2807-
let materialized_file_system_sandbox_policy = file_system_sandbox_policy
2808-
.clone()
2809-
.materialize_project_roots_with_workspace_roots(&workspace_roots);
2810-
let materialized_permission_profile =
2811-
PermissionProfile::from_runtime_permissions_with_enforcement(
2812-
permission_profile.enforcement(),
2813-
&materialized_file_system_sandbox_policy,
2814-
network_sandbox_policy,
2815-
);
2816-
let sandbox_policy = compatibility_sandbox_policy_for_permission_profile(
2817-
&materialized_permission_profile,
2818-
&materialized_file_system_sandbox_policy,
2819-
network_sandbox_policy,
2820-
resolved_cwd.as_path(),
2821-
);
2822-
if matches!(sandbox_policy, SandboxPolicy::WorkspaceWrite { .. }) {
2823-
file_system_sandbox_policy = file_system_sandbox_policy
2824-
.with_additional_legacy_workspace_writable_roots(&internal_writable_roots);
2825-
permission_profile = PermissionProfile::from_runtime_permissions_with_enforcement(
2826-
permission_profile.enforcement(),
2827-
&file_system_sandbox_policy,
2828-
network_sandbox_policy,
2829-
);
2830-
}
28312782
let active_permission_profile = if using_implicit_builtin_profile
28322783
&& default_permissions == BUILT_IN_WORKSPACE_PROFILE
28332784
&& cfg.sandbox_workspace_write.is_some()
@@ -2885,29 +2836,8 @@ impl Config {
28852836
);
28862837
permission_profile = PermissionProfile::read_only();
28872838
}
2888-
let (mut file_system_sandbox_policy, network_sandbox_policy) =
2839+
let (file_system_sandbox_policy, _network_sandbox_policy) =
28892840
permission_profile.to_runtime_permissions();
2890-
let materialized_file_system_sandbox_policy = permission_profile
2891-
.clone()
2892-
.materialize_project_roots_with_workspace_roots(&workspace_roots)
2893-
.file_system_sandbox_policy();
2894-
if matches!(permission_profile.enforcement(), SandboxEnforcement::Managed)
2895-
&& materialized_file_system_sandbox_policy.can_write_path_with_cwd(
2896-
resolved_cwd.as_path(),
2897-
resolved_cwd.as_path(),
2898-
)
2899-
&& !materialized_file_system_sandbox_policy.has_full_disk_write_access()
2900-
{
2901-
// Keep Codex runtime write access while storing the runtime
2902-
// workspace roots separately on the thread.
2903-
file_system_sandbox_policy = file_system_sandbox_policy
2904-
.with_additional_legacy_workspace_writable_roots(&internal_writable_roots);
2905-
permission_profile = PermissionProfile::from_runtime_permissions_with_enforcement(
2906-
permission_profile.enforcement(),
2907-
&file_system_sandbox_policy,
2908-
network_sandbox_policy,
2909-
);
2910-
}
29112841
(
29122842
configured_network_proxy_config,
29132843
permission_profile,
@@ -3324,11 +3254,14 @@ impl Config {
33243254
network_requirements,
33253255
&network_permission_profile,
33263256
)?;
3327-
let helper_readable_roots = get_readable_roots_required_for_codex_runtime(
3257+
let mut helper_readable_roots = get_readable_roots_required_for_codex_runtime(
33283258
&codex_home,
33293259
zsh_path.as_ref(),
33303260
main_execve_wrapper_exe.as_ref(),
33313261
);
3262+
if features.enabled(Feature::MemoryTool) && memories_config.use_memories {
3263+
helper_readable_roots.push(memories_root);
3264+
}
33323265
let effective_permission_profile = constrained_permission_profile.value.get().clone();
33333266
let (mut effective_file_system_sandbox_policy, effective_network_sandbox_policy) =
33343267
effective_permission_profile.to_runtime_permissions();
@@ -3438,7 +3371,7 @@ impl Config {
34383371
agent_max_threads,
34393372
agent_max_depth,
34403373
agent_roles,
3441-
memories: cfg.memories.unwrap_or_default().into(),
3374+
memories: memories_config,
34423375
agent_job_max_runtime_seconds,
34433376
agent_interrupt_message_enabled,
34443377
codex_home,

codex-rs/memories/write/src/start.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,10 @@ pub fn start_memories_startup_task(
5050

5151
tokio::spawn(async move {
5252
let root = memory_root(&config.codex_home);
53+
if let Err(err) = tokio::fs::create_dir_all(&root).await {
54+
warn!("failed creating memories root: {err}");
55+
return;
56+
}
5357
if let Err(err) = seed_extension_instructions(&root).await {
5458
warn!("failed seeding memory extension instructions: {err}");
5559
}

codex-rs/memories/write/src/startup_tests.rs

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,21 @@ use tempfile::TempDir;
2626
use tokio::time::Duration;
2727
use tokio::time::Instant;
2828

29+
#[tokio::test]
30+
async fn memories_startup_creates_memory_root() -> anyhow::Result<()> {
31+
let server = start_mock_server().await;
32+
let home = Arc::new(TempDir::new()?);
33+
let memory_root = home.path().join("memories");
34+
let test = build_test_codex(&server, home).await?;
35+
36+
assert!(!memory_root.exists());
37+
trigger_memories_startup(&test).await;
38+
wait_for_dir(&memory_root).await?;
39+
40+
shutdown_test_codex(&test).await?;
41+
Ok(())
42+
}
43+
2944
#[tokio::test]
3045
async fn memories_startup_phase2_tracks_workspace_diff_across_runs() -> anyhow::Result<()> {
3146
let server = start_mock_server().await;
@@ -408,6 +423,21 @@ async fn wait_for_file_removed(path: &Path) -> anyhow::Result<()> {
408423
}
409424
}
410425

426+
async fn wait_for_dir(path: &Path) -> anyhow::Result<()> {
427+
let deadline = Instant::now() + Duration::from_secs(10);
428+
loop {
429+
if tokio::fs::try_exists(path).await? && path.is_dir() {
430+
return Ok(());
431+
}
432+
assert!(
433+
Instant::now() < deadline,
434+
"timed out waiting for {} to be created",
435+
path.display()
436+
);
437+
tokio::time::sleep(Duration::from_millis(50)).await;
438+
}
439+
}
440+
411441
async fn wait_for_request(mock: &ResponseMock, expected_count: usize) -> Vec<ResponsesRequest> {
412442
let deadline = Instant::now() + Duration::from_secs(10);
413443
loop {

0 commit comments

Comments
 (0)