diff --git a/codex-rs/Cargo.lock b/codex-rs/Cargo.lock index d94daa0e8f..6e2e7d65d9 100644 --- a/codex-rs/Cargo.lock +++ b/codex-rs/Cargo.lock @@ -2844,6 +2844,7 @@ dependencies = [ "codex-utils-cargo-bin", "codex-utils-cli", "codex-utils-oss", + "codex-utils-path-uri", "codex-utils-sandbox-summary", "core_test_support", "libc", diff --git a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json index 6b37f7c45e..719abb3ce3 100644 --- a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json +++ b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json @@ -19041,7 +19041,7 @@ "description": "Reviewer currently used for approval requests on this thread." }, "cwd": { - "$ref": "#/definitions/v2/AbsolutePathBuf" + "$ref": "#/definitions/v2/ApiPathString" }, "instructionSources": { "default": [], diff --git a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json index f36b90fffa..508935e020 100644 --- a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json +++ b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json @@ -16849,7 +16849,7 @@ "description": "Reviewer currently used for approval requests on this thread." }, "cwd": { - "$ref": "#/definitions/AbsolutePathBuf" + "$ref": "#/definitions/ApiPathString" }, "instructionSources": { "default": [], diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadStartResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadStartResponse.json index 33373b9f72..5b8be8f50a 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadStartResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadStartResponse.json @@ -28,6 +28,9 @@ "AgentPath": { "type": "string" }, + "ApiPathString": { + "type": "string" + }, "ApprovalsReviewer": { "description": "Configures who approval requests are routed to for review. Examples include sandbox escapes, blocked network access, MCP approval prompts, and ARC escalations. Defaults to `user`. `auto_review` uses a carefully prompted subagent to gather relevant context and apply a risk-based decision framework before approving or denying the request. The legacy value `guardian_subagent` is accepted for compatibility.", "enum": [ @@ -2319,7 +2322,7 @@ "description": "Reviewer currently used for approval requests on this thread." }, "cwd": { - "$ref": "#/definitions/AbsolutePathBuf" + "$ref": "#/definitions/ApiPathString" }, "instructionSources": { "default": [], diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/ThreadStartResponse.ts b/codex-rs/app-server-protocol/schema/typescript/v2/ThreadStartResponse.ts index 38859a3805..85929eefe3 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/ThreadStartResponse.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/ThreadStartResponse.ts @@ -2,13 +2,14 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. import type { AbsolutePathBuf } from "../AbsolutePathBuf"; +import type { ApiPathString } from "../ApiPathString"; import type { ReasoningEffort } from "../ReasoningEffort"; import type { ApprovalsReviewer } from "./ApprovalsReviewer"; import type { AskForApproval } from "./AskForApproval"; import type { SandboxPolicy } from "./SandboxPolicy"; import type { Thread } from "./Thread"; -export type ThreadStartResponse = {thread: Thread, model: string, modelProvider: string, serviceTier: string | null, cwd: AbsolutePathBuf, /** +export type ThreadStartResponse = {thread: Thread, model: string, modelProvider: string, serviceTier: string | null, cwd: ApiPathString, /** * Instruction source files currently loaded for this thread. */ instructionSources: Array, approvalPolicy: AskForApproval, /** diff --git a/codex-rs/app-server-protocol/src/protocol/common.rs b/codex-rs/app-server-protocol/src/protocol/common.rs index 7611c27ff1..49a5b5671e 100644 --- a/codex-rs/app-server-protocol/src/protocol/common.rs +++ b/codex-rs/app-server-protocol/src/protocol/common.rs @@ -2516,7 +2516,10 @@ mod tests { model: "gpt-5".to_string(), model_provider: "openai".to_string(), service_tier: None, - cwd, + cwd: codex_utils_path_uri::ApiPathString::from_abs_path( + &cwd, + codex_utils_path_uri::PathConvention::native(), + )?, runtime_workspace_roots: Vec::new(), instruction_sources: vec![absolute_path("/tmp/AGENTS.md")], approval_policy: v2::AskForApproval::OnFailure, diff --git a/codex-rs/app-server-protocol/src/protocol/v2/thread.rs b/codex-rs/app-server-protocol/src/protocol/v2/thread.rs index 0252c4b048..b990662349 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2/thread.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2/thread.rs @@ -26,6 +26,7 @@ use codex_protocol::protocol::ThreadGoalStatus as CoreThreadGoalStatus; use codex_protocol::protocol::TokenUsage as CoreTokenUsage; use codex_protocol::protocol::TokenUsageInfo as CoreTokenUsageInfo; use codex_utils_absolute_path::AbsolutePathBuf; +use codex_utils_path_uri::ApiPathString; use schemars::JsonSchema; use serde::Deserialize; use serde::Serialize; @@ -155,7 +156,7 @@ pub struct ThreadStartResponse { pub model: String, pub model_provider: String, pub service_tier: Option, - pub cwd: AbsolutePathBuf, + pub cwd: ApiPathString, /// Thread-scoped runtime workspace roots used to materialize /// `:workspace_roots`. #[experimental("thread/start.runtimeWorkspaceRoots")] diff --git a/codex-rs/app-server/src/request_processors/thread_processor.rs b/codex-rs/app-server/src/request_processors/thread_processor.rs index 747e1a091b..ff9ae13947 100644 --- a/codex-rs/app-server/src/request_processors/thread_processor.rs +++ b/codex-rs/app-server/src/request_processors/thread_processor.rs @@ -4,6 +4,8 @@ use codex_app_server_protocol::SelectedCapabilityRoot; use codex_extension_api::ExtensionDataInit; use codex_protocol::models::BUILT_IN_PERMISSION_PROFILE_DANGER_FULL_ACCESS; use codex_protocol::models::BUILT_IN_PERMISSION_PROFILE_WORKSPACE; +use codex_utils_path_uri::ApiPathString; +use codex_utils_path_uri::PathUri; const THREAD_LIST_DEFAULT_LIMIT: usize = 25; const THREAD_LIST_MAX_LIMIT: usize = 100; @@ -1230,7 +1232,21 @@ impl ThreadRequestProcessor { &config_snapshot.permission_profile, config_snapshot.cwd().as_path(), ); - let cwd = config_snapshot.cwd().clone(); + let cwd_uri = config_snapshot + .environment_selections() + .first() + .map(|environment| environment.cwd.clone()) + .unwrap_or_else(|| PathUri::from_abs_path(config_snapshot.cwd())); + let cwd_convention = cwd_uri.infer_path_convention().ok_or_else(|| { + internal_error(format!( + "could not infer the path convention for thread cwd `{cwd_uri}`" + )) + })?; + let cwd = ApiPathString::from_path_uri(&cwd_uri, cwd_convention).map_err(|err| { + internal_error(format!( + "could not render thread cwd `{cwd_uri}` using {cwd_convention}: {err}" + )) + })?; let active_permission_profile = thread_response_active_permission_profile(config_snapshot.active_permission_profile); diff --git a/codex-rs/app-server/tests/suite/v2/thread_start.rs b/codex-rs/app-server/tests/suite/v2/thread_start.rs index c0946ba679..75a093850a 100644 --- a/codex-rs/app-server/tests/suite/v2/thread_start.rs +++ b/codex-rs/app-server/tests/suite/v2/thread_start.rs @@ -232,7 +232,7 @@ async fn thread_start_accepts_absolute_runtime_workspace_roots() -> Result<()> { .. } = to_response::(resp)?; - assert_eq!(response_cwd, cwd.abs()); + assert_eq!(response_cwd.as_str(), cwd.to_string_lossy()); assert_eq!(runtime_workspace_roots, vec![extra_root.abs()]); Ok(()) diff --git a/codex-rs/core/tests/remote_env_windows/remote_env_windows_test.rs b/codex-rs/core/tests/remote_env_windows/remote_env_windows_test.rs index 5f73e468ed..a68ee42486 100644 --- a/codex-rs/core/tests/remote_env_windows/remote_env_windows_test.rs +++ b/codex-rs/core/tests/remote_env_windows/remote_env_windows_test.rs @@ -268,8 +268,7 @@ async fn app_server_starts_thread_with_windows_environment_native_cwd() -> Resul let response: ThreadStartResponse = to_response(response)?; assert!(!response.thread.id.is_empty()); let host_cwd = codex_home.path().to_path_buf().abs(); - // TODO(anp): Return the selected environment's native cwd from thread/start. - assert_eq!(response.cwd, host_cwd); + assert_eq!(response.cwd.as_str(), NATIVE_CWD); // TODO(anp): Derive runtime workspace roots from the selected remote environment. assert_eq!(response.runtime_workspace_roots, vec![host_cwd]); // TODO(anp): Discover and report instruction sources from the remote filesystem. diff --git a/codex-rs/exec/Cargo.toml b/codex-rs/exec/Cargo.toml index cdfc569b68..d78d9edf0d 100644 --- a/codex-rs/exec/Cargo.toml +++ b/codex-rs/exec/Cargo.toml @@ -39,6 +39,7 @@ codex-protocol = { workspace = true } codex-utils-absolute-path = { workspace = true } codex-utils-cli = { workspace = true } codex-utils-oss = { workspace = true } +codex-utils-path-uri = { workspace = true } codex-utils-sandbox-summary = { workspace = true } owo-colors = { workspace = true } serde = { workspace = true, features = ["derive"] } diff --git a/codex-rs/exec/src/lib.rs b/codex-rs/exec/src/lib.rs index f8ac851bf9..d01258768c 100644 --- a/codex-rs/exec/src/lib.rs +++ b/codex-rs/exec/src/lib.rs @@ -100,6 +100,8 @@ use codex_utils_absolute_path::canonicalize_existing_preserving_symlinks; use codex_utils_cli::SharedCliOptions; use codex_utils_oss::ensure_oss_provider_ready; use codex_utils_oss::get_default_model_for_oss_provider; +use codex_utils_path_uri::ApiPathString; +use codex_utils_path_uri::PathConvention; use event_processor_with_human_output::EventProcessorWithHumanOutput; pub use event_processor_with_jsonl_output::CodexStatus; pub use event_processor_with_jsonl_output::CollectedThreadEvents; @@ -1154,6 +1156,18 @@ fn session_configured_from_thread_start_response( response: &ThreadStartResponse, config: &Config, ) -> Result { + let cwd_convention = response + .cwd + .infer_absolute_path_convention() + .ok_or_else(|| format!("thread cwd `{}` is not absolute", response.cwd.as_str()))?; + let cwd_uri = response + .cwd + .to_path_uri(cwd_convention) + .map_err(|err| format!("thread cwd is invalid: {err}"))?; + let cwd = ApiPathString::from_path_uri(&cwd_uri, PathConvention::native()) + .map_err(|err| format!("thread cwd cannot be represented on this host: {err}"))?; + let cwd = AbsolutePathBuf::try_from(cwd.into_string()) + .map_err(|err| format!("thread cwd is invalid on this host: {err}"))?; session_configured_from_thread_response( &response.thread.session_id, &response.thread.id, @@ -1168,7 +1182,7 @@ fn session_configured_from_thread_start_response( response.approvals_reviewer.to_core(), config.permissions.effective_permission_profile(), response.active_permission_profile.clone().map(Into::into), - response.cwd.clone(), + cwd, response.reasoning_effort.clone(), ) } diff --git a/codex-rs/exec/src/lib_tests.rs b/codex-rs/exec/src/lib_tests.rs index 6b27d01116..ff541e276f 100644 --- a/codex-rs/exec/src/lib_tests.rs +++ b/codex-rs/exec/src/lib_tests.rs @@ -775,7 +775,8 @@ fn sample_thread_start_response() -> ThreadStartResponse { model: "gpt-5.4".to_string(), model_provider: "openai".to_string(), service_tier: None, - cwd: test_path_buf("/tmp").abs(), + cwd: ApiPathString::from_abs_path(&test_path_buf("/tmp").abs(), PathConvention::native()) + .expect("API cwd"), runtime_workspace_roots: Vec::new(), instruction_sources: Vec::new(), approval_policy: codex_app_server_protocol::AskForApproval::OnRequest, diff --git a/codex-rs/tui/Cargo.toml b/codex-rs/tui/Cargo.toml index b000cba124..6af093286d 100644 --- a/codex-rs/tui/Cargo.toml +++ b/codex-rs/tui/Cargo.toml @@ -62,6 +62,7 @@ codex-utils-fuzzy-match = { workspace = true } codex-utils-home-dir = { workspace = true } codex-utils-oss = { workspace = true } codex-utils-path = { workspace = true } +codex-utils-path-uri = { workspace = true } codex-utils-plugins = { workspace = true } codex-utils-sandbox-summary = { workspace = true } codex-utils-sleep-inhibitor = { workspace = true } diff --git a/codex-rs/tui/src/app_server_session.rs b/codex-rs/tui/src/app_server_session.rs index 5e36fb1c9d..eecdf73cf0 100644 --- a/codex-rs/tui/src/app_server_session.rs +++ b/codex-rs/tui/src/app_server_session.rs @@ -120,6 +120,8 @@ use codex_protocol::openai_models::ModelServiceTier; use codex_protocol::openai_models::ModelUpgrade; use codex_protocol::openai_models::ReasoningEffortPreset; use codex_utils_absolute_path::AbsolutePathBuf; +use codex_utils_path_uri::ApiPathString; +use codex_utils_path_uri::PathConvention; use color_eyre::eyre::ContextCompat; use color_eyre::eyre::Result; use color_eyre::eyre::WrapErr; @@ -1538,9 +1540,21 @@ async fn thread_session_state_from_thread_start_response( config: &Config, thread_params_mode: ThreadParamsMode, ) -> Result { + let cwd_convention = response + .cwd + .infer_absolute_path_convention() + .ok_or_else(|| format!("thread cwd `{}` is not absolute", response.cwd.as_str()))?; + let cwd_uri = response + .cwd + .to_path_uri(cwd_convention) + .map_err(|err| format!("thread cwd is invalid: {err}"))?; + let cwd = ApiPathString::from_path_uri(&cwd_uri, PathConvention::native()) + .map_err(|err| format!("thread cwd cannot be represented on this host: {err}"))?; + let cwd = AbsolutePathBuf::try_from(cwd.into_string()) + .map_err(|err| format!("thread cwd is invalid on this host: {err}"))?; let permission_profile = display_permission_profile_from_thread_response( &response.sandbox, - response.cwd.as_path(), + cwd.as_path(), config, thread_params_mode, ); @@ -1556,7 +1570,7 @@ async fn thread_session_state_from_thread_start_response( response.approvals_reviewer.to_core(), permission_profile, response.active_permission_profile.clone().map(Into::into), - response.cwd.clone(), + cwd, response.runtime_workspace_roots.clone(), response.instruction_sources.clone(), response.reasoning_effort.clone(),