mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
app-server: preserve target-native environment cwd (#28146)
## Why app-server may run on a different OS from the selected exec-server environment. Parsing that environment’s cwd with the Codex host’s path rules prevents thread startup. ## What Carry environment cwd values as `LegacyAppPathString` at the app-server boundary and `PathUri` internally. Existing tool-call schemas and relative-path behavior stay host-native; remaining local-only consumers convert explicitly and leave follow-up TODOs. The Wine integration test verifies app-server can start a thread and complete an ordinary turn with a Windows environment cwd from Linux. ## Validation - `bazel test //codex-rs/core/tests/remote_env_windows:smoke-test --test_output=errors` - focused app-server environment-selection and protocol schema tests - scoped Clippy for `codex-core` and `codex-app-server-protocol`
This commit is contained in:
committed by
GitHub
Unverified
parent
33d50234a8
commit
f8850cab1d
+4
-1
@@ -1303,6 +1303,9 @@
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"LegacyAppPathString": {
|
||||
"type": "string"
|
||||
},
|
||||
"ListMcpServerStatusParams": {
|
||||
"properties": {
|
||||
"cursor": {
|
||||
@@ -4331,7 +4334,7 @@
|
||||
"TurnEnvironmentParams": {
|
||||
"properties": {
|
||||
"cwd": {
|
||||
"$ref": "#/definitions/AbsolutePathBuf"
|
||||
"$ref": "#/definitions/LegacyAppPathString"
|
||||
},
|
||||
"environmentId": {
|
||||
"type": "string"
|
||||
|
||||
+1
-1
@@ -19517,7 +19517,7 @@
|
||||
"TurnEnvironmentParams": {
|
||||
"properties": {
|
||||
"cwd": {
|
||||
"$ref": "#/definitions/v2/AbsolutePathBuf"
|
||||
"$ref": "#/definitions/v2/LegacyAppPathString"
|
||||
},
|
||||
"environmentId": {
|
||||
"type": "string"
|
||||
|
||||
+1
-1
@@ -17325,7 +17325,7 @@
|
||||
"TurnEnvironmentParams": {
|
||||
"properties": {
|
||||
"cwd": {
|
||||
"$ref": "#/definitions/AbsolutePathBuf"
|
||||
"$ref": "#/definitions/LegacyAppPathString"
|
||||
},
|
||||
"environmentId": {
|
||||
"type": "string"
|
||||
|
||||
@@ -191,6 +191,9 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"LegacyAppPathString": {
|
||||
"type": "string"
|
||||
},
|
||||
"Personality": {
|
||||
"enum": [
|
||||
"none",
|
||||
@@ -242,7 +245,7 @@
|
||||
"TurnEnvironmentParams": {
|
||||
"properties": {
|
||||
"cwd": {
|
||||
"$ref": "#/definitions/AbsolutePathBuf"
|
||||
"$ref": "#/definitions/LegacyAppPathString"
|
||||
},
|
||||
"environmentId": {
|
||||
"type": "string"
|
||||
|
||||
@@ -130,6 +130,9 @@
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"LegacyAppPathString": {
|
||||
"type": "string"
|
||||
},
|
||||
"ModeKind": {
|
||||
"description": "Initial collaboration mode to use when the TUI starts.",
|
||||
"enum": [
|
||||
@@ -331,7 +334,7 @@
|
||||
"TurnEnvironmentParams": {
|
||||
"properties": {
|
||||
"cwd": {
|
||||
"$ref": "#/definitions/AbsolutePathBuf"
|
||||
"$ref": "#/definitions/LegacyAppPathString"
|
||||
},
|
||||
"environmentId": {
|
||||
"type": "string"
|
||||
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
// GENERATED CODE! DO NOT MODIFY BY HAND!
|
||||
|
||||
// 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 { LegacyAppPathString } from "../LegacyAppPathString";
|
||||
|
||||
export type TurnEnvironmentParams = { environmentId: string, cwd: AbsolutePathBuf, };
|
||||
export type TurnEnvironmentParams = { environmentId: string, cwd: LegacyAppPathString, };
|
||||
|
||||
@@ -3723,7 +3723,14 @@ fn thread_settings_update_params_preserve_field_level_experimental_gates() {
|
||||
|
||||
#[test]
|
||||
fn turn_start_params_round_trip_environments() {
|
||||
let cwd = test_absolute_path();
|
||||
// Use a path foreign to the test host so this exercises syntax preservation instead of the
|
||||
// host-native conversion performed by test_absolute_path().
|
||||
#[cfg(windows)]
|
||||
let raw_cwd = "/workspace";
|
||||
#[cfg(not(windows))]
|
||||
let raw_cwd = r"C:\workspace";
|
||||
let cwd: LegacyAppPathString =
|
||||
serde_json::from_value(json!(raw_cwd)).expect("API path should deserialize");
|
||||
let params: TurnStartParams = serde_json::from_value(json!({
|
||||
"threadId": "thread_123",
|
||||
"input": [],
|
||||
@@ -3805,27 +3812,6 @@ fn turn_start_params_treat_null_or_omitted_environments_as_default() {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn turn_start_params_reject_relative_environment_cwd() {
|
||||
let err = serde_json::from_value::<TurnStartParams>(json!({
|
||||
"threadId": "thread_123",
|
||||
"input": [],
|
||||
"environments": [
|
||||
{
|
||||
"environmentId": "local",
|
||||
"cwd": "relative"
|
||||
}
|
||||
],
|
||||
}))
|
||||
.expect_err("relative environment cwd should fail");
|
||||
|
||||
assert!(
|
||||
err.to_string()
|
||||
.contains("AbsolutePathBuf deserialized without a base path"),
|
||||
"unexpected error: {err}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn realtime_append_text_defaults_role_to_user() {
|
||||
let params = serde_json::from_value::<ThreadRealtimeAppendTextParams>(json!({
|
||||
|
||||
@@ -14,6 +14,7 @@ use codex_protocol::user_input::ByteRange as CoreByteRange;
|
||||
use codex_protocol::user_input::TextElement as CoreTextElement;
|
||||
use codex_protocol::user_input::UserInput as CoreUserInput;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use codex_utils_path_uri::LegacyAppPathString;
|
||||
use schemars::JsonSchema;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
@@ -38,7 +39,7 @@ pub enum TurnStatus {
|
||||
#[ts(export_to = "v2/")]
|
||||
pub struct TurnEnvironmentParams {
|
||||
pub environment_id: String,
|
||||
pub cwd: AbsolutePathBuf,
|
||||
pub cwd: LegacyAppPathString,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS)]
|
||||
|
||||
@@ -541,6 +541,37 @@ fn resolve_request_cwd(cwd: Option<PathBuf>) -> Result<Option<AbsolutePathBuf>,
|
||||
.transpose()
|
||||
}
|
||||
|
||||
fn resolve_turn_environment_selections(
|
||||
thread_manager: &ThreadManager,
|
||||
environments: Option<Vec<TurnEnvironmentParams>>,
|
||||
) -> Result<Option<Vec<TurnEnvironmentSelection>>, JSONRPCErrorError> {
|
||||
let Some(environments) = environments else {
|
||||
return Ok(None);
|
||||
};
|
||||
let mut selections = Vec::with_capacity(environments.len());
|
||||
for environment in environments {
|
||||
let environment_id = environment.environment_id;
|
||||
let cwd = environment
|
||||
.cwd
|
||||
.infer_absolute_path_convention()
|
||||
.and_then(|convention| environment.cwd.to_path_uri(convention).ok())
|
||||
.ok_or_else(|| {
|
||||
invalid_request(format!(
|
||||
"invalid cwd for environment `{environment_id}`: path `{}` does not use absolute POSIX or Windows path syntax",
|
||||
environment.cwd
|
||||
))
|
||||
})?;
|
||||
selections.push(TurnEnvironmentSelection {
|
||||
environment_id,
|
||||
cwd,
|
||||
});
|
||||
}
|
||||
thread_manager
|
||||
.validate_environment_selections(&selections)
|
||||
.map_err(environment_selection_error)?;
|
||||
Ok(Some(selections))
|
||||
}
|
||||
|
||||
fn resolve_runtime_workspace_roots(workspace_roots: Vec<AbsolutePathBuf>) -> Vec<AbsolutePathBuf> {
|
||||
let mut resolved_roots = Vec::new();
|
||||
for root in workspace_roots {
|
||||
|
||||
@@ -4,7 +4,6 @@ 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::PathUri;
|
||||
|
||||
const THREAD_LIST_DEFAULT_LIMIT: usize = 25;
|
||||
const THREAD_LIST_MAX_LIMIT: usize = 100;
|
||||
@@ -906,7 +905,8 @@ impl ThreadRequestProcessor {
|
||||
"`permissions` cannot be combined with `sandbox`",
|
||||
));
|
||||
}
|
||||
let environment_selections = self.parse_environment_selections(environments)?;
|
||||
let environment_selections =
|
||||
resolve_turn_environment_selections(self.thread_manager.as_ref(), environments)?;
|
||||
let runtime_workspace_roots = runtime_workspace_roots.map(resolve_runtime_workspace_roots);
|
||||
let mut typesafe_overrides = self.build_thread_config_overrides(
|
||||
model,
|
||||
@@ -1311,27 +1311,6 @@ impl ThreadRequestProcessor {
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_environment_selections(
|
||||
&self,
|
||||
environments: Option<Vec<TurnEnvironmentParams>>,
|
||||
) -> Result<Option<Vec<TurnEnvironmentSelection>>, JSONRPCErrorError> {
|
||||
let environment_selections = environments.map(|environments| {
|
||||
environments
|
||||
.into_iter()
|
||||
.map(|environment| TurnEnvironmentSelection {
|
||||
environment_id: environment.environment_id,
|
||||
cwd: PathUri::from_abs_path(&environment.cwd),
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
});
|
||||
if let Some(environment_selections) = environment_selections.as_ref() {
|
||||
self.thread_manager
|
||||
.validate_environment_selections(environment_selections)
|
||||
.map_err(environment_selection_error)?;
|
||||
}
|
||||
Ok(environment_selections)
|
||||
}
|
||||
|
||||
async fn thread_archive_inner(
|
||||
&self,
|
||||
params: ThreadArchiveParams,
|
||||
|
||||
@@ -4,7 +4,6 @@ use codex_protocol::protocol::AdditionalContextKind as CoreAdditionalContextKind
|
||||
use codex_protocol::protocol::MultiAgentVersion;
|
||||
use codex_protocol::protocol::SessionSource;
|
||||
use codex_protocol::protocol::SubAgentSource;
|
||||
use codex_utils_path_uri::PathUri;
|
||||
|
||||
const DIRECT_INPUT_TO_MULTI_AGENT_V2_SUBAGENT_ERROR: &str =
|
||||
"direct app-server input is not allowed for multi-agent v2 sub-agents";
|
||||
@@ -341,27 +340,6 @@ impl TurnRequestProcessor {
|
||||
Ok((review_request, hint))
|
||||
}
|
||||
|
||||
fn parse_environment_selections(
|
||||
&self,
|
||||
environments: Option<Vec<TurnEnvironmentParams>>,
|
||||
) -> Result<Option<Vec<TurnEnvironmentSelection>>, JSONRPCErrorError> {
|
||||
let environment_selections = environments.map(|environments| {
|
||||
environments
|
||||
.into_iter()
|
||||
.map(|environment| TurnEnvironmentSelection {
|
||||
environment_id: environment.environment_id,
|
||||
cwd: PathUri::from_abs_path(&environment.cwd),
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
});
|
||||
if let Some(environment_selections) = environment_selections.as_ref() {
|
||||
self.thread_manager
|
||||
.validate_environment_selections(environment_selections)
|
||||
.map_err(environment_selection_error)?;
|
||||
}
|
||||
Ok(environment_selections)
|
||||
}
|
||||
|
||||
async fn request_trace_context(
|
||||
&self,
|
||||
request_id: &ConnectionRequestId,
|
||||
@@ -433,7 +411,8 @@ impl TurnRequestProcessor {
|
||||
self.track_error_response(&request_id, error, /*error_type*/ None);
|
||||
})?;
|
||||
|
||||
let environment_selections = self.parse_environment_selections(params.environments)?;
|
||||
let environment_selections =
|
||||
resolve_turn_environment_selections(self.thread_manager.as_ref(), params.environments)?;
|
||||
|
||||
// Map v2 input items to core input items.
|
||||
let mapped_items: Vec<CoreInputItem> = params
|
||||
|
||||
@@ -293,7 +293,10 @@ async fn thread_start_rejects_unknown_environment_as_invalid_request() -> Result
|
||||
.send_thread_start_request(ThreadStartParams {
|
||||
environments: Some(vec![TurnEnvironmentParams {
|
||||
environment_id: "missing".to_string(),
|
||||
cwd: codex_home.path().to_path_buf().try_into()?,
|
||||
cwd: codex_utils_absolute_path::AbsolutePathBuf::try_from(
|
||||
codex_home.path().to_path_buf(),
|
||||
)?
|
||||
.into(),
|
||||
}]),
|
||||
..Default::default()
|
||||
})
|
||||
@@ -312,6 +315,40 @@ async fn thread_start_rejects_unknown_environment_as_invalid_request() -> Result
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn thread_start_rejects_relative_environment_cwd_as_invalid_request() -> Result<()> {
|
||||
let server = create_mock_responses_server_repeating_assistant("Done").await;
|
||||
let codex_home = TempDir::new()?;
|
||||
create_config_toml_without_approval_policy(codex_home.path(), &server.uri())?;
|
||||
|
||||
let mut mcp = TestAppServer::new(codex_home.path()).await?;
|
||||
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let request_id = mcp
|
||||
.send_thread_start_request(ThreadStartParams {
|
||||
environments: Some(vec![TurnEnvironmentParams {
|
||||
environment_id: "local".to_string(),
|
||||
cwd: serde_json::from_value(json!("relative"))?,
|
||||
}]),
|
||||
..Default::default()
|
||||
})
|
||||
.await?;
|
||||
let error: JSONRPCError = timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
mcp.read_stream_until_error_message(RequestId::Integer(request_id)),
|
||||
)
|
||||
.await??;
|
||||
|
||||
assert_eq!(error.id, RequestId::Integer(request_id));
|
||||
assert_eq!(error.error.code, INVALID_REQUEST_ERROR_CODE);
|
||||
assert_eq!(
|
||||
error.error.message,
|
||||
"invalid cwd for environment `local`: path `relative` does not use absolute POSIX or Windows path syntax"
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn thread_start_response_includes_loaded_instruction_sources() -> Result<()> {
|
||||
let server = create_mock_responses_server_repeating_assistant("Done").await;
|
||||
|
||||
@@ -76,6 +76,7 @@ use codex_protocol::models::BUILT_IN_PERMISSION_PROFILE_DANGER_FULL_ACCESS;
|
||||
use codex_protocol::models::ImageDetail;
|
||||
use codex_protocol::openai_models::ReasoningEffort;
|
||||
use codex_protocol::user_input::MAX_USER_INPUT_TEXT_CHARS;
|
||||
use codex_utils_absolute_path::test_support::PathExt;
|
||||
use core_test_support::responses;
|
||||
use core_test_support::skip_if_no_network;
|
||||
use pretty_assertions::assert_eq;
|
||||
@@ -1326,7 +1327,10 @@ async fn turn_start_rejects_unknown_environment_before_starting_turn() -> Result
|
||||
}],
|
||||
environments: Some(vec![TurnEnvironmentParams {
|
||||
environment_id: "missing".to_string(),
|
||||
cwd: codex_home.path().to_path_buf().try_into()?,
|
||||
cwd: codex_utils_absolute_path::AbsolutePathBuf::try_from(
|
||||
codex_home.path().to_path_buf(),
|
||||
)?
|
||||
.into(),
|
||||
}]),
|
||||
..Default::default()
|
||||
})
|
||||
@@ -2669,7 +2673,7 @@ async fn run_environment_selection_case(
|
||||
.send_thread_start_request(ThreadStartParams {
|
||||
model: Some("mock-model".to_string()),
|
||||
cwd: Some(workspace.to_string_lossy().into_owned()),
|
||||
environments: environment_params(case.sticky, workspace)?,
|
||||
environments: environment_params(case.sticky, workspace),
|
||||
..Default::default()
|
||||
})
|
||||
.await?;
|
||||
@@ -2688,7 +2692,7 @@ async fn run_environment_selection_case(
|
||||
text: format!("run {}", case.name),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
environments: environment_params(case.turn, workspace)?,
|
||||
environments: environment_params(case.turn, workspace),
|
||||
cwd: Some(workspace.to_path_buf()),
|
||||
model: Some("mock-model".to_string()),
|
||||
..Default::default()
|
||||
@@ -2735,21 +2739,15 @@ async fn run_environment_selection_case(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn environment_params(
|
||||
ids: Option<&[&str]>,
|
||||
cwd: &Path,
|
||||
) -> Result<Option<Vec<TurnEnvironmentParams>>> {
|
||||
fn environment_params(ids: Option<&[&str]>, cwd: &Path) -> Option<Vec<TurnEnvironmentParams>> {
|
||||
ids.map(|ids| {
|
||||
ids.iter()
|
||||
.map(|id| {
|
||||
Ok(TurnEnvironmentParams {
|
||||
environment_id: (*id).to_string(),
|
||||
cwd: cwd.to_path_buf().try_into()?,
|
||||
})
|
||||
.map(|id| TurnEnvironmentParams {
|
||||
environment_id: (*id).to_string(),
|
||||
cwd: cwd.abs().into(),
|
||||
})
|
||||
.collect()
|
||||
})
|
||||
.transpose()
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
|
||||
@@ -53,11 +53,16 @@ pub(crate) async fn load_project_instructions(
|
||||
let mut loaded = LoadedAgentsMd::from_user_instructions(user_instructions);
|
||||
for turn_environment in &environments.turn_environments {
|
||||
let filesystem = turn_environment.environment.get_filesystem();
|
||||
// TODO(anp): Migrate AGENTS.md discovery to PathUri so instructions can be loaded from
|
||||
// environment-native foreign working directories.
|
||||
let Ok(cwd) = turn_environment.cwd().to_abs_path() else {
|
||||
continue;
|
||||
};
|
||||
match read_agents_md(
|
||||
config,
|
||||
filesystem.as_ref(),
|
||||
&turn_environment.environment_id,
|
||||
turn_environment.cwd(),
|
||||
&cwd,
|
||||
)
|
||||
.await
|
||||
{
|
||||
|
||||
@@ -267,7 +267,7 @@ fn resolved_local_environments<const N: usize>(
|
||||
Environment::create_for_tests(/*exec_server_url*/ None)
|
||||
.expect("local environment"),
|
||||
),
|
||||
cwd,
|
||||
PathUri::from_abs_path(&cwd),
|
||||
/*shell*/ None,
|
||||
)
|
||||
})
|
||||
|
||||
@@ -44,14 +44,18 @@ impl EnvironmentContextEnvironment {
|
||||
fn from_turn_environments(environments: &[TurnEnvironment], shell: &Shell) -> Vec<Self> {
|
||||
environments
|
||||
.iter()
|
||||
.map(|environment| Self {
|
||||
id: environment.environment_id.clone(),
|
||||
cwd: environment.cwd().clone(),
|
||||
shell: environment
|
||||
.shell
|
||||
.as_ref()
|
||||
.map(|shell| shell.name().to_string())
|
||||
.unwrap_or_else(|| shell.name().to_string()),
|
||||
.filter_map(|environment| {
|
||||
// TODO(anp): Migrate EnvironmentContextEnvironment to PathUri so foreign
|
||||
// environments remain visible in model context.
|
||||
Some(Self {
|
||||
id: environment.environment_id.clone(),
|
||||
cwd: environment.cwd().to_abs_path().ok()?,
|
||||
shell: environment
|
||||
.shell
|
||||
.as_ref()
|
||||
.map(|shell| shell.name().to_string())
|
||||
.unwrap_or_else(|| shell.name().to_string()),
|
||||
})
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
@@ -97,7 +97,7 @@ impl ThreadEnvironments {
|
||||
}
|
||||
let turn_environment = match current.turn_environments.iter().find(|environment| {
|
||||
environment.environment_id == selected_environment.environment_id
|
||||
&& environment.cwd_uri() == &selected_environment.cwd
|
||||
&& environment.cwd() == &selected_environment.cwd
|
||||
}) {
|
||||
Some(environment) => environment.clone(),
|
||||
None => match Self::resolve_selection(
|
||||
@@ -157,12 +157,7 @@ impl ThreadEnvironments {
|
||||
let mut turn_environment = TurnEnvironment::new(
|
||||
environment_id,
|
||||
environment,
|
||||
selected_environment.cwd.to_abs_path().map_err(|err| {
|
||||
CodexErr::InvalidRequest(format!(
|
||||
"turn environment cwd `{}` is not valid on this host: {err}",
|
||||
selected_environment.cwd
|
||||
))
|
||||
})?,
|
||||
selected_environment.cwd.clone(),
|
||||
shell,
|
||||
);
|
||||
let task = shell_snapshot
|
||||
@@ -226,8 +221,10 @@ impl TurnEnvironmentSnapshot {
|
||||
(!environment.environment.is_remote()).then_some(environment)
|
||||
}
|
||||
|
||||
pub(crate) fn single_local_environment_cwd(&self) -> Option<&AbsolutePathBuf> {
|
||||
self.single_local_environment().map(TurnEnvironment::cwd)
|
||||
pub(crate) fn single_local_environment_cwd(&self) -> Option<AbsolutePathBuf> {
|
||||
// TODO(anp): Migrate local-environment consumers to PathUri so this compatibility
|
||||
// conversion can be removed.
|
||||
self.single_local_environment()?.cwd().to_abs_path().ok()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -560,7 +557,7 @@ url = "ws://127.0.0.1:8765"
|
||||
Arc::clone(&local_manager),
|
||||
&[TurnEnvironmentSelection {
|
||||
environment_id: LOCAL_ENVIRONMENT_ID.to_string(),
|
||||
cwd: cwd_uri,
|
||||
cwd: cwd_uri.clone(),
|
||||
}],
|
||||
)
|
||||
.await;
|
||||
@@ -573,7 +570,7 @@ url = "ws://127.0.0.1:8765"
|
||||
turn_environments: vec![TurnEnvironment::new(
|
||||
REMOTE_ENVIRONMENT_ID.to_string(),
|
||||
remote_environment.clone(),
|
||||
cwd.clone(),
|
||||
cwd_uri.clone(),
|
||||
/*shell*/ None,
|
||||
)],
|
||||
};
|
||||
@@ -583,13 +580,13 @@ url = "ws://127.0.0.1:8765"
|
||||
TurnEnvironment::new(
|
||||
REMOTE_ENVIRONMENT_ID.to_string(),
|
||||
remote_environment,
|
||||
cwd.clone(),
|
||||
cwd_uri,
|
||||
/*shell*/ None,
|
||||
),
|
||||
],
|
||||
};
|
||||
|
||||
assert_eq!(local.single_local_environment_cwd(), Some(&cwd));
|
||||
assert_eq!(local.single_local_environment_cwd(), Some(cwd));
|
||||
assert_eq!(remote.single_local_environment_cwd(), None);
|
||||
assert_eq!(multiple.single_local_environment_cwd(), None);
|
||||
}
|
||||
|
||||
@@ -750,11 +750,13 @@ async fn run_review_on_session(
|
||||
.unwrap_or_default();
|
||||
let guardian_permission_profile = PermissionProfile::read_only();
|
||||
let parent_turn_environments = params.parent_turn.environments.to_selections();
|
||||
// TODO(anp): Migrate guardian review thread settings to a PathUri fallback cwd so foreign
|
||||
// parent environments do not fall back to the host-native config cwd.
|
||||
let parent_turn_legacy_fallback_cwd = params
|
||||
.parent_turn
|
||||
.environments
|
||||
.primary()
|
||||
.map(|environment| environment.cwd().clone())
|
||||
.and_then(|environment| environment.cwd().to_abs_path().ok())
|
||||
.unwrap_or_else(|| params.parent_turn.config.cwd.clone());
|
||||
|
||||
let submit_result = run_before_review_deadline(
|
||||
|
||||
@@ -122,7 +122,13 @@ async fn build_uploaded_argument_value(
|
||||
"no primary turn environment is available".to_string(),
|
||||
));
|
||||
};
|
||||
let resolved_path = turn_environment.cwd().join(file_path);
|
||||
// TODO(anp): Resolve app tool file arguments using the selected environment's native path
|
||||
// convention so uploads can read relative paths from foreign environments.
|
||||
let native_environment_cwd = turn_environment
|
||||
.cwd()
|
||||
.to_abs_path()
|
||||
.map_err(|error| contextualize_error(error.to_string()))?;
|
||||
let resolved_path = native_environment_cwd.join(file_path);
|
||||
let path_uri = PathUri::from_abs_path(&resolved_path);
|
||||
let fs = turn_environment.environment.get_filesystem();
|
||||
let metadata = fs
|
||||
@@ -178,6 +184,7 @@ mod tests {
|
||||
use crate::session::tests::make_session_and_context;
|
||||
use crate::session::turn_context::TurnEnvironment;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use codex_utils_path_uri::PathUri;
|
||||
use pretty_assertions::assert_eq;
|
||||
use std::path::Path;
|
||||
use std::sync::Arc;
|
||||
@@ -194,7 +201,7 @@ mod tests {
|
||||
*primary = TurnEnvironment::new(
|
||||
primary.environment_id.clone(),
|
||||
Arc::clone(&primary.environment),
|
||||
cwd,
|
||||
PathUri::from_abs_path(&cwd),
|
||||
primary.shell.clone(),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -318,17 +318,18 @@ impl Session {
|
||||
)
|
||||
.await;
|
||||
let environment_manager = self.services.turn_environments.environment_manager();
|
||||
let mcp_runtime_context = match turn_context.environments.primary() {
|
||||
Some(turn_environment) => McpRuntimeContext::new(
|
||||
Arc::clone(&environment_manager),
|
||||
turn_environment.cwd().to_path_buf(),
|
||||
),
|
||||
None => McpRuntimeContext::new(
|
||||
environment_manager,
|
||||
// TODO(anp): Migrate MCP runtime cwd plumbing to PathUri so foreign environment cwd
|
||||
// values can be used without falling back to the legacy host cwd.
|
||||
let cwd = turn_context
|
||||
.environments
|
||||
.primary()
|
||||
.and_then(|turn_environment| turn_environment.cwd().to_abs_path().ok())
|
||||
.map(|cwd| cwd.to_path_buf())
|
||||
.unwrap_or_else(|| {
|
||||
#[allow(deprecated)]
|
||||
turn_context.cwd.to_path_buf(),
|
||||
),
|
||||
};
|
||||
turn_context.cwd.to_path_buf()
|
||||
});
|
||||
let mcp_runtime_context = McpRuntimeContext::new(environment_manager, cwd);
|
||||
let mcp_startup_cancellation_token = {
|
||||
let mut guard = self.services.mcp_startup_cancellation_token.lock().await;
|
||||
guard.cancel();
|
||||
|
||||
@@ -1119,9 +1119,12 @@ impl Session {
|
||||
};
|
||||
let mcp_runtime_context = {
|
||||
let turn_environments = sess.services.turn_environments.snapshot().await;
|
||||
// TODO(anp): Migrate MCP runtime cwd plumbing to PathUri so foreign environment
|
||||
// cwd values can be used without falling back to the session host cwd.
|
||||
let cwd = turn_environments
|
||||
.primary()
|
||||
.map(|turn_environment| turn_environment.cwd().to_path_buf())
|
||||
.and_then(|turn_environment| turn_environment.cwd().to_abs_path().ok())
|
||||
.map(|cwd| cwd.to_path_buf())
|
||||
.unwrap_or_else(|| session_configuration.cwd().to_path_buf());
|
||||
McpRuntimeContext::new(
|
||||
sess.services.turn_environments.environment_manager(),
|
||||
|
||||
@@ -5697,7 +5697,7 @@ async fn request_permissions_tool_resolves_relative_paths_against_selected_envir
|
||||
turn_context_mut.environments.turn_environments[0] = TurnEnvironment::new(
|
||||
"remote".to_string(),
|
||||
current_environment.environment,
|
||||
environment_cwd.clone(),
|
||||
PathUri::from_abs_path(&environment_cwd),
|
||||
current_environment.shell,
|
||||
);
|
||||
|
||||
@@ -6316,13 +6316,14 @@ async fn primary_environment_uses_first_turn_environment() {
|
||||
let first_environment = turn_context.environments.turn_environments[0].clone();
|
||||
#[allow(deprecated)]
|
||||
let second_cwd = turn_context.cwd.join("second");
|
||||
let second_cwd_uri = codex_utils_path_uri::PathUri::from_abs_path(&second_cwd);
|
||||
turn_context
|
||||
.environments
|
||||
.turn_environments
|
||||
.push(TurnEnvironment::new(
|
||||
"second".to_string(),
|
||||
Arc::clone(&first_environment.environment),
|
||||
second_cwd.clone(),
|
||||
second_cwd_uri.clone(),
|
||||
/*shell*/ None,
|
||||
));
|
||||
|
||||
@@ -6342,12 +6343,12 @@ async fn primary_environment_uses_first_turn_environment() {
|
||||
.find(|environment| environment.environment_id == "second")
|
||||
.expect("second environment")
|
||||
.cwd(),
|
||||
&second_cwd
|
||||
&second_cwd_uri
|
||||
);
|
||||
assert_eq!(turn_context.environments.turn_environments.len(), 2);
|
||||
assert_eq!(
|
||||
turn_context.environments.turn_environments[1].cwd(),
|
||||
&second_cwd
|
||||
&second_cwd_uri
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -414,13 +414,16 @@ pub(crate) async fn run_turn(
|
||||
async fn turn_diff_display_roots(turn_context: &TurnContext) -> Vec<(String, PathBuf)> {
|
||||
let mut display_roots = Vec::new();
|
||||
for turn_environment in &turn_context.environments.turn_environments {
|
||||
let root = get_git_repo_root_with_fs(
|
||||
turn_environment.environment.get_filesystem().as_ref(),
|
||||
turn_environment.cwd(),
|
||||
)
|
||||
.await
|
||||
.unwrap_or_else(|| turn_environment.cwd().clone())
|
||||
.into_path_buf();
|
||||
// TODO(anp): Migrate git-root discovery and diff display roots to PathUri so foreign
|
||||
// environment roots can participate without host-native conversion.
|
||||
let Ok(cwd) = turn_environment.cwd().to_abs_path() else {
|
||||
continue;
|
||||
};
|
||||
let root =
|
||||
get_git_repo_root_with_fs(turn_environment.environment.get_filesystem().as_ref(), &cwd)
|
||||
.await
|
||||
.unwrap_or(cwd)
|
||||
.into_path_buf();
|
||||
display_roots.push((turn_environment.environment_id.clone(), root));
|
||||
}
|
||||
display_roots
|
||||
@@ -634,10 +637,14 @@ async fn build_extension_turn_input_items(
|
||||
.turn_environments
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(index, environment)| TurnInputEnvironment {
|
||||
environment_id: environment.environment_id.clone(),
|
||||
cwd: environment.cwd().as_path().to_path_buf(),
|
||||
is_primary: index == 0,
|
||||
.filter_map(|(index, environment)| {
|
||||
// TODO(anp): Migrate extension turn-input environments to PathUri so foreign cwd
|
||||
// values are not omitted from extension context.
|
||||
Some(TurnInputEnvironment {
|
||||
environment_id: environment.environment_id.clone(),
|
||||
cwd: environment.cwd().to_abs_path().ok()?.into_path_buf(),
|
||||
is_primary: index == 0,
|
||||
})
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
|
||||
@@ -3,7 +3,6 @@ use crate::SkillLoadOutcome;
|
||||
use crate::agents_md::LoadedAgentsMd;
|
||||
use crate::config::GhostSnapshotConfig;
|
||||
use crate::environment_selection::TurnEnvironmentSnapshot;
|
||||
use crate::path_utils;
|
||||
use crate::shell_snapshot::ShellSnapshotFile;
|
||||
use codex_core_skills::HostLoadedSkills;
|
||||
use codex_file_system::FileSystemSandboxContext;
|
||||
@@ -49,13 +48,7 @@ pub(crate) type ShellSnapshotTask = Shared<BoxFuture<'static, Option<Arc<ShellSn
|
||||
pub(crate) struct TurnEnvironment {
|
||||
pub(crate) environment_id: String,
|
||||
pub(crate) environment: Arc<Environment>,
|
||||
// Keep both representations together while cwd consumers migrate to URI semantics. Keeping
|
||||
// them synchronized means neither representation can be exposed through a mutable reference;
|
||||
// updates must rebuild the validated pair through `TurnEnvironment::new`. Once
|
||||
// `TurnEnvironment::cwd` itself becomes a `PathUri`, convert only at native filesystem and
|
||||
// process-launch boundaries and remove this paired migration state.
|
||||
cwd: AbsolutePathBuf,
|
||||
cwd_uri: PathUri,
|
||||
cwd: PathUri,
|
||||
pub(crate) shell: Option<shell::Shell>,
|
||||
pub(crate) shell_snapshot: ShellSnapshotTask,
|
||||
}
|
||||
@@ -64,22 +57,20 @@ impl TurnEnvironment {
|
||||
pub(crate) fn new(
|
||||
environment_id: String,
|
||||
environment: Arc<Environment>,
|
||||
cwd: AbsolutePathBuf,
|
||||
cwd: PathUri,
|
||||
shell: Option<shell::Shell>,
|
||||
) -> Self {
|
||||
let cwd_uri = PathUri::from_abs_path(&cwd);
|
||||
Self {
|
||||
environment_id,
|
||||
environment,
|
||||
cwd,
|
||||
cwd_uri,
|
||||
shell,
|
||||
shell_snapshot: futures::future::ready(None).boxed().shared(),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn shell_snapshot(&self, cwd: &AbsolutePathBuf) -> Option<AbsolutePathBuf> {
|
||||
if !path_utils::paths_match_after_normalization(self.cwd.as_path(), cwd.as_path()) {
|
||||
if self.cwd != PathUri::from_abs_path(cwd) {
|
||||
return None;
|
||||
}
|
||||
self.shell_snapshot
|
||||
@@ -88,18 +79,14 @@ impl TurnEnvironment {
|
||||
.map(ShellSnapshotFile::path)
|
||||
}
|
||||
|
||||
pub(crate) fn cwd(&self) -> &AbsolutePathBuf {
|
||||
pub(crate) fn cwd(&self) -> &PathUri {
|
||||
&self.cwd
|
||||
}
|
||||
|
||||
pub(crate) fn cwd_uri(&self) -> &PathUri {
|
||||
&self.cwd_uri
|
||||
}
|
||||
|
||||
pub(crate) fn selection(&self) -> TurnEnvironmentSelection {
|
||||
TurnEnvironmentSelection {
|
||||
environment_id: self.environment_id.clone(),
|
||||
cwd: self.cwd_uri.clone(),
|
||||
cwd: self.cwd.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -110,7 +97,6 @@ impl std::fmt::Debug for TurnEnvironment {
|
||||
.field("environment_id", &self.environment_id)
|
||||
.field("environment", &self.environment)
|
||||
.field("cwd", &self.cwd)
|
||||
.field("cwd_uri", &self.cwd_uri)
|
||||
.field("shell", &self.shell)
|
||||
.finish_non_exhaustive()
|
||||
}
|
||||
@@ -755,9 +741,11 @@ impl Session {
|
||||
) -> Arc<TurnContext> {
|
||||
let turn_environments = self.services.turn_environments.snapshot().await;
|
||||
let primary_turn_environment = turn_environments.primary().cloned();
|
||||
// TODO(anp): Migrate per-turn config and legacy TurnContext cwd consumers to PathUri so
|
||||
// a foreign primary environment does not fall back to the session's host cwd.
|
||||
let cwd = primary_turn_environment
|
||||
.as_ref()
|
||||
.map(|turn_environment| turn_environment.cwd().clone())
|
||||
.and_then(|turn_environment| turn_environment.cwd().to_abs_path().ok())
|
||||
.unwrap_or_else(|| session_configuration.cwd().clone());
|
||||
let per_turn_config = Self::build_per_turn_config(&session_configuration, cwd.clone());
|
||||
{
|
||||
|
||||
@@ -76,7 +76,9 @@ impl ShellSnapshot {
|
||||
}
|
||||
|
||||
let shell = environment.shell.clone()?;
|
||||
let cwd = environment.cwd().clone();
|
||||
// TODO(anp): Migrate shell snapshot creation to accept PathUri and defer native
|
||||
// conversion to the spawned shell process.
|
||||
let cwd = environment.cwd().to_abs_path().ok()?;
|
||||
Self::build_for_cwd(Arc::clone(config), cwd, shell).await
|
||||
}
|
||||
|
||||
|
||||
@@ -144,7 +144,18 @@ pub(crate) async fn execute_user_shell_command(
|
||||
// We do not source rc files or otherwise reformat the script.
|
||||
let use_login_shell = true;
|
||||
let display_command = environment_shell.derive_exec_args(&command, use_login_shell);
|
||||
let shell_snapshot_location = turn_environment.shell_snapshot(turn_environment.cwd());
|
||||
// TODO(anp): Migrate user-shell events and execution plumbing to PathUri so this local-only
|
||||
// feature does not need to project the selected environment cwd onto the Codex host.
|
||||
let Ok(cwd) = turn_environment.cwd().to_abs_path() else {
|
||||
send_user_shell_error(
|
||||
&session,
|
||||
turn_context.as_ref(),
|
||||
"shell working directory is not native to the Codex host",
|
||||
)
|
||||
.await;
|
||||
return;
|
||||
};
|
||||
let shell_snapshot_location = turn_environment.shell_snapshot(&cwd);
|
||||
let mut exec_env_map = create_env(
|
||||
&turn_context.shell_environment_policy,
|
||||
Some(session.thread_id),
|
||||
@@ -162,7 +173,6 @@ pub(crate) async fn execute_user_shell_command(
|
||||
|
||||
let call_id = Uuid::new_v4().to_string();
|
||||
let raw_command = command;
|
||||
let cwd = turn_environment.cwd().clone();
|
||||
|
||||
let parsed_cmd = parse_command(&display_command);
|
||||
session
|
||||
|
||||
@@ -606,11 +606,11 @@ async fn resume_and_fork_do_not_restore_thread_environments_from_rollout() {
|
||||
assert_eq!(resumed_turn.environments.turn_environments.len(), 1);
|
||||
assert_eq!(
|
||||
resumed_turn.environments.turn_environments[0].cwd(),
|
||||
&default_cwd
|
||||
&PathUri::from_abs_path(&default_cwd)
|
||||
);
|
||||
assert_ne!(
|
||||
resumed_turn.environments.turn_environments[0].cwd(),
|
||||
&selected_cwd
|
||||
&PathUri::from_abs_path(&selected_cwd)
|
||||
);
|
||||
|
||||
let forked = manager
|
||||
@@ -633,11 +633,11 @@ async fn resume_and_fork_do_not_restore_thread_environments_from_rollout() {
|
||||
assert_eq!(forked_turn.environments.turn_environments.len(), 1);
|
||||
assert_eq!(
|
||||
forked_turn.environments.turn_environments[0].cwd(),
|
||||
&default_cwd
|
||||
&PathUri::from_abs_path(&default_cwd)
|
||||
);
|
||||
assert_ne!(
|
||||
forked_turn.environments.turn_environments[0].cwd(),
|
||||
&selected_cwd
|
||||
&PathUri::from_abs_path(&selected_cwd)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -299,7 +299,7 @@ pub async fn handle(
|
||||
Ok(FunctionToolOutput::from_text(content, Some(true)))
|
||||
}
|
||||
|
||||
fn single_local_environment_cwd(turn: &TurnContext) -> Result<&AbsolutePathBuf, FunctionCallError> {
|
||||
fn single_local_environment_cwd(turn: &TurnContext) -> Result<AbsolutePathBuf, FunctionCallError> {
|
||||
let [turn_environment] = turn.environments.turn_environments.as_slice() else {
|
||||
return Err(FunctionCallError::RespondToModel(
|
||||
"spawn_agents_on_csv requires exactly one local environment".to_string(),
|
||||
@@ -312,5 +312,12 @@ fn single_local_environment_cwd(turn: &TurnContext) -> Result<&AbsolutePathBuf,
|
||||
));
|
||||
}
|
||||
|
||||
Ok(turn_environment.cwd())
|
||||
// TODO(anp): Migrate spawn_agents_on_csv filesystem access to PathUri before enabling it for
|
||||
// remote environments.
|
||||
turn_environment.cwd().to_abs_path().map_err(|err| {
|
||||
FunctionCallError::RespondToModel(format!(
|
||||
"spawn_agents_on_csv cwd `{}` is not native to the Codex host: {err}",
|
||||
turn_environment.cwd()
|
||||
))
|
||||
})
|
||||
}
|
||||
|
||||
@@ -359,11 +359,18 @@ impl ApplyPatchHandler {
|
||||
"apply_patch is unavailable in this session".to_string(),
|
||||
));
|
||||
};
|
||||
let cwd = turn_environment.cwd().clone();
|
||||
// TODO(anp): Migrate apply-patch verification and permission accounting to PathUri so
|
||||
// patches can target environment-native foreign paths without host projection.
|
||||
let cwd = turn_environment.cwd().to_abs_path().map_err(|err| {
|
||||
FunctionCallError::RespondToModel(format!(
|
||||
"apply_patch cwd `{}` is not native to the Codex host: {err}",
|
||||
turn_environment.cwd()
|
||||
))
|
||||
})?;
|
||||
let fs = turn_environment.environment.get_filesystem();
|
||||
let sandbox = turn.file_system_sandbox_context(
|
||||
/*additional_permissions*/ None,
|
||||
turn_environment.cwd_uri(),
|
||||
turn_environment.cwd(),
|
||||
);
|
||||
match codex_apply_patch::verify_apply_patch_args(args, &cwd, fs.as_ref(), Some(&sandbox))
|
||||
.await
|
||||
|
||||
@@ -114,10 +114,15 @@ async fn to_extension_call(invocation: &ToolInvocation) -> ExtensionToolCall {
|
||||
ConversationHistory::new(invocation.session.clone_history().await.into_raw_items());
|
||||
let mut environments = Vec::with_capacity(invocation.turn.environments.turn_environments.len());
|
||||
for environment in &invocation.turn.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.environment_id,
|
||||
environment.cwd().as_path(),
|
||||
native_cwd.as_path(),
|
||||
SandboxPermissions::UseDefault,
|
||||
/*additional_permissions*/ None,
|
||||
)
|
||||
@@ -125,10 +130,10 @@ async fn to_extension_call(invocation: &ToolInvocation) -> ExtensionToolCall {
|
||||
.additional_permissions;
|
||||
let file_system_sandbox_context = invocation
|
||||
.turn
|
||||
.file_system_sandbox_context(additional_permissions, environment.cwd_uri());
|
||||
.file_system_sandbox_context(additional_permissions, environment.cwd());
|
||||
environments.push(ToolEnvironment {
|
||||
environment_id: environment.environment_id.clone(),
|
||||
cwd: environment.cwd().clone(),
|
||||
cwd: native_cwd,
|
||||
file_system: environment.environment.get_filesystem(),
|
||||
file_system_sandbox_context,
|
||||
});
|
||||
@@ -315,7 +320,7 @@ mod tests {
|
||||
.environments
|
||||
.turn_environments
|
||||
.iter()
|
||||
.map(|environment| Some(environment.cwd_uri().clone()))
|
||||
.map(|environment| Some(environment.cwd().clone()))
|
||||
.collect::<Vec<_>>();
|
||||
let history_item = ResponseItem::Message {
|
||||
id: None,
|
||||
|
||||
@@ -70,8 +70,16 @@ impl RequestPermissionsHandler {
|
||||
"request_permissions requires a primary environment".to_string(),
|
||||
));
|
||||
};
|
||||
// TODO(anp): Migrate request_permissions parsing and permission profiles to PathUri so
|
||||
// environment-native foreign paths do not require host conversion.
|
||||
let native_cwd = turn_environment.cwd().to_abs_path().map_err(|err| {
|
||||
FunctionCallError::RespondToModel(format!(
|
||||
"request_permissions cwd `{}` is not native to the Codex host: {err}",
|
||||
turn_environment.cwd()
|
||||
))
|
||||
})?;
|
||||
let mut args: RequestPermissionsArgs =
|
||||
parse_arguments_with_base_path(&arguments, turn_environment.cwd())?;
|
||||
parse_arguments_with_base_path(&arguments, &native_cwd)?;
|
||||
args.permissions = normalize_additional_permissions(args.permissions.into())
|
||||
.map(codex_protocol::request_permissions::RequestPermissionProfile::from)
|
||||
.map_err(FunctionCallError::RespondToModel)?;
|
||||
|
||||
@@ -129,13 +129,21 @@ impl ExecCommandHandler {
|
||||
"unified exec is unavailable in this session".to_string(),
|
||||
));
|
||||
};
|
||||
// TODO(anp): Resolve tool paths using the selected environment's native path convention
|
||||
// so unified exec can support relative paths in foreign environments.
|
||||
let native_environment_cwd = turn_environment.cwd().to_abs_path().map_err(|err| {
|
||||
FunctionCallError::RespondToModel(format!(
|
||||
"environment cwd `{}` is not native to the Codex host: {err}",
|
||||
turn_environment.cwd()
|
||||
))
|
||||
})?;
|
||||
let cwd = environment_args
|
||||
.workdir
|
||||
.as_deref()
|
||||
.filter(|workdir| !workdir.is_empty())
|
||||
.map_or_else(
|
||||
|| turn_environment.cwd().clone(),
|
||||
|workdir| turn_environment.cwd().join(workdir),
|
||||
|| native_environment_cwd.clone(),
|
||||
|workdir| native_environment_cwd.join(workdir),
|
||||
);
|
||||
let environment = Arc::clone(&turn_environment.environment);
|
||||
let fs = environment.get_filesystem();
|
||||
@@ -277,7 +285,7 @@ impl ExecCommandHandler {
|
||||
yield_time_ms,
|
||||
max_output_tokens,
|
||||
cwd,
|
||||
sandbox_cwd: turn_environment.cwd().clone(),
|
||||
sandbox_cwd: native_environment_cwd,
|
||||
turn_environment: turn_environment.clone(),
|
||||
shell_mode,
|
||||
network: context.turn.network.clone(),
|
||||
|
||||
@@ -143,11 +143,18 @@ impl ViewImageHandler {
|
||||
"view_image is unavailable in this session".to_string(),
|
||||
));
|
||||
};
|
||||
let cwd = turn_environment.cwd().clone();
|
||||
// TODO(anp): Resolve tool paths using the selected environment's native path convention
|
||||
// so view_image can support relative paths in foreign environments.
|
||||
let cwd = turn_environment.cwd().to_abs_path().map_err(|err| {
|
||||
FunctionCallError::RespondToModel(format!(
|
||||
"environment cwd `{}` is not native to the Codex host: {err}",
|
||||
turn_environment.cwd()
|
||||
))
|
||||
})?;
|
||||
let abs_path = cwd.join(path);
|
||||
let sandbox = turn.file_system_sandbox_context(
|
||||
/*additional_permissions*/ None,
|
||||
turn_environment.cwd_uri(),
|
||||
turn_environment.cwd(),
|
||||
);
|
||||
let fs = turn_environment.environment.get_filesystem();
|
||||
let path_uri = PathUri::from_abs_path(&abs_path);
|
||||
@@ -272,6 +279,7 @@ mod tests {
|
||||
use crate::turn_diff_tracker::TurnDiffTracker;
|
||||
use codex_protocol::models::PermissionProfile;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use codex_utils_path_uri::PathUri;
|
||||
use core_test_support::TempDirExt;
|
||||
use pretty_assertions::assert_eq;
|
||||
use serde_json::json;
|
||||
@@ -288,7 +296,7 @@ mod tests {
|
||||
turn.environments.turn_environments[0] = TurnEnvironment::new(
|
||||
current.environment_id,
|
||||
current.environment,
|
||||
cwd,
|
||||
PathUri::from_abs_path(&cwd),
|
||||
current.shell,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ fn test_turn_environment(environment_id: &str) -> crate::session::turn_context::
|
||||
crate::session::turn_context::TurnEnvironment::new(
|
||||
environment_id.to_string(),
|
||||
std::sync::Arc::new(codex_exec_server::Environment::default_for_tests()),
|
||||
std::env::temp_dir().abs(),
|
||||
PathUri::from_abs_path(&std::env::temp_dir().abs()),
|
||||
/*shell*/ None,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -427,6 +427,7 @@ mod tests {
|
||||
use codex_exec_server::Environment;
|
||||
use codex_exec_server::LOCAL_ENVIRONMENT_ID;
|
||||
use codex_tools::ZshForkConfig;
|
||||
use codex_utils_path_uri::PathUri;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
use tempfile::tempdir;
|
||||
@@ -435,7 +436,7 @@ mod tests {
|
||||
TurnEnvironment::new(
|
||||
LOCAL_ENVIRONMENT_ID.to_string(),
|
||||
Arc::new(Environment::default_for_tests()),
|
||||
cwd,
|
||||
PathUri::from_abs_path(&cwd),
|
||||
/*shell*/ None,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ wine_rust_test(
|
||||
"//codex-rs/protocol",
|
||||
"//codex-rs/utils/path-uri",
|
||||
"@crates//:anyhow",
|
||||
"@crates//:base64",
|
||||
"@crates//:pretty_assertions",
|
||||
"@crates//:serde_json",
|
||||
"@crates//:tempfile",
|
||||
|
||||
@@ -2,9 +2,18 @@
|
||||
|
||||
use anyhow::Context;
|
||||
use anyhow::Result;
|
||||
use app_test_support::PathBufExt;
|
||||
use app_test_support::TestAppServer;
|
||||
use codex_app_server_protocol::JSONRPCError;
|
||||
use app_test_support::create_mock_responses_server_repeating_assistant;
|
||||
use app_test_support::to_response;
|
||||
use app_test_support::write_mock_responses_config_toml;
|
||||
use codex_app_server_protocol::RequestId;
|
||||
use codex_app_server_protocol::ThreadStartParams;
|
||||
use codex_app_server_protocol::ThreadStartResponse;
|
||||
use codex_app_server_protocol::TurnEnvironmentParams;
|
||||
use codex_app_server_protocol::TurnStartParams;
|
||||
use codex_app_server_protocol::TurnStartResponse;
|
||||
use codex_app_server_protocol::UserInput as V2UserInput;
|
||||
use codex_exec_server::REMOTE_ENVIRONMENT_ID;
|
||||
use codex_exec_server::CODEX_EXEC_SERVER_URL_ENV_VAR;
|
||||
use codex_features::Feature;
|
||||
@@ -26,20 +35,22 @@ use core_test_support::responses::start_mock_server;
|
||||
use core_test_support::test_codex::test_codex;
|
||||
use core_test_support::test_codex::turn_permission_fields;
|
||||
use core_test_support::wait_for_event;
|
||||
use codex_utils_path_uri::LegacyAppPathString;
|
||||
use codex_utils_path_uri::PathUri;
|
||||
use pretty_assertions::assert_eq;
|
||||
use serde_json::json;
|
||||
use std::collections::BTreeMap;
|
||||
use tempfile::TempDir;
|
||||
use tokio::time::timeout;
|
||||
use wine_exec_server_test_support::WineExecServer;
|
||||
|
||||
const APP_SERVER_READ_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(10);
|
||||
const CALL_ID: &str = "wine-cmd-smoke";
|
||||
const COMMAND: &str = r#"if ((Get-Location).Path -ne 'C:\windows') { exit 1 }"#;
|
||||
const INVALID_REQUEST_ERROR_CODE: i64 = -32600;
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn windows_exec_server_runs_with_native_shell_and_cwd() -> Result<()> {
|
||||
const CALL_ID: &str = "wine-cmd-smoke";
|
||||
const COMMAND: &str = r#"if ((Get-Location).Path -ne 'C:\windows') { exit 1 }"#;
|
||||
|
||||
WineExecServer
|
||||
.scope(|exec_server_url| async move {
|
||||
let server = start_mock_server().await;
|
||||
@@ -162,10 +173,20 @@ async fn windows_exec_server_runs_with_native_shell_and_cwd() -> Result<()> {
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn app_server_rejects_windows_environment_cwd() -> Result<()> {
|
||||
async fn app_server_starts_thread_with_windows_environment_native_cwd() -> Result<()> {
|
||||
WineExecServer
|
||||
.scope(|exec_server_url| async move {
|
||||
let codex_home = TempDir::new()?;
|
||||
let server = create_mock_responses_server_repeating_assistant("done").await;
|
||||
write_mock_responses_config_toml(
|
||||
codex_home.path(),
|
||||
&server.uri(),
|
||||
&BTreeMap::new(),
|
||||
100_000,
|
||||
/*requires_openai_auth*/ None,
|
||||
"mock",
|
||||
"compact",
|
||||
)?;
|
||||
let mut app_server = TestAppServer::new_with_env(
|
||||
codex_home.path(),
|
||||
&[(
|
||||
@@ -177,28 +198,53 @@ async fn app_server_rejects_windows_environment_cwd() -> Result<()> {
|
||||
timeout(APP_SERVER_READ_TIMEOUT, app_server.initialize()).await??;
|
||||
|
||||
let request_id = app_server
|
||||
.send_raw_request(
|
||||
"thread/start",
|
||||
Some(json!({
|
||||
"environments": [{
|
||||
"environmentId": REMOTE_ENVIRONMENT_ID,
|
||||
"cwd": r"C:\windows",
|
||||
}],
|
||||
})),
|
||||
)
|
||||
.send_thread_start_request(ThreadStartParams {
|
||||
environments: Some(vec![TurnEnvironmentParams {
|
||||
environment_id: REMOTE_ENVIRONMENT_ID.to_string(),
|
||||
cwd: serde_json::from_value::<LegacyAppPathString>(json!(r"C:\windows"))?,
|
||||
}]),
|
||||
..Default::default()
|
||||
})
|
||||
.await?;
|
||||
let error: JSONRPCError = timeout(
|
||||
let response = timeout(
|
||||
APP_SERVER_READ_TIMEOUT,
|
||||
app_server.read_stream_until_error_message(RequestId::Integer(request_id)),
|
||||
app_server.read_stream_until_response_message(RequestId::Integer(request_id)),
|
||||
)
|
||||
.await??;
|
||||
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);
|
||||
// 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.
|
||||
assert_eq!(response.instruction_sources, Vec::new());
|
||||
// TODO(anp): Report the implicit built-in permission profile instead of None.
|
||||
assert_eq!(response.active_permission_profile, None);
|
||||
|
||||
assert_eq!(error.id, RequestId::Integer(request_id));
|
||||
assert_eq!(error.error.code, INVALID_REQUEST_ERROR_CODE);
|
||||
assert_eq!(
|
||||
error.error.message,
|
||||
"Invalid request: AbsolutePathBuf deserialized without a base path"
|
||||
);
|
||||
let turn_request_id = app_server
|
||||
.send_turn_start_request(TurnStartParams {
|
||||
thread_id: response.thread.id,
|
||||
client_user_message_id: None,
|
||||
input: vec![V2UserInput::Text {
|
||||
text: "say done".to_string(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
..Default::default()
|
||||
})
|
||||
.await?;
|
||||
let turn_response = timeout(
|
||||
APP_SERVER_READ_TIMEOUT,
|
||||
app_server.read_stream_until_response_message(RequestId::Integer(turn_request_id)),
|
||||
)
|
||||
.await??;
|
||||
let _: TurnStartResponse = to_response(turn_response)?;
|
||||
timeout(
|
||||
APP_SERVER_READ_TIMEOUT,
|
||||
app_server.read_stream_until_notification_message("turn/completed"),
|
||||
)
|
||||
.await??;
|
||||
|
||||
Ok(())
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user