app-server: return selected environment cwd

This commit is contained in:
Adam Perry
2026-06-16 16:42:44 +00:00
parent 54304e5472
commit 42830cf677
15 changed files with 69 additions and 14 deletions
+1
View File
@@ -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",
@@ -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": [],
@@ -16849,7 +16849,7 @@
"description": "Reviewer currently used for approval requests on this thread."
},
"cwd": {
"$ref": "#/definitions/AbsolutePathBuf"
"$ref": "#/definitions/ApiPathString"
},
"instructionSources": {
"default": [],
@@ -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": [],
@@ -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<AbsolutePathBuf>, approvalPolicy: AskForApproval, /**
@@ -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,
@@ -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<String>,
pub cwd: AbsolutePathBuf,
pub cwd: ApiPathString,
/// Thread-scoped runtime workspace roots used to materialize
/// `:workspace_roots`.
#[experimental("thread/start.runtimeWorkspaceRoots")]
@@ -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);
@@ -232,7 +232,7 @@ async fn thread_start_accepts_absolute_runtime_workspace_roots() -> Result<()> {
..
} = to_response::<ThreadStartResponse>(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(())
@@ -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.
+1
View File
@@ -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"] }
+15 -1
View File
@@ -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<SessionConfiguredEvent, String> {
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(),
)
}
+2 -1
View File
@@ -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,
+1
View File
@@ -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 }
+16 -2
View File
@@ -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<ThreadSessionState, String> {
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(),