Skip to content

Commit cc85cac

Browse files
caseychow-oaicodex
andcommitted
Scope OpenAI file temp roots per session
Co-authored-by: Codex <noreply@openai.com>
1 parent b48bece commit cc85cac

File tree

5 files changed

+113
-4
lines changed

5 files changed

+113
-4
lines changed

codex-rs/core/src/codex.rs

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -230,6 +230,7 @@ use crate::mentions::collect_explicit_app_ids;
230230
use crate::mentions::collect_explicit_plugin_mentions;
231231
use crate::mentions::collect_tool_mentions_from_messages;
232232
use crate::network_policy_decision::execpolicy_network_rule_amendment;
233+
use crate::openai_files::managed_download_root_for_session;
233234
use crate::plugins::PluginsManager;
234235
use crate::plugins::build_plugin_injections;
235236
use crate::plugins::render_plugins_section;
@@ -1153,6 +1154,41 @@ pub(crate) struct SessionSettingsUpdate {
11531154
}
11541155

11551156
impl Session {
1157+
fn add_session_openai_file_root_to_sandbox(
1158+
session_configuration: &mut SessionConfiguration,
1159+
conversation_id: ThreadId,
1160+
) -> anyhow::Result<()> {
1161+
let session_openai_file_root =
1162+
managed_download_root_for_session(&conversation_id.to_string());
1163+
let session_openai_file_root =
1164+
AbsolutePathBuf::from_absolute_path(&session_openai_file_root).map_err(|error| {
1165+
anyhow::anyhow!(
1166+
"failed to resolve session OpenAI file root `{}`: {error}",
1167+
session_openai_file_root.display()
1168+
)
1169+
})?;
1170+
session_configuration.file_system_sandbox_policy = session_configuration
1171+
.file_system_sandbox_policy
1172+
.clone()
1173+
.with_additional_readable_roots(
1174+
&session_configuration.cwd,
1175+
std::slice::from_ref(&session_openai_file_root),
1176+
);
1177+
if matches!(
1178+
session_configuration.sandbox_policy.get(),
1179+
SandboxPolicy::WorkspaceWrite { .. }
1180+
) {
1181+
session_configuration.file_system_sandbox_policy = session_configuration
1182+
.file_system_sandbox_policy
1183+
.clone()
1184+
.with_additional_writable_roots(
1185+
&session_configuration.cwd,
1186+
std::slice::from_ref(&session_openai_file_root),
1187+
);
1188+
}
1189+
Ok(())
1190+
}
1191+
11561192
/// Builds the `x-codex-beta-features` header value for this session.
11571193
///
11581194
/// `ModelClient` is session-scoped and intentionally does not depend on the full `Config`, so
@@ -1462,6 +1498,7 @@ impl Session {
14621498
),
14631499
InitialHistory::New | InitialHistory::Forked(_) => None,
14641500
};
1501+
Self::add_session_openai_file_root_to_sandbox(&mut session_configuration, conversation_id)?;
14651502

14661503
// Kick off independent async setup tasks in parallel to reduce startup latency.
14671504
//

codex-rs/core/src/codex_tests.rs

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ use crate::exec::ExecToolCallOutput;
1212
use crate::function_tool::FunctionCallError;
1313
use crate::mcp_connection_manager::ToolInfo;
1414
use crate::models_manager::model_info;
15+
use crate::openai_files::managed_download_root_for_session;
1516
use crate::shell::default_user_shell;
1617
use crate::tools::format_exec_output_str;
1718

@@ -2156,6 +2157,57 @@ async fn session_configuration_apply_preserves_split_file_system_policy_on_cwd_o
21562157
);
21572158
}
21582159

2160+
#[tokio::test]
2161+
async fn session_openai_file_root_is_scoped_to_the_current_session() {
2162+
let mut session_configuration = make_session_configuration_for_tests().await;
2163+
session_configuration.sandbox_policy =
2164+
codex_config::Constrained::allow_any(SandboxPolicy::WorkspaceWrite {
2165+
writable_roots: Vec::new(),
2166+
read_only_access: ReadOnlyAccess::Restricted {
2167+
include_platform_defaults: true,
2168+
readable_roots: Vec::new(),
2169+
},
2170+
network_access: false,
2171+
exclude_tmpdir_env_var: true,
2172+
exclude_slash_tmp: true,
2173+
});
2174+
session_configuration.file_system_sandbox_policy =
2175+
FileSystemSandboxPolicy::restricted(vec![FileSystemSandboxEntry {
2176+
path: FileSystemPath::Special {
2177+
value: FileSystemSpecialPath::CurrentWorkingDirectory,
2178+
},
2179+
access: FileSystemAccessMode::Write,
2180+
}]);
2181+
2182+
let session_id = ThreadId::new();
2183+
let session_root = managed_download_root_for_session(&session_id.to_string());
2184+
let other_session_root = managed_download_root_for_session(&ThreadId::new().to_string());
2185+
2186+
Session::add_session_openai_file_root_to_sandbox(&mut session_configuration, session_id)
2187+
.expect("session OpenAI file root should be added to sandbox");
2188+
2189+
assert!(
2190+
session_configuration
2191+
.file_system_sandbox_policy
2192+
.can_read_path_with_cwd(&session_root, &session_configuration.cwd)
2193+
);
2194+
assert!(
2195+
session_configuration
2196+
.file_system_sandbox_policy
2197+
.can_write_path_with_cwd(&session_root, &session_configuration.cwd)
2198+
);
2199+
assert!(
2200+
!session_configuration
2201+
.file_system_sandbox_policy
2202+
.can_read_path_with_cwd(&other_session_root, &session_configuration.cwd)
2203+
);
2204+
assert!(
2205+
!session_configuration
2206+
.file_system_sandbox_policy
2207+
.can_write_path_with_cwd(&other_session_root, &session_configuration.cwd)
2208+
);
2209+
}
2210+
21592211
#[cfg_attr(windows, ignore)]
21602212
#[tokio::test]
21612213
async fn new_default_turn_uses_config_aware_skills_for_role_overrides() {

codex-rs/core/src/mcp_tool_call.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1089,6 +1089,7 @@ async fn rewrite_mcp_tool_result_for_openai_files(
10891089
continue;
10901090
};
10911091
rewrite_output_value_for_openai_files(
1092+
sess,
10921093
turn_context,
10931094
auth.as_ref(),
10941095
call_id,
@@ -1102,6 +1103,7 @@ async fn rewrite_mcp_tool_result_for_openai_files(
11021103
}
11031104

11041105
async fn rewrite_output_value_for_openai_files(
1106+
sess: &Session,
11051107
turn_context: &TurnContext,
11061108
auth: Option<&crate::CodexAuth>,
11071109
call_id: &str,
@@ -1111,6 +1113,7 @@ async fn rewrite_output_value_for_openai_files(
11111113
match value {
11121114
serde_json::Value::String(file_ref) => {
11131115
if let Some(downloaded_path) = auto_download_openai_file_value(
1116+
sess,
11141117
turn_context,
11151118
auth,
11161119
call_id,
@@ -1128,6 +1131,7 @@ async fn rewrite_output_value_for_openai_files(
11281131
continue;
11291132
};
11301133
if let Some(downloaded_path) = auto_download_openai_file_value(
1134+
sess,
11311135
turn_context,
11321136
auth,
11331137
call_id,
@@ -1145,6 +1149,7 @@ async fn rewrite_output_value_for_openai_files(
11451149
}
11461150

11471151
async fn auto_download_openai_file_value(
1152+
sess: &Session,
11481153
turn_context: &TurnContext,
11491154
auth: Option<&crate::CodexAuth>,
11501155
call_id: &str,
@@ -1160,6 +1165,7 @@ async fn auto_download_openai_file_value(
11601165
turn_context.config.as_ref(),
11611166
auth,
11621167
file_ref,
1168+
&sess.conversation_id.to_string(),
11631169
call_id,
11641170
max_bytes,
11651171
)

codex-rs/core/src/openai_files.rs

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -379,6 +379,7 @@ pub(crate) async fn download_file_to_managed_temp(
379379
config: &Config,
380380
auth: Option<&CodexAuth>,
381381
reference: &str,
382+
session_id: &str,
382383
scope: &str,
383384
max_bytes: u64,
384385
) -> Result<DownloadedOpenAiFile, OpenAiFileError> {
@@ -393,7 +394,7 @@ pub(crate) async fn download_file_to_managed_temp(
393394
});
394395
}
395396

396-
let download_dir = managed_download_dir(scope)?;
397+
let download_dir = managed_download_dir(session_id, scope)?;
397398

398399
let file_name = sanitize_download_file_name(
399400
resolved
@@ -483,8 +484,17 @@ pub(crate) async fn download_file_to_managed_temp(
483484
})
484485
}
485486

486-
pub(crate) fn managed_download_dir(scope: &str) -> Result<PathBuf, OpenAiFileError> {
487-
let parent = std::env::temp_dir().join("codex-openai-files").join(scope);
487+
pub(crate) fn managed_download_root_for_session(session_id: &str) -> PathBuf {
488+
std::env::temp_dir()
489+
.join("codex-openai-files")
490+
.join(sanitize_download_file_name(session_id))
491+
}
492+
493+
pub(crate) fn managed_download_dir(
494+
session_id: &str,
495+
scope: &str,
496+
) -> Result<PathBuf, OpenAiFileError> {
497+
let parent = managed_download_root_for_session(session_id).join(scope);
488498
std::fs::create_dir_all(&parent).map_err(|source| OpenAiFileError::CreateDirectory {
489499
path: parent.clone(),
490500
source,
@@ -658,6 +668,7 @@ mod tests {
658668
&test_config_for(&server),
659669
Some(&chatgpt_auth()),
660670
"sediment://file_123",
671+
"session-1",
661672
"call-1",
662673
OPENAI_FILE_DOWNLOAD_LIMIT_BYTES,
663674
)
@@ -670,7 +681,7 @@ mod tests {
670681
assert!(
671682
downloaded
672683
.destination_path
673-
.starts_with(std::env::temp_dir().join("codex-openai-files/call-1"))
684+
.starts_with(std::env::temp_dir().join("codex-openai-files/session-1/call-1"))
674685
);
675686
assert_eq!(
676687
tokio::fs::read_to_string(&downloaded.destination_path)
@@ -699,6 +710,7 @@ mod tests {
699710
&test_config_for(&server),
700711
Some(&chatgpt_auth()),
701712
"file_123",
713+
"session-2",
702714
"call-2",
703715
128,
704716
)
@@ -739,6 +751,7 @@ mod tests {
739751
&test_config_for(&server),
740752
Some(&auth),
741753
"sediment://file_123",
754+
"session-3",
742755
"call-3",
743756
OPENAI_FILE_DOWNLOAD_LIMIT_BYTES,
744757
)

codex-rs/core/src/tools/handlers/openai_file.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ impl ToolHandler for DownloadOpenAiFileHandler {
6060
turn.config.as_ref(),
6161
auth.as_ref(),
6262
&args.file_id,
63+
&session.conversation_id.to_string(),
6364
&unique_manual_download_scope(),
6465
OPENAI_FILE_DOWNLOAD_LIMIT_BYTES,
6566
)

0 commit comments

Comments
 (0)