Preserve foreign working directories in extension tool environments (#47129)

## Why

Extension calls omitted environments whose working directories could not be converted to host-native paths, preventing extensions from receiving those environments.

## What changed

Store `ToolEnvironment.cwd` as `PathUri` and pass all turn environments to extension tools without requiring host-native path conversion. Keep image generation's existing path requirements by selecting the first environment with a convertible working directory when reading referenced images or saving generated images.

GitOrigin-RevId: b4320c6e07d27ef59885cd0013e5d1c535f69659
This commit is contained in:
Adam Perry @ OpenAI
2026-09-21 23:27:42 +00:00
committed by copyberry
parent 26cb4d73e2
commit 69759baefd
5 changed files with 17 additions and 13 deletions
+1
View File
@@ -4634,6 +4634,7 @@ dependencies = [
"codex-utils-absolute-path",
"codex-utils-cargo-bin",
"codex-utils-output-truncation",
"codex-utils-path-uri",
"codex-utils-string",
"jsonptr",
"pretty_assertions",
@@ -186,11 +186,6 @@ async fn to_extension_call(invocation: &ToolInvocation) -> ExtensionToolCall<'_>
.and_then(|metadata| to_ascii_json_string(&metadata).ok());
let mut environments = Vec::new();
for environment in invocation.step_context.environments.turn_environments() {
// TODO(anp): Migrate extension ToolEnvironment and granted-permission lookup to PathUri
// so extensions can receive foreign environment cwd values.
let Ok(native_cwd) = environment.cwd().to_abs_path() else {
continue;
};
let additional_permissions = apply_granted_turn_permissions(
invocation.session.as_ref(),
environment,
@@ -204,7 +199,7 @@ async fn to_extension_call(invocation: &ToolInvocation) -> ExtensionToolCall<'_>
environments.push(ToolEnvironment {
_lifetime: PhantomData,
environment_id: environment.selection.environment_id.clone(),
cwd: native_cwd,
cwd: environment.cwd().clone(),
file_system: environment.environment.get_filesystem(),
file_system_sandbox_context,
});
+12 -5
View File
@@ -217,9 +217,13 @@ impl ImageGenerationTool {
return Err(FunctionCallError::RespondToModel(message));
}
};
// TODO(anp): Migrate image tool path arguments and saved-path events to PathUri so image
// operations can use the primary environment even when its paths are foreign to the host.
let saved_path = save_image_generation_result(
self.save_root.as_ref(),
call.environments.first(),
call.environments
.iter()
.find(|environment| environment.cwd.to_abs_path().is_ok()),
&self.thread_id,
&call.call_id,
&result,
@@ -320,7 +324,8 @@ async fn save_image_generation_result(
}
None => {
let environment = environment?;
let output_dir = environment.cwd.join("generated_images");
let cwd = environment.cwd.to_abs_path().ok()?;
let output_dir = cwd.join("generated_images");
let save_result: io::Result<AbsolutePathBuf> = async {
let result = result.trim();
if result.len() > MAX_EXECUTOR_GENERATED_IMAGE_BASE64_BYTES {
@@ -339,8 +344,7 @@ async fn save_image_generation_result(
));
}
let artifact_path =
image_generation_artifact_path(&environment.cwd, session_id, call_id);
let artifact_path = image_generation_artifact_path(&cwd, session_id, call_id);
let path = output_dir.join(artifact_path.as_path().file_name().unwrap_or_default());
let sandbox = Some(&environment.file_system_sandbox_context);
if let Some(parent) = path.parent() {
@@ -440,7 +444,10 @@ async fn request_for_call_args(
}));
}
(false, None) => {
let Some(environment) = environments.first() else {
let Some(environment) = environments
.iter()
.find(|environment| environment.cwd.to_abs_path().is_ok())
else {
return Err(FunctionCallError::RespondToModel(
"referenced image paths are unavailable in this session".to_string(),
));
+1
View File
@@ -17,6 +17,7 @@ codex-extension-items = { workspace = true }
codex-protocol = { workspace = true }
codex-utils-absolute-path = { workspace = true }
codex-utils-output-truncation = { workspace = true }
codex-utils-path-uri = { workspace = true }
codex-utils-string = { workspace = true }
jsonptr = { workspace = true }
rmcp = { workspace = true, default-features = false, features = [
+2 -2
View File
@@ -6,9 +6,9 @@ use codex_file_system::ExecutorFileSystem;
use codex_file_system::FileSystemSandboxContext;
use codex_protocol::models::ResponseItem;
use codex_protocol::protocol::EventMsg;
use codex_utils_absolute_path::AbsolutePathBuf;
use codex_utils_output_truncation::TruncationPolicy;
use codex_utils_output_truncation::with_serialization_allowance;
use codex_utils_path_uri::PathUri;
use std::fmt;
use std::future::Future;
use std::marker::PhantomData;
@@ -91,7 +91,7 @@ pub struct ToolEnvironment<'call> {
/// Stable host environment id used to route executor-scoped capabilities.
pub environment_id: String,
/// Effective working directory for this turn in the environment.
pub cwd: AbsolutePathBuf,
pub cwd: PathUri,
/// Filesystem implementation for this environment.
pub file_system: Arc<dyn ExecutorFileSystem>,
/// Sandbox context to use for filesystem operations.