core: move workspace roots onto environments

This commit is contained in:
pakrym-oai
2026-07-09 19:39:42 -07:00
parent dd51887ce7
commit 014742db1f
47 changed files with 390 additions and 308 deletions
@@ -4496,6 +4496,16 @@
},
"environmentId": {
"type": "string"
},
"runtimeWorkspaceRoots": {
"description": "Environment-native runtime workspace roots. Omitted inherits the request's top-level `runtimeWorkspaceRoots` fallback.",
"items": {
"$ref": "#/definitions/LegacyAppPathString"
},
"type": [
"array",
"null"
]
}
},
"required": [
@@ -20170,6 +20170,16 @@
},
"environmentId": {
"type": "string"
},
"runtimeWorkspaceRoots": {
"description": "Environment-native runtime workspace roots. Omitted inherits the request's top-level `runtimeWorkspaceRoots` fallback.",
"items": {
"$ref": "#/definitions/v2/LegacyAppPathString"
},
"type": [
"array",
"null"
]
}
},
"required": [
@@ -17949,6 +17949,16 @@
},
"environmentId": {
"type": "string"
},
"runtimeWorkspaceRoots": {
"description": "Environment-native runtime workspace roots. Omitted inherits the request's top-level `runtimeWorkspaceRoots` fallback.",
"items": {
"$ref": "#/definitions/LegacyAppPathString"
},
"type": [
"array",
"null"
]
}
},
"required": [
@@ -281,6 +281,16 @@
},
"environmentId": {
"type": "string"
},
"runtimeWorkspaceRoots": {
"description": "Environment-native runtime workspace roots. Omitted inherits the request's top-level `runtimeWorkspaceRoots` fallback.",
"items": {
"$ref": "#/definitions/LegacyAppPathString"
},
"type": [
"array",
"null"
]
}
},
"required": [
@@ -362,6 +362,16 @@
},
"environmentId": {
"type": "string"
},
"runtimeWorkspaceRoots": {
"description": "Environment-native runtime workspace roots. Omitted inherits the request's top-level `runtimeWorkspaceRoots` fallback.",
"items": {
"$ref": "#/definitions/LegacyAppPathString"
},
"type": [
"array",
"null"
]
}
},
"required": [
@@ -3,4 +3,9 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { LegacyAppPathString } from "../LegacyAppPathString";
export type TurnEnvironmentParams = { environmentId: string, cwd: LegacyAppPathString, };
export type TurnEnvironmentParams = { environmentId: string, cwd: LegacyAppPathString,
/**
* Environment-native runtime workspace roots. Omitted inherits the
* request's top-level `runtimeWorkspaceRoots` fallback.
*/
runtimeWorkspaceRoots?: Array<LegacyAppPathString> | null, };
@@ -4217,13 +4217,15 @@ fn turn_start_params_round_trip_environments() {
let raw_cwd = r"C:\workspace";
let cwd: LegacyAppPathString =
serde_json::from_value(json!(raw_cwd)).expect("API path should deserialize");
let workspace_root = cwd.clone();
let params: TurnStartParams = serde_json::from_value(json!({
"threadId": "thread_123",
"input": [],
"environments": [
{
"environmentId": "local",
"cwd": cwd
"cwd": cwd,
"runtimeWorkspaceRoots": [workspace_root]
}
],
}))
@@ -4234,6 +4236,7 @@ fn turn_start_params_round_trip_environments() {
Some(vec![TurnEnvironmentParams {
environment_id: "local".to_string(),
cwd: cwd.clone(),
runtime_workspace_roots: Some(vec![workspace_root.clone()]),
}])
);
assert_eq!(
@@ -4247,7 +4250,8 @@ fn turn_start_params_round_trip_environments() {
Some(&json!([
{
"environmentId": "local",
"cwd": cwd
"cwd": cwd,
"runtimeWorkspaceRoots": [workspace_root]
}
]))
);
@@ -41,6 +41,10 @@ pub enum TurnStatus {
pub struct TurnEnvironmentParams {
pub environment_id: String,
pub cwd: LegacyAppPathString,
/// Environment-native runtime workspace roots. Omitted inherits the
/// request's top-level `runtimeWorkspaceRoots` fallback.
#[ts(optional = nullable)]
pub runtime_workspace_roots: Option<Vec<LegacyAppPathString>>,
}
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS)]
+2 -2
View File
@@ -137,7 +137,7 @@ Example with notification opt-out:
## API Overview
- `thread/start` — create a new thread; emits `thread/started` (including the current `thread.status`) and auto-subscribes you to turn/item events for that thread. When the request includes a `cwd` and the resolved sandbox is `workspace-write` or full access, app-server also marks that project as trusted in the user `config.toml`. Pass `sessionStartSource: "clear"` when starting a replacement thread after clearing the current session so `SessionStart` hooks receive `source: "clear"` instead of the default `"startup"`. Experimental `allowProviderModelFallback` lets providers backed by an authoritative static model catalog replace an unavailable requested `model` with the catalog default; dynamic or cached catalogs preserve the requested model. Experimental `runtimeWorkspaceRoots` replaces the thread-scoped runtime workspace roots used to materialize `:workspace_roots`; paths must be absolute. For permissions, prefer experimental `permissions` profile selection by id; the legacy `sandbox` shorthand is still accepted but cannot be combined with `permissions`. Deprecated experimental `multiAgentMode` is ignored; use Ultra reasoning effort for proactive multi-agent behavior. Experimental `environments` selects the sticky execution environments for turns on the thread; omit it to use the server default, pass `[]` to disable environments, or pass explicit environment ids with per-environment `cwd`. Experimental `selectedCapabilityRoots` selects environment-owned plugin or standalone-skill roots using environment-native absolute paths. Skills found below those roots are listed and read through the owning environment. Stdio MCP servers declared by selected plugins are started in that environment, and HTTP MCP connections use that environment's HTTP client.
- `thread/start` — create a new thread; emits `thread/started` (including the current `thread.status`) and auto-subscribes you to turn/item events for that thread. When the request includes a `cwd` and the resolved sandbox is `workspace-write` or full access, app-server also marks that project as trusted in the user `config.toml`. Pass `sessionStartSource: "clear"` when starting a replacement thread after clearing the current session so `SessionStart` hooks receive `source: "clear"` instead of the default `"startup"`. Experimental `allowProviderModelFallback` lets providers backed by an authoritative static model catalog replace an unavailable requested `model` with the catalog default; dynamic or cached catalogs preserve the requested model. Experimental `runtimeWorkspaceRoots` supplies the default runtime workspace roots used to materialize `:workspace_roots`; paths must be absolute. For permissions, prefer experimental `permissions` profile selection by id; the legacy `sandbox` shorthand is still accepted but cannot be combined with `permissions`. Deprecated experimental `multiAgentMode` is ignored; use Ultra reasoning effort for proactive multi-agent behavior. Experimental `environments` selects the sticky execution environments for turns on the thread; omit it to use the server default, pass `[]` to disable environments, or pass explicit environment ids with per-environment `cwd` and optional environment-native `runtimeWorkspaceRoots`. Omitted per-environment roots inherit the top-level fallback; an empty list explicitly selects no roots for that environment. Experimental `selectedCapabilityRoots` selects environment-owned plugin or standalone-skill roots using environment-native absolute paths. Skills found below those roots are listed and read through the owning environment. Stdio MCP servers declared by selected plugins are started in that environment, and HTTP MCP connections use that environment's HTTP client.
- `thread/resume` — reopen an existing thread by id so subsequent `turn/start` calls append to it. Accepts the same permission override rules as `thread/start`.
- `thread/fork` — fork an existing thread into a new thread id by copying the stored history; pass an optional `lastTurnId` to copy history only through that turn, inclusive, and drop later turns from the fork. An in-progress `lastTurnId` is rejected. If `lastTurnId` is null while the source thread is mid-turn, the fork records the same interruption marker as `turn/interrupt` instead of inheriting an unmarked partial turn suffix. The returned `thread.forkedFromId` points at the source thread when known. Accepts `ephemeral: true` for an in-memory temporary fork, emits `thread/started` (including the current `thread.status`), and auto-subscribes you to turn/item events for the new thread. Experimental clients can pass `excludeTurns: true` when they plan to page fork history via `thread/turns/list` instead of receiving the full turn array immediately. Accepts the same permission override rules as `thread/start`.
- `thread/start`, `thread/resume`, and `thread/fork` responses include the legacy `sandbox` compatibility projection. `instructionSources` lists loaded instruction files using each source environment's native absolute path syntax, including files loaded from remote environments. Experimental clients can read `runtimeWorkspaceRoots` for the thread-scoped runtime roots and `activePermissionProfile` for the named or implicit built-in profile identity/provenance when known. Their deprecated experimental `multiAgentMode` field, and the corresponding thread setting, always report `explicitRequestOnly`; Ultra reasoning effort is the source of proactive multi-agent behavior.
@@ -168,7 +168,7 @@ Example with notification opt-out:
- `thread/backgroundTerminals/list` — list running background terminals for a loaded thread (experimental; requires `capabilities.experimentalApi`); returns `data` with the running terminal ids.
- `thread/backgroundTerminals/terminate` — terminate one running background terminal by app-server `processId` (experimental; requires `capabilities.experimentalApi`); returns whether a process was terminated.
- `thread/rollback` — deprecated and will be removed soon. Drop the last N turns from the agent’s in-memory context and persist a rollback marker in the rollout so future resumes see the pruned history; returns the updated `thread` (with `turns` populated) on success.
- `turn/start` — add user input to a thread and begin Codex generation; responds with the initial `turn` object and streams `turn/started`, `item/*`, and `turn/completed` notifications. `clientUserMessageId` is optional; when supplied, the corresponding `userMessage` item echoes it as `clientId`. Experimental `runtimeWorkspaceRoots` replaces the thread-scoped runtime workspace roots used to materialize `:workspace_roots`; paths must be absolute. Prefer experimental `permissions` profile selection by id for permission overrides; the legacy `sandboxPolicy` field is still accepted but cannot be combined with `permissions`. For `collaborationMode`, `settings.developer_instructions: null` means "use built-in instructions for the selected mode". Deprecated experimental `multiAgentMode` is ignored; Ultra reasoning effort selects proactive behavior.
- `turn/start` — add user input to a thread and begin Codex generation; responds with the initial `turn` object and streams `turn/started`, `item/*`, and `turn/completed` notifications. `clientUserMessageId` is optional; when supplied, the corresponding `userMessage` item echoes it as `clientId`. Experimental `runtimeWorkspaceRoots` supplies the default roots for newly resolved environment selections. Explicit `environments[].runtimeWorkspaceRoots` override that fallback with environment-native absolute paths. Prefer experimental `permissions` profile selection by id for permission overrides; the legacy `sandboxPolicy` field is still accepted but cannot be combined with `permissions`. For `collaborationMode`, `settings.developer_instructions: null` means "use built-in instructions for the selected mode". Deprecated experimental `multiAgentMode` is ignored; Ultra reasoning effort selects proactive behavior.
- `thread/inject_items` — append raw Responses API items to a loaded thread’s model-visible history without starting a user turn; returns `{}` on success.
- `turn/steer` — add user input to an already in-flight regular turn without starting a new turn; returns the active `turnId` that accepted the input. `clientUserMessageId` is optional; when supplied, the corresponding `userMessage` item echoes it as `clientId`. Review and manual compaction turns reject `turn/steer`.
- `turn/interrupt` — request cancellation of an in-flight turn by `(thread_id, turn_id)`; success is an empty `{}` response and the turn finishes with `status: "interrupted"`.
+26 -1
View File
@@ -368,7 +368,6 @@ use codex_core_plugins::remote::RemotePluginShareContext as RemoteCatalogPluginS
use codex_core_plugins::remote::RemotePluginShareSummary as RemoteCatalogPluginShareSummary;
use codex_core_plugins::remote::RemotePluginSummary as RemoteCatalogPluginSummary;
use codex_exec_server::EnvironmentManager;
use codex_exec_server::LOCAL_ENVIRONMENT_ID;
use codex_exec_server::LOCAL_FS;
use codex_features::FEATURES;
use codex_features::Feature;
@@ -465,6 +464,7 @@ use codex_thread_store::ThreadSortKey as StoreThreadSortKey;
use codex_thread_store::ThreadStore;
use codex_thread_store::ThreadStoreError;
use codex_utils_absolute_path::AbsolutePathBuf;
use codex_utils_path_uri::PathUri;
use codex_utils_pty::DEFAULT_OUTPUT_BYTES_CAP;
use std::collections::BTreeMap;
use std::collections::HashMap;
@@ -564,6 +564,7 @@ fn resolve_request_cwd(cwd: Option<PathBuf>) -> Result<Option<AbsolutePathBuf>,
fn resolve_turn_environment_selections(
thread_manager: &ThreadManager,
environments: Option<Vec<TurnEnvironmentParams>>,
fallback_workspace_roots: &[AbsolutePathBuf],
) -> Result<Option<Vec<TurnEnvironmentSelection>>, JSONRPCErrorError> {
let Some(environments) = environments else {
return Ok(None);
@@ -580,9 +581,33 @@ fn resolve_turn_environment_selections(
environment.cwd
))
})?;
let workspace_roots = environment
.runtime_workspace_roots
.map(|roots| {
let mut resolved_roots = Vec::new();
for root in roots {
let root = root.to_inferred_path_uri().ok_or_else(|| {
invalid_request(format!(
"invalid runtime workspace root for environment `{environment_id}`: path `{root}` does not use absolute POSIX or Windows path syntax"
))
})?;
if !resolved_roots.contains(&root) {
resolved_roots.push(root);
}
}
Ok::<_, JSONRPCErrorError>(resolved_roots)
})
.transpose()?
.unwrap_or_else(|| {
fallback_workspace_roots
.iter()
.map(PathUri::from_abs_path)
.collect()
});
selections.push(TurnEnvironmentSelection {
environment_id,
cwd,
workspace_roots,
});
}
thread_manager
@@ -968,8 +968,6 @@ impl ThreadRequestProcessor {
"`permissions` cannot be combined with `sandbox`",
));
}
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,
@@ -1017,7 +1015,7 @@ impl ThreadRequestProcessor {
history_mode.map(Into::into),
session_start_source,
thread_source.map(Into::into),
environment_selections,
environments,
service_name,
allow_provider_model_fallback,
experimental_raw_events,
@@ -1094,7 +1092,7 @@ impl ThreadRequestProcessor {
history_mode: Option<ThreadHistoryMode>,
session_start_source: Option<codex_app_server_protocol::ThreadStartSource>,
thread_source: Option<codex_protocol::protocol::ThreadSource>,
environments: Option<Vec<TurnEnvironmentSelection>>,
environments: Option<Vec<codex_app_server_protocol::TurnEnvironmentParams>>,
service_name: Option<String>,
allow_provider_model_fallback: bool,
experimental_raw_events: bool,
@@ -1188,10 +1186,15 @@ impl ThreadRequestProcessor {
}
}
let environments = environments.unwrap_or_else(|| {
let environments = resolve_turn_environment_selections(
listener_task_context.thread_manager.as_ref(),
environments,
&config.workspace_roots,
)?
.unwrap_or_else(|| {
listener_task_context
.thread_manager
.default_environment_selections(&config.cwd)
.default_environment_selections(&config.cwd, &config.workspace_roots)
});
let dynamic_tools = dynamic_tools.unwrap_or_default();
if !dynamic_tools.is_empty() {
@@ -481,8 +481,18 @@ impl TurnRequestProcessor {
))
})?;
let environment_selections =
resolve_turn_environment_selections(self.thread_manager.as_ref(), params.environments)?;
let runtime_workspace_roots = params
.runtime_workspace_roots
.map(resolve_runtime_workspace_roots);
let fallback_workspace_roots = match runtime_workspace_roots.as_ref() {
Some(workspace_roots) => workspace_roots.clone(),
None => thread.config_snapshot().await.workspace_roots,
};
let environment_selections = resolve_turn_environment_selections(
self.thread_manager.as_ref(),
params.environments,
&fallback_workspace_roots,
)?;
// Map v2 input items to core input items.
let mapped_items: Vec<CoreInputItem> = params
@@ -495,7 +505,12 @@ impl TurnRequestProcessor {
let turn_has_input = !mapped_items.is_empty();
let cwd = resolve_request_cwd(params.cwd)?;
let environments = self
.build_environment_override(thread.as_ref(), cwd, environment_selections)
.build_environment_override(
thread.as_ref(),
cwd,
runtime_workspace_roots.clone(),
environment_selections,
)
.await;
let thread_settings = self
.build_thread_settings_overrides(
@@ -503,7 +518,7 @@ impl TurnRequestProcessor {
ThreadSettingsBuildParams {
method: "turn/start",
environments,
runtime_workspace_roots: params.runtime_workspace_roots,
runtime_workspace_roots,
approval_policy: params.approval_policy,
approvals_reviewer: params.approvals_reviewer,
sandbox_policy: params.sandbox_policy,
@@ -572,33 +587,25 @@ impl TurnRequestProcessor {
&self,
thread: &CodexThread,
cwd: Option<AbsolutePathBuf>,
workspace_roots: Option<Vec<AbsolutePathBuf>>,
environment_selections: Option<Vec<TurnEnvironmentSelection>>,
) -> Option<TurnEnvironmentSelections> {
match (cwd, environment_selections) {
(None, None) => None,
(Some(cwd), None) => {
let environment_selections =
self.thread_manager.default_environment_selections(&cwd);
Some(TurnEnvironmentSelections::new(cwd, environment_selections))
}
(cwd, Some(environment_selections)) => {
let legacy_fallback_cwd = match cwd {
Some(cwd) => cwd,
None => {
let snapshot = thread.config_snapshot().await;
environment_selections
.iter()
.find(|selection| selection.environment_id == LOCAL_ENVIRONMENT_ID)
.and_then(|selection| selection.cwd.to_abs_path().ok())
.unwrap_or_else(|| snapshot.cwd().clone())
}
};
Some(TurnEnvironmentSelections::new(
legacy_fallback_cwd,
environment_selections,
))
}
if cwd.is_none() && workspace_roots.is_none() && environment_selections.is_none() {
return None;
}
let snapshot = thread.config_snapshot().await;
let legacy_fallback_cwd = cwd.unwrap_or_else(|| snapshot.cwd().clone());
let legacy_fallback_workspace_roots = workspace_roots.unwrap_or(snapshot.workspace_roots);
let environment_selections = environment_selections.unwrap_or_else(|| {
self.thread_manager.default_environment_selections(
&legacy_fallback_cwd,
&legacy_fallback_workspace_roots,
)
});
Some(TurnEnvironmentSelections::new(
legacy_fallback_cwd,
environment_selections,
))
}
async fn build_thread_settings_overrides(
@@ -654,8 +661,7 @@ impl TurnRequestProcessor {
|| collaboration_mode.is_some()
|| personality.is_some();
let runtime_workspace_roots =
runtime_workspace_roots_request.map(resolve_runtime_workspace_roots);
let runtime_workspace_roots = runtime_workspace_roots_request;
let approval_policy =
approval_policy.map(codex_app_server_protocol::AskForApproval::to_core);
let approvals_reviewer =
@@ -715,7 +721,6 @@ impl TurnRequestProcessor {
thread
.preview_thread_settings_overrides(CodexThreadSettingsOverrides {
environments: environments.clone(),
workspace_roots: runtime_workspace_roots.clone(),
approval_policy,
approvals_reviewer,
sandbox_policy: sandbox_policy.clone(),
@@ -738,7 +743,6 @@ impl TurnRequestProcessor {
Ok(codex_protocol::protocol::ThreadSettingsOverrides {
environments,
workspace_roots: runtime_workspace_roots,
profile_workspace_roots,
approval_policy,
approvals_reviewer,
@@ -763,7 +767,12 @@ impl TurnRequestProcessor {
let (_, thread) = self.load_thread(&params.thread_id).await?;
let cwd = resolve_request_cwd(params.cwd)?;
let environments = self
.build_environment_override(thread.as_ref(), cwd, /*environment_selections*/ None)
.build_environment_override(
thread.as_ref(),
cwd,
/*workspace_roots*/ None,
/*environment_selections*/ None,
)
.await;
let thread_settings = self
.build_thread_settings_overrides(
@@ -192,6 +192,7 @@ impl TestAppServer {
Ok(TurnEnvironmentParams {
environment_id: selection.environment_id.clone(),
cwd: selection.cwd.clone().into(),
runtime_workspace_roots: None,
})
}
@@ -381,6 +381,7 @@ url = "{mcp_server_url}/mcp"
capability_root.path().to_path_buf(),
)?
.into(),
runtime_workspace_roots: None,
}]),
selected_capability_roots: Some(vec![SelectedCapabilityRoot {
id: "late-plugin@1".to_string(),
@@ -366,6 +366,7 @@ async fn selected_capabilities_become_available_between_samples_in_one_turn() ->
environments: Some(vec![TurnEnvironmentParams {
environment_id: LOCAL_ENVIRONMENT_ID.to_string(),
cwd: fixture.environment_cwd.into(),
runtime_workspace_roots: None,
}]),
collaboration_mode: Some(CollaborationMode {
mode: ModeKind::Plan,
@@ -644,6 +645,7 @@ async fn start_thread(
environments: Some(vec![TurnEnvironmentParams {
environment_id: LOCAL_ENVIRONMENT_ID.to_string(),
cwd: environment_cwd.into(),
runtime_workspace_roots: None,
}]),
selected_capability_roots: Some(vec![selected_root]),
..Default::default()
@@ -674,6 +676,7 @@ async fn run_turn(
environments: Some(vec![TurnEnvironmentParams {
environment_id: LOCAL_ENVIRONMENT_ID.to_string(),
cwd: environment_cwd.into(),
runtime_workspace_roots: None,
}]),
..Default::default()
})
@@ -619,6 +619,7 @@ async fn thread_start_rejects_unknown_environment_as_invalid_request() -> Result
codex_home.path().to_path_buf(),
)?
.into(),
runtime_workspace_roots: None,
}]),
..Default::default()
})
@@ -655,6 +656,7 @@ async fn thread_start_rejects_relative_environment_cwd_as_invalid_request() -> R
environments: Some(vec![TurnEnvironmentParams {
environment_id: environment_id.clone(),
cwd: serde_json::from_value(json!("relative"))?,
runtime_workspace_roots: None,
}]),
..Default::default()
})
@@ -1378,6 +1378,7 @@ async fn turn_start_rejects_unknown_environment_before_starting_turn() -> Result
codex_home.path().to_path_buf(),
)?
.into(),
runtime_workspace_roots: None,
}]),
..Default::default()
})
@@ -2711,8 +2712,7 @@ async fn turn_start_updates_sandbox_and_cwd_between_turns_v2() -> Result<()> {
#[cfg(unix)]
#[tokio::test]
async fn turn_start_permission_profile_rebinds_runtime_workspace_roots_between_turns() -> Result<()>
{
async fn turn_environment_workspace_roots_override_and_inherit_turn_fallback() -> Result<()> {
skip_if_no_network!(Ok(()));
let tmp = TempDir::new()?;
@@ -2720,12 +2720,19 @@ async fn turn_start_permission_profile_rebinds_runtime_workspace_roots_between_t
std::fs::create_dir(&codex_home)?;
let old_root = tmp.path().join("old-root");
let new_root = tmp.path().join("new-root");
let fallback_root = tmp.path().join("fallback-root");
std::fs::create_dir(&old_root)?;
std::fs::create_dir(&new_root)?;
std::fs::create_dir(&fallback_root)?;
let old_root_text = old_root.to_string_lossy().into_owned();
let new_root_text = new_root.to_string_lossy().into_owned();
let fallback_root_text = fallback_root.to_string_lossy().into_owned();
let old_root = codex_utils_absolute_path::AbsolutePathBuf::from_absolute_path(old_root)?;
let new_root = codex_utils_absolute_path::AbsolutePathBuf::from_absolute_path(new_root)?;
let fallback_root =
codex_utils_absolute_path::AbsolutePathBuf::from_absolute_path(fallback_root)?;
let environment_cwd =
codex_utils_absolute_path::AbsolutePathBuf::from_absolute_path(tmp.path())?;
let server = responses::start_mock_server().await;
let response_mock = responses::mount_sse_sequence(
@@ -2794,7 +2801,12 @@ stream_max_retries = 0
text: "select dev profile".to_string(),
text_elements: Vec::new(),
}],
runtime_workspace_roots: Some(vec![old_root]),
runtime_workspace_roots: Some(vec![fallback_root]),
environments: Some(vec![TurnEnvironmentParams {
environment_id: "local".to_string(),
cwd: environment_cwd.clone().into(),
runtime_workspace_roots: Some(vec![old_root.into()]),
}]),
permissions: Some("dev".to_string()),
..Default::default()
})
@@ -2819,6 +2831,11 @@ stream_max_retries = 0
text_elements: Vec::new(),
}],
runtime_workspace_roots: Some(vec![new_root]),
environments: Some(vec![TurnEnvironmentParams {
environment_id: "local".to_string(),
cwd: environment_cwd.into(),
runtime_workspace_roots: None,
}]),
..Default::default()
})
.await?;
@@ -2848,15 +2865,15 @@ stream_max_retries = 0
let first_permissions = latest_permissions_instructions(&requests[0]);
assert!(first_permissions.contains(&old_root_text));
assert!(
!first_permissions.contains(&new_root_text),
"first turn should materialize the initial runtime workspace root"
!first_permissions.contains(&fallback_root_text),
"environment roots should override the turn-level fallback"
);
let second_permissions = latest_permissions_instructions(&requests[1]);
assert!(second_permissions.contains(&new_root_text));
assert!(
!second_permissions.contains(&old_root_text),
"second turn should rebind :workspace_roots to the updated runtime workspace root"
"omitted environment roots should inherit the updated turn-level fallback"
);
Ok(())
@@ -3010,6 +3027,7 @@ fn environment_params(ids: Option<&[&str]>, cwd: &Path) -> Option<Vec<TurnEnviro
.map(|id| TurnEnvironmentParams {
environment_id: (*id).to_string(),
cwd: cwd.abs().into(),
runtime_workspace_roots: None,
})
.collect()
})
-3
View File
@@ -143,7 +143,6 @@ impl ThreadConfigSnapshot {
#[derive(Clone, Default)]
pub struct CodexThreadSettingsOverrides {
pub environments: Option<TurnEnvironmentSelections>,
pub workspace_roots: Option<Vec<AbsolutePathBuf>>,
pub profile_workspace_roots: Option<Vec<AbsolutePathBuf>>,
pub approval_policy: Option<AskForApproval>,
pub approvals_reviewer: Option<ApprovalsReviewer>,
@@ -369,7 +368,6 @@ impl CodexThread {
) -> SessionSettingsUpdate {
let CodexThreadSettingsOverrides {
environments,
workspace_roots,
profile_workspace_roots,
approval_policy,
approvals_reviewer,
@@ -396,7 +394,6 @@ impl CodexThread {
SessionSettingsUpdate {
environments,
workspace_roots,
profile_workspace_roots,
approval_policy,
approvals_reviewer,
+25 -6
View File
@@ -22,6 +22,7 @@ use crate::shell_snapshot::ShellSnapshot;
pub(crate) fn default_thread_environment_selections(
environment_manager: &EnvironmentManager,
cwd: &AbsolutePathBuf,
workspace_roots: &[AbsolutePathBuf],
) -> Vec<TurnEnvironmentSelection> {
environment_manager
.default_environment_ids()
@@ -29,6 +30,7 @@ pub(crate) fn default_thread_environment_selections(
.map(|environment_id| TurnEnvironmentSelection {
environment_id,
cwd: PathUri::from_abs_path(cwd),
workspace_roots: workspace_roots.iter().map(PathUri::from_abs_path).collect(),
})
.collect()
}
@@ -176,7 +178,8 @@ impl ThreadEnvironments {
Some(local_shell)
};
let mut turn_environment =
TurnEnvironment::new(selection.environment_id, environment, selection.cwd, shell);
TurnEnvironment::new(selection.environment_id, environment, selection.cwd, shell)
.with_workspace_roots(selection.workspace_roots);
let task = shell_snapshot
.build(turn_environment.clone())
.boxed()
@@ -406,10 +409,11 @@ mod tests {
.await;
assert_eq!(
default_thread_environment_selections(&manager, &cwd),
default_thread_environment_selections(&manager, &cwd, std::slice::from_ref(&cwd)),
vec![TurnEnvironmentSelection {
environment_id: REMOTE_ENVIRONMENT_ID.to_string(),
cwd: cwd_uri,
cwd: cwd_uri.clone(),
workspace_roots: vec![cwd_uri],
}]
);
}
@@ -434,15 +438,17 @@ url = "ws://127.0.0.1:8765"
.expect("environment manager");
assert_eq!(
default_thread_environment_selections(&manager, &cwd),
default_thread_environment_selections(&manager, &cwd, std::slice::from_ref(&cwd)),
vec![
TurnEnvironmentSelection {
environment_id: LOCAL_ENVIRONMENT_ID.to_string(),
cwd: cwd_uri.clone(),
workspace_roots: vec![cwd_uri.clone()],
},
TurnEnvironmentSelection {
environment_id: REMOTE_ENVIRONMENT_ID.to_string(),
cwd: cwd_uri,
cwd: cwd_uri.clone(),
workspace_roots: vec![cwd_uri],
},
]
);
@@ -454,7 +460,7 @@ url = "ws://127.0.0.1:8765"
let manager = EnvironmentManager::without_environments();
assert_eq!(
default_thread_environment_selections(&manager, &cwd),
default_thread_environment_selections(&manager, &cwd, std::slice::from_ref(&cwd)),
Vec::<TurnEnvironmentSelection>::new()
);
}
@@ -476,6 +482,7 @@ url = "ws://127.0.0.1:8765"
turn_environments.update_selections(&[TurnEnvironmentSelection {
environment_id: LOCAL_ENVIRONMENT_ID.to_string(),
cwd: PathUri::from_abs_path(&cwd),
workspace_roots: Vec::new(),
}]);
let snapshot = turn_environments.snapshot().await;
@@ -496,6 +503,7 @@ url = "ws://127.0.0.1:8765"
let first = TurnEnvironmentSelection {
environment_id: LOCAL_ENVIRONMENT_ID.to_string(),
cwd: cwd_uri.clone(),
workspace_roots: Vec::new(),
};
let resolved = resolve_turn_environments(
@@ -505,6 +513,7 @@ url = "ws://127.0.0.1:8765"
TurnEnvironmentSelection {
environment_id: LOCAL_ENVIRONMENT_ID.to_string(),
cwd: cwd_uri.join("other").expect("other cwd URI"),
workspace_roots: Vec::new(),
},
],
)
@@ -525,6 +534,7 @@ url = "ws://127.0.0.1:8765"
&[TurnEnvironmentSelection {
environment_id: "local".to_string(),
cwd: selected_cwd_uri,
workspace_roots: Vec::new(),
}],
)
.await;
@@ -562,6 +572,7 @@ url = "ws://127.0.0.1:8765"
let local = TurnEnvironmentSelection {
environment_id: LOCAL_ENVIRONMENT_ID.to_string(),
cwd: cwd_uri.clone(),
workspace_roots: Vec::new(),
};
let resolved = resolve_turn_environments(
@@ -570,6 +581,7 @@ url = "ws://127.0.0.1:8765"
TurnEnvironmentSelection {
environment_id: "missing".to_string(),
cwd: cwd_uri,
workspace_roots: Vec::new(),
},
local.clone(),
],
@@ -597,6 +609,7 @@ url = "ws://127.0.0.1:8765"
let selection = TurnEnvironmentSelection {
environment_id: REMOTE_ENVIRONMENT_ID.to_string(),
cwd: PathUri::from_abs_path(&AbsolutePathBuf::current_dir().expect("cwd")),
workspace_roots: Vec::new(),
};
let environments = Arc::new(ThreadEnvironments::new(
manager,
@@ -644,10 +657,12 @@ url = "ws://127.0.0.1:8765"
let remote = TurnEnvironmentSelection {
environment_id: REMOTE_ENVIRONMENT_ID.to_string(),
cwd: cwd.clone(),
workspace_roots: Vec::new(),
};
let local = TurnEnvironmentSelection {
environment_id: LOCAL_ENVIRONMENT_ID.to_string(),
cwd,
workspace_roots: Vec::new(),
};
let turn_environments = ThreadEnvironments::new(
manager,
@@ -706,6 +721,7 @@ url = "ws://127.0.0.1:8765"
let selection = TurnEnvironmentSelection {
environment_id: REMOTE_ENVIRONMENT_ID.to_string(),
cwd: PathUri::from_abs_path(&AbsolutePathBuf::current_dir().expect("cwd")),
workspace_roots: Vec::new(),
};
let environments = ThreadEnvironments::new(
Arc::clone(&manager),
@@ -757,6 +773,7 @@ url = "ws://127.0.0.1:8765"
let selection = TurnEnvironmentSelection {
environment_id: REMOTE_ENVIRONMENT_ID.to_string(),
cwd: PathUri::from_abs_path(&cwd),
workspace_roots: Vec::new(),
};
let environments = ThreadEnvironments::new(
Arc::clone(&manager),
@@ -813,6 +830,7 @@ url = "ws://127.0.0.1:8765"
let selection = TurnEnvironmentSelection {
environment_id: REMOTE_ENVIRONMENT_ID.to_string(),
cwd: PathUri::from_abs_path(&cwd),
workspace_roots: Vec::new(),
};
let inherited_environment = Arc::new(
Environment::create_for_tests(Some("ws://127.0.0.1:8765".to_string()))
@@ -865,6 +883,7 @@ url = "ws://127.0.0.1:8765"
&[TurnEnvironmentSelection {
environment_id: LOCAL_ENVIRONMENT_ID.to_string(),
cwd: cwd_uri.clone(),
workspace_roots: Vec::new(),
}],
)
.await;
-2
View File
@@ -122,7 +122,6 @@ async fn thread_settings_update(
) -> SessionSettingsUpdate {
let ThreadSettingsOverrides {
environments,
workspace_roots,
profile_workspace_roots,
approval_policy,
approvals_reviewer,
@@ -151,7 +150,6 @@ async fn thread_settings_update(
};
SessionSettingsUpdate {
environments,
workspace_roots,
profile_workspace_roots,
approval_policy,
approvals_reviewer,
-1
View File
@@ -658,7 +658,6 @@ impl Codex {
config.cwd.clone(),
environment_selections,
),
workspace_roots: config.workspace_roots.clone(),
codex_home: config.codex_home.clone(),
thread_name: None,
original_config_do_not_use: Arc::clone(&config),
+18 -24
View File
@@ -80,9 +80,6 @@ pub(crate) struct SessionConfiguration {
/// Sticky thread-level environment selections plus the legacy cwd used
/// when a turn does not select an environment.
pub(super) environments: TurnEnvironmentSelections,
/// Thread-scoped runtime workspace roots for materializing symbolic
/// workspace permissions at session runtime.
pub(super) workspace_roots: Vec<AbsolutePathBuf>,
/// Directory containing all Codex state for this session.
pub(super) codex_home: AbsolutePathBuf,
/// Optional user-facing name for the thread, updated during the session.
@@ -119,6 +116,21 @@ impl SessionConfiguration {
&self.environments.environments
}
pub(super) fn workspace_roots(&self) -> Vec<AbsolutePathBuf> {
self.environments
.environments
.first()
.and_then(|environment| {
environment
.workspace_roots
.iter()
.map(PathUri::to_abs_path)
.collect::<std::io::Result<Vec<_>>>()
.ok()
})
.unwrap_or_else(|| vec![self.cwd().clone()])
}
pub(crate) fn codex_home(&self) -> &AbsolutePathBuf {
&self.codex_home
}
@@ -131,7 +143,7 @@ impl SessionConfiguration {
self.permission_profile_state
.permission_profile()
.clone()
.materialize_project_roots_with_workspace_roots(&self.workspace_roots)
.materialize_project_roots_with_workspace_roots(&self.workspace_roots())
}
pub(super) fn active_permission_profile(&self) -> Option<ActivePermissionProfile> {
@@ -186,7 +198,7 @@ impl SessionConfiguration {
permission_profile: self.permission_profile(),
active_permission_profile: self.active_permission_profile(),
environments: self.environments.clone(),
workspace_roots: self.workspace_roots.clone(),
workspace_roots: self.workspace_roots(),
profile_workspace_roots: self.profile_workspace_roots().to_vec(),
ephemeral: self.original_config_do_not_use.ephemeral,
reasoning_effort: self.collaboration_mode.reasoning_effort(),
@@ -265,25 +277,8 @@ impl SessionConfiguration {
.environments
.clone()
.unwrap_or_else(|| self.environments.clone());
let cwd_changed = next_environments.legacy_fallback_cwd.as_path() != current_cwd.as_path();
let cwd_changed = next_environments.legacy_fallback_cwd != current_cwd;
next_configuration.environments = next_environments;
if let Some(workspace_roots) = updates.workspace_roots.clone() {
next_configuration.workspace_roots = workspace_roots;
} else if cwd_changed && self.workspace_roots.contains(&current_cwd) {
let mut retargeted_workspace_roots =
Vec::with_capacity(next_configuration.workspace_roots.len());
for root in &self.workspace_roots {
let root = if root == &current_cwd {
next_configuration.cwd().clone()
} else {
root.clone()
};
if !retargeted_workspace_roots.contains(&root) {
retargeted_workspace_roots.push(root);
}
}
next_configuration.workspace_roots = retargeted_workspace_roots;
}
if let Some(permission_profile) = updates.permission_profile.clone() {
let active_permission_profile =
@@ -416,7 +411,6 @@ impl SessionConfiguration {
#[derive(Default, Clone)]
pub(crate) struct SessionSettingsUpdate {
pub(crate) environments: Option<TurnEnvironmentSelections>,
pub(crate) workspace_roots: Option<Vec<AbsolutePathBuf>>,
pub(crate) profile_workspace_roots: Option<Vec<AbsolutePathBuf>>,
pub(crate) approval_policy: Option<AskForApproval>,
pub(crate) approvals_reviewer: Option<ApprovalsReviewer>,
+14 -152
View File
@@ -49,9 +49,7 @@ use codex_protocol::config_types::SERVICE_TIER_DEFAULT_REQUEST_VALUE;
use codex_protocol::config_types::ServiceTier;
use codex_protocol::config_types::TrustLevel;
use codex_protocol::exec_output::ExecToolCallOutput;
use codex_protocol::models::ActivePermissionProfile;
use codex_protocol::models::AgentMessageInputContent;
use codex_protocol::models::BUILT_IN_PERMISSION_PROFILE_WORKSPACE;
use codex_protocol::models::FileSystemPermissions;
use codex_protocol::models::FunctionCallOutputBody;
use codex_protocol::models::FunctionCallOutputContentItem;
@@ -181,6 +179,7 @@ use tokio::sync::Semaphore;
use tokio::time::sleep;
use tokio::time::timeout;
use tracing_opentelemetry::OpenTelemetrySpanExt;
use uuid::Uuid;
use codex_protocol::mcp::CallToolResult as McpCallToolResult;
@@ -2919,52 +2918,6 @@ async fn session_configured_reports_permission_profile_for_external_sandbox() ->
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn session_permission_profile_rebinds_runtime_workspace_roots() -> anyhow::Result<()> {
let codex_home = tempfile::TempDir::new()?;
let cwd = tempfile::TempDir::new()?;
let old_root = test_path_buf("/workspace/old").abs();
let new_root = test_path_buf("/workspace/new").abs();
let config = ConfigBuilder::default()
.codex_home(codex_home.path().to_path_buf())
.harness_overrides(crate::config::ConfigOverrides {
cwd: Some(cwd.path().to_path_buf()),
default_permissions: Some(BUILT_IN_PERMISSION_PROFILE_WORKSPACE.to_string()),
additional_writable_roots: vec![old_root.to_path_buf()],
..Default::default()
})
.build()
.await?;
let session_permission_profile_state = session_permission_profile_state_from_config(&config)?;
let stored_file_system_policy = session_permission_profile_state
.permission_profile()
.file_system_sandbox_policy();
assert!(
!stored_file_system_policy
.can_write_path_with_cwd(old_root.as_path(), config.cwd.as_path()),
"session permission profile state should keep runtime workspace roots symbolic"
);
let mut session_configuration = make_session_configuration_for_tests().await;
session_configuration.environments =
TurnEnvironmentSelections::new(config.cwd.clone(), Vec::new());
session_configuration.workspace_roots = config.workspace_roots.clone();
session_configuration.permission_profile_state = session_permission_profile_state;
let initial_policy = session_configuration.file_system_sandbox_policy();
assert!(initial_policy.can_write_path_with_cwd(old_root.as_path(), config.cwd.as_path()));
let updated = session_configuration.apply(&SessionSettingsUpdate {
workspace_roots: Some(vec![new_root.clone()]),
..Default::default()
})?;
let updated_policy = updated.file_system_sandbox_policy();
assert!(updated_policy.can_write_path_with_cwd(new_root.as_path(), updated.cwd().as_path()));
assert!(!updated_policy.can_write_path_with_cwd(old_root.as_path(), updated.cwd().as_path()));
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn fork_startup_context_then_first_turn_diff_snapshot() -> anyhow::Result<()> {
let server = start_mock_server().await;
@@ -3746,7 +3699,6 @@ async fn set_rate_limits_retains_previous_credits() {
permission_profile_state: config.permissions.permission_profile_state().clone(),
windows_sandbox_level: WindowsSandboxLevel::from_config(&config),
environments: TurnEnvironmentSelections::new(config.cwd.clone(), Vec::new()),
workspace_roots: config.workspace_roots.clone(),
codex_home: config.codex_home.clone(),
thread_name: None,
original_config_do_not_use: Arc::clone(&config),
@@ -3853,7 +3805,6 @@ async fn set_rate_limits_updates_plan_type_when_present() {
permission_profile_state: config.permissions.permission_profile_state().clone(),
windows_sandbox_level: WindowsSandboxLevel::from_config(&config),
environments: TurnEnvironmentSelections::new(config.cwd.clone(), Vec::new()),
workspace_roots: config.workspace_roots.clone(),
codex_home: config.codex_home.clone(),
thread_name: None,
original_config_do_not_use: Arc::clone(&config),
@@ -4390,7 +4341,6 @@ pub(crate) async fn make_session_configuration_for_tests() -> SessionConfigurati
permission_profile_state: config.permissions.permission_profile_state().clone(),
windows_sandbox_level: WindowsSandboxLevel::from_config(&config),
environments: TurnEnvironmentSelections::new(config.cwd.clone(), Vec::new()),
workspace_roots: config.workspace_roots.clone(),
codex_home: config.codex_home.clone(),
thread_name: None,
original_config_do_not_use: Arc::clone(&config),
@@ -4548,7 +4498,7 @@ async fn session_configuration_apply_preserves_profile_file_system_policy_on_cwd
)
.expect("set permission profile");
let expected_file_system_sandbox_policy = file_system_sandbox_policy
.materialize_project_roots_with_workspace_roots(&session_configuration.workspace_roots);
.materialize_project_roots_with_workspace_roots(&session_configuration.workspace_roots());
let updated = session_configuration
.apply(&SessionSettingsUpdate {
@@ -4610,7 +4560,7 @@ async fn session_configuration_apply_permission_profile_preserves_existing_deny_
.expect("permission profile update should succeed");
let mut expected_file_system_policy = requested_file_system_policy
.materialize_project_roots_with_workspace_roots(&session_configuration.workspace_roots);
.materialize_project_roots_with_workspace_roots(&session_configuration.workspace_roots());
expected_file_system_policy.glob_scan_max_depth = Some(2);
expected_file_system_policy.entries.push(deny_entry);
assert_eq!(
@@ -4666,94 +4616,6 @@ async fn session_configuration_apply_permission_profile_accepts_direct_write_roo
);
}
#[tokio::test]
async fn session_configuration_apply_rebinds_symbolic_profile_to_updated_workspace_roots() {
let mut session_configuration = make_session_configuration_for_tests().await;
let old_root = tempfile::tempdir().expect("create old root");
let new_root = tempfile::tempdir().expect("create new root");
let profile_root = tempfile::tempdir().expect("create profile root");
let old_root = old_root.path().abs();
let new_root = new_root.path().abs();
let profile_root = profile_root.path().abs();
session_configuration.workspace_roots = vec![old_root.clone()];
let file_system_sandbox_policy =
FileSystemSandboxPolicy::restricted(vec![FileSystemSandboxEntry {
path: FileSystemPath::Special {
value: FileSystemSpecialPath::project_roots(/*subpath*/ None),
},
access: FileSystemAccessMode::Write,
}]);
let permission_profile = PermissionProfile::from_runtime_permissions(
&file_system_sandbox_policy,
NetworkSandboxPolicy::Restricted,
);
let updated = session_configuration
.apply(&SessionSettingsUpdate {
workspace_roots: Some(vec![new_root.clone()]),
permission_profile: Some(permission_profile),
active_permission_profile: Some(ActivePermissionProfile::new("dev")),
profile_workspace_roots: Some(vec![profile_root.clone()]),
..Default::default()
})
.expect("permission profile update should succeed");
let updated_policy = updated.file_system_sandbox_policy();
assert!(updated_policy.can_write_path_with_cwd(new_root.as_path(), updated.cwd().as_path()));
assert!(!updated_policy.can_write_path_with_cwd(old_root.as_path(), updated.cwd().as_path()));
assert_eq!(
updated.active_permission_profile(),
Some(ActivePermissionProfile::new("dev"))
);
assert_eq!(updated.profile_workspace_roots(), &[profile_root]);
}
#[tokio::test]
async fn session_configuration_apply_retargets_implicit_workspace_root_on_cwd_update() {
let mut session_configuration = make_session_configuration_for_tests().await;
let old_root = tempfile::tempdir().expect("create old root");
let new_root = tempfile::tempdir().expect("create new root");
let extra_root = tempfile::tempdir().expect("create extra root");
let old_root = old_root.path().abs();
let new_root = new_root.path().abs();
let extra_root = extra_root.path().abs();
session_configuration.environments =
TurnEnvironmentSelections::new(old_root.clone(), Vec::new());
session_configuration.workspace_roots = vec![old_root.clone(), extra_root.clone()];
let file_system_sandbox_policy =
FileSystemSandboxPolicy::restricted(vec![FileSystemSandboxEntry {
path: FileSystemPath::Special {
value: FileSystemSpecialPath::project_roots(/*subpath*/ None),
},
access: FileSystemAccessMode::Write,
}]);
let permission_profile = PermissionProfile::from_runtime_permissions(
&file_system_sandbox_policy,
NetworkSandboxPolicy::Restricted,
);
session_configuration
.set_permission_profile_for_tests(permission_profile)
.expect("set permission profile");
let updated = session_configuration
.apply(&SessionSettingsUpdate {
environments: Some(TurnEnvironmentSelections::new(new_root.clone(), Vec::new())),
..Default::default()
})
.expect("cwd-only update should succeed");
assert_eq!(
updated.workspace_roots,
vec![new_root.clone(), extra_root.clone()]
);
let updated_policy = updated.file_system_sandbox_policy();
assert!(updated_policy.can_write_path_with_cwd(new_root.as_path(), updated.cwd().as_path()));
assert!(updated_policy.can_write_path_with_cwd(extra_root.as_path(), updated.cwd().as_path()));
assert!(!updated_policy.can_write_path_with_cwd(old_root.as_path(), updated.cwd().as_path()));
}
#[tokio::test]
async fn active_profile_update_rebuilds_network_proxy_config() -> std::io::Result<()> {
let codex_home = tempfile::tempdir().expect("create codex home");
@@ -4963,7 +4825,6 @@ async fn session_configuration_apply_retargets_legacy_workspace_root_on_cwd_upda
let project_root = workspace.path().join("repo-b").abs();
session_configuration.environments =
TurnEnvironmentSelections::new(original_cwd.clone(), Vec::new());
session_configuration.workspace_roots = vec![session_configuration.cwd().clone()];
let sandbox_policy = SandboxPolicy::WorkspaceWrite {
writable_roots: Vec::new(),
network_access: false,
@@ -4994,7 +4855,7 @@ async fn session_configuration_apply_retargets_legacy_workspace_root_on_cwd_upda
})
.expect("cwd-only update should succeed");
assert_eq!(updated.workspace_roots, vec![project_root.clone()]);
assert_eq!(updated.workspace_roots(), vec![project_root.clone()]);
assert!(
updated
.file_system_sandbox_policy()
@@ -5261,7 +5122,6 @@ async fn session_new_fails_when_zsh_fork_enabled_without_packaged_zsh() {
permission_profile_state: config.permissions.permission_profile_state().clone(),
windows_sandbox_level: WindowsSandboxLevel::from_config(&config),
environments: TurnEnvironmentSelections::new(config.cwd.clone(), Vec::new()),
workspace_roots: config.workspace_roots.clone(),
codex_home: config.codex_home.clone(),
thread_name: None,
original_config_do_not_use: Arc::clone(&config),
@@ -5393,7 +5253,6 @@ pub(crate) async fn make_session_and_context() -> (Session, TurnContext) {
permission_profile_state: config.permissions.permission_profile_state().clone(),
windows_sandbox_level: WindowsSandboxLevel::from_config(&config),
environments: TurnEnvironmentSelections::new(config.cwd.clone(), default_environments),
workspace_roots: config.workspace_roots.clone(),
codex_home: config.codex_home.clone(),
thread_name: None,
original_config_do_not_use: Arc::clone(&config),
@@ -5409,8 +5268,11 @@ pub(crate) async fn make_session_and_context() -> (Session, TurnContext) {
dynamic_tools: Vec::new(),
user_shell_override: None,
};
let per_turn_config =
Session::build_per_turn_config(&session_configuration, session_configuration.cwd().clone());
let per_turn_config = Session::build_per_turn_config(
&session_configuration,
session_configuration.cwd().clone(),
session_configuration.workspace_roots(),
);
let model_info = construct_model_info_offline_for_tests(
session_configuration.collaboration_mode.model(),
&per_turn_config.to_models_manager_config(),
@@ -5647,7 +5509,6 @@ async fn make_session_with_config_and_rx(
permission_profile_state: config.permissions.permission_profile_state().clone(),
windows_sandbox_level: WindowsSandboxLevel::from_config(&config),
environments: TurnEnvironmentSelections::new(config.cwd.clone(), default_environments),
workspace_roots: config.workspace_roots.clone(),
codex_home: config.codex_home.clone(),
thread_name: None,
original_config_do_not_use: Arc::clone(&config),
@@ -5755,7 +5616,6 @@ async fn make_session_with_history_source_and_agent_control_and_rx(
permission_profile_state: config.permissions.permission_profile_state().clone(),
windows_sandbox_level: WindowsSandboxLevel::from_config(&config),
environments: TurnEnvironmentSelections::new(config.cwd.clone(), default_environments),
workspace_roots: config.workspace_roots.clone(),
codex_home: config.codex_home.clone(),
thread_name: None,
original_config_do_not_use: Arc::clone(&config),
@@ -7525,7 +7385,6 @@ where
permission_profile_state: config.permissions.permission_profile_state().clone(),
windows_sandbox_level: WindowsSandboxLevel::from_config(&config),
environments: TurnEnvironmentSelections::new(config.cwd.clone(), default_environments),
workspace_roots: config.workspace_roots.clone(),
codex_home: config.codex_home.clone(),
thread_name: None,
original_config_do_not_use: Arc::clone(&config),
@@ -7541,8 +7400,11 @@ where
dynamic_tools,
user_shell_override: None,
};
let per_turn_config =
Session::build_per_turn_config(&session_configuration, session_configuration.cwd().clone());
let per_turn_config = Session::build_per_turn_config(
&session_configuration,
session_configuration.cwd().clone(),
session_configuration.workspace_roots(),
);
let model_info = construct_model_info_offline_for_tests(
session_configuration.collaboration_mode.model(),
&per_turn_config.to_models_manager_config(),
+48 -21
View File
@@ -45,6 +45,7 @@ pub(crate) struct TurnEnvironment {
pub(crate) environment_id: String,
pub(crate) environment: Arc<Environment>,
cwd: PathUri,
workspace_roots: Vec<PathUri>,
pub(crate) shell: Option<shell::Shell>,
pub(crate) shell_snapshot: ShellSnapshotTask,
}
@@ -60,6 +61,7 @@ impl TurnEnvironment {
environment_id,
environment,
cwd,
workspace_roots: Vec::new(),
shell,
shell_snapshot: futures::future::ready(None).boxed().shared(),
}
@@ -79,10 +81,20 @@ impl TurnEnvironment {
&self.cwd
}
pub(crate) fn workspace_roots(&self) -> &[PathUri] {
&self.workspace_roots
}
pub(crate) fn with_workspace_roots(mut self, workspace_roots: Vec<PathUri>) -> Self {
self.workspace_roots = workspace_roots;
self
}
pub(crate) fn selection(&self) -> TurnEnvironmentSelection {
TurnEnvironmentSelection {
environment_id: self.environment_id.clone(),
cwd: self.cwd.clone(),
workspace_roots: self.workspace_roots.clone(),
}
}
}
@@ -93,6 +105,7 @@ impl std::fmt::Debug for TurnEnvironment {
.field("environment_id", &self.environment_id)
.field("environment", &self.environment)
.field("cwd", &self.cwd)
.field("workspace_roots", &self.workspace_roots)
.field("shell", &self.shell)
.finish_non_exhaustive()
}
@@ -310,10 +323,11 @@ impl TurnContext {
pub(crate) fn file_system_sandbox_context(
&self,
additional_permissions: Option<AdditionalPermissionProfile>,
cwd: &PathUri,
environment: &TurnEnvironment,
) -> FileSystemSandboxContext {
let permission_profile = self.config.permissions.permission_profile();
let (base_file_system_sandbox_policy, base_network_sandbox_policy) =
self.permission_profile.to_runtime_permissions();
permission_profile.to_runtime_permissions();
let file_system_sandbox_policy = effective_file_system_sandbox_policy(
&base_file_system_sandbox_policy,
additional_permissions.as_ref(),
@@ -323,19 +337,14 @@ impl TurnContext {
additional_permissions.as_ref(),
);
let permissions = PermissionProfile::from_runtime_permissions_with_enforcement(
self.permission_profile.enforcement(),
permission_profile.enforcement(),
&file_system_sandbox_policy,
network_sandbox_policy,
);
FileSystemSandboxContext {
permissions: permissions.into(),
cwd: Some(cwd.clone()),
workspace_roots: self
.config
.effective_workspace_roots()
.iter()
.map(PathUri::from_abs_path)
.collect(),
cwd: Some(environment.cwd().clone()),
workspace_roots: environment.workspace_roots().to_vec(),
windows_sandbox_level: self.windows_sandbox_level,
windows_sandbox_private_desktop: self
.config
@@ -426,15 +435,16 @@ impl Session {
pub(crate) fn build_per_turn_config(
session_configuration: &SessionConfiguration,
cwd: AbsolutePathBuf,
workspace_roots: Vec<AbsolutePathBuf>,
) -> Config {
// todo(aibrahim): store this state somewhere else so we don't need to mut config
let config = session_configuration.original_config_do_not_use.clone();
let mut per_turn_config = (*config).clone();
per_turn_config.cwd = cwd;
per_turn_config.workspace_roots = session_configuration.workspace_roots.clone();
per_turn_config.workspace_roots = workspace_roots.clone();
per_turn_config
.permissions
.set_workspace_roots(session_configuration.workspace_roots.clone());
.set_workspace_roots(workspace_roots);
per_turn_config.model_reasoning_effort =
session_configuration.collaboration_mode.reasoning_effort();
per_turn_config.model_reasoning_summary = session_configuration.model_reasoning_summary;
@@ -465,14 +475,16 @@ impl Session {
pub(crate) fn build_effective_session_config(
session_configuration: &SessionConfiguration,
) -> Config {
let mut config =
Self::build_per_turn_config(session_configuration, session_configuration.cwd().clone());
let workspace_roots = session_configuration.workspace_roots();
let mut config = Self::build_per_turn_config(
session_configuration,
session_configuration.cwd().clone(),
workspace_roots.clone(),
);
config.model = Some(session_configuration.collaboration_mode.model().to_string());
config.permissions.approval_policy = session_configuration.approval_policy.clone();
config.workspace_roots = session_configuration.workspace_roots.clone();
config
.permissions
.set_workspace_roots(session_configuration.workspace_roots.clone());
config.workspace_roots = workspace_roots.clone();
config.permissions.set_workspace_roots(workspace_roots);
config
}
@@ -523,6 +535,7 @@ impl Session {
per_turn_config.features.enabled(Feature::FastMode),
&model_info,
);
let permission_profile = per_turn_config.permissions.effective_permission_profile();
let per_turn_config = Arc::new(per_turn_config);
let turn_metadata_state = Arc::new(TurnMetadataState::new(
session_id.to_string(),
@@ -533,7 +546,7 @@ impl Session {
session_configuration.thread_source.clone(),
sub_id.clone(),
cwd.clone(),
&session_configuration.permission_profile(),
&permission_profile,
session_configuration.windows_sandbox_level,
network.is_some(),
));
@@ -566,7 +579,7 @@ impl Session {
multi_agent_version,
personality: session_configuration.personality,
approval_policy: session_configuration.approval_policy.clone(),
permission_profile: session_configuration.permission_profile(),
permission_profile,
network,
windows_sandbox_level: session_configuration.windows_sandbox_level,
available_models,
@@ -697,7 +710,21 @@ impl Session {
.as_ref()
.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());
let workspace_roots = primary_turn_environment
.as_ref()
.map(TurnEnvironment::workspace_roots)
.map(|workspace_roots| {
workspace_roots
.iter()
.map(PathUri::to_abs_path)
.collect::<std::io::Result<Vec<_>>>()
})
.transpose()
.ok()
.flatten()
.unwrap_or_else(|| session_configuration.workspace_roots());
let per_turn_config =
Self::build_per_turn_config(&session_configuration, cwd.clone(), workspace_roots);
{
let mcp_runtime = self.services.latest_mcp_runtime();
let mcp_connection_manager = mcp_runtime.manager();
+26 -5
View File
@@ -509,8 +509,13 @@ impl ThreadManager {
pub fn default_environment_selections(
&self,
cwd: &AbsolutePathBuf,
workspace_roots: &[AbsolutePathBuf],
) -> Vec<TurnEnvironmentSelection> {
default_thread_environment_selections(self.state.environment_manager.as_ref(), cwd)
default_thread_environment_selections(
self.state.environment_manager.as_ref(),
cwd,
workspace_roots,
)
}
pub fn validate_environment_selections(
@@ -658,6 +663,7 @@ impl ThreadManager {
let environments = default_thread_environment_selections(
self.state.environment_manager.as_ref(),
&config.cwd,
&config.workspace_roots,
);
Box::pin(self.start_thread_with_options(StartThreadOptions {
config,
@@ -789,6 +795,7 @@ impl ThreadManager {
let environments = default_thread_environment_selections(
self.state.environment_manager.as_ref(),
&config.cwd,
&config.workspace_roots,
);
let (session_source, thread_source) = initial_history
.get_resumed_session_sources()
@@ -827,6 +834,7 @@ impl ThreadManager {
let environments = default_thread_environment_selections(
self.state.environment_manager.as_ref(),
&config.cwd,
&config.workspace_roots,
);
Box::pin(self.state.spawn_thread(
config,
@@ -860,6 +868,7 @@ impl ThreadManager {
let environments = default_thread_environment_selections(
self.state.environment_manager.as_ref(),
&config.cwd,
&config.workspace_roots,
);
let (session_source, thread_source) = initial_history
.get_resumed_session_sources()
@@ -1048,6 +1057,7 @@ impl ThreadManager {
let environments = default_thread_environment_selections(
self.state.environment_manager.as_ref(),
&config.cwd,
&config.workspace_roots,
);
let agent_control = self.agent_control_for_config(&config);
Box::pin(self.state.spawn_thread(
@@ -1364,7 +1374,11 @@ impl ThreadManagerState {
environments: Option<Vec<TurnEnvironmentSelection>>,
) -> CodexResult<NewThread> {
let environments = environments.unwrap_or_else(|| {
default_thread_environment_selections(self.environment_manager.as_ref(), &config.cwd)
default_thread_environment_selections(
self.environment_manager.as_ref(),
&config.cwd,
&config.workspace_roots,
)
});
Box::pin(self.spawn_thread_with_source(
config,
@@ -1403,8 +1417,11 @@ impl ThreadManagerState {
inherited_environments,
inherited_exec_policy,
} = options;
let environments =
default_thread_environment_selections(self.environment_manager.as_ref(), &config.cwd);
let environments = default_thread_environment_selections(
self.environment_manager.as_ref(),
&config.cwd,
&config.workspace_roots,
);
let thread_source = initial_history.get_resumed_thread_source();
Box::pin(self.spawn_thread_with_source(
config,
@@ -1446,7 +1463,11 @@ impl ThreadManagerState {
thread_extension_init: ExtensionDataInit,
) -> CodexResult<NewThread> {
let environments = environments.unwrap_or_else(|| {
default_thread_environment_selections(self.environment_manager.as_ref(), &config.cwd)
default_thread_environment_selections(
self.environment_manager.as_ref(),
&config.cwd,
&config.workspace_roots,
)
});
Box::pin(self.spawn_thread_with_source(
config,
@@ -829,6 +829,7 @@ async fn resume_and_fork_do_not_restore_thread_environments_from_rollout() {
let environments = vec![TurnEnvironmentSelection {
environment_id: "local".to_string(),
cwd: PathUri::from_abs_path(&selected_cwd),
workspace_roots: Vec::new(),
}];
let default_cwd = config.cwd.clone();
let mut source_config = config.clone();
@@ -383,10 +383,8 @@ impl ApplyPatchHandler {
));
};
let fs = turn_environment.environment.get_filesystem();
let sandbox = turn.file_system_sandbox_context(
/*additional_permissions*/ None,
turn_environment.cwd(),
);
let sandbox = turn
.file_system_sandbox_context(/*additional_permissions*/ None, turn_environment);
match codex_apply_patch::verify_apply_patch_args(
args,
turn_environment.cwd(),
@@ -554,7 +552,8 @@ pub(crate) async fn intercept_apply_patch(
call_id: &str,
tool_name: &str,
) -> Result<Option<FunctionToolOutput>, FunctionCallError> {
let sandbox = turn.file_system_sandbox_context(/*additional_permissions*/ None, cwd);
let sandbox =
turn.file_system_sandbox_context(/*additional_permissions*/ None, &turn_environment);
match codex_apply_patch::maybe_parse_apply_patch_verified(command, cwd, fs, Some(&sandbox))
.await
{
@@ -133,7 +133,7 @@ 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());
.file_system_sandbox_context(additional_permissions, environment);
environments.push(ToolEnvironment {
environment_id: environment.environment_id.clone(),
cwd: native_cwd,
@@ -147,10 +147,8 @@ impl ViewImageHandler {
))
})?;
let model_visible_path = path_uri.inferred_native_path_string();
let sandbox = turn.file_system_sandbox_context(
/*additional_permissions*/ None,
turn_environment.cwd(),
);
let sandbox = turn
.file_system_sandbox_context(/*additional_permissions*/ None, turn_environment);
let fs = turn_environment.environment.get_filesystem();
let metadata = fs
+23 -5
View File
@@ -13,6 +13,7 @@ use crate::guardian::review_approval_request;
use crate::guardian::routes_approval_to_guardian;
use crate::hook_runtime::run_permission_request_hooks;
use crate::network_policy_decision::network_approval_context_from_payload;
use crate::session::turn_context::TurnEnvironment;
use crate::tools::flat_tool_name;
use crate::tools::network_approval::ActiveNetworkApproval;
use crate::tools::network_approval::DeferredNetworkApproval;
@@ -265,16 +266,33 @@ impl ToolOrchestrator {
.sandbox_cwd(req)
.cloned()
.unwrap_or_else(|| PathUri::from_abs_path(&turn_ctx.cwd));
let workspace_roots = turn_ctx.config.effective_workspace_roots();
let workspace_roots = tool
.turn_environment(req)
.or_else(|| turn_ctx.environments.primary())
.map(TurnEnvironment::workspace_roots)
.unwrap_or_default();
let permissions = workspace_roots
.iter()
.map(PathUri::to_abs_path)
.collect::<std::io::Result<Vec<_>>>()
.map(|workspace_roots| {
turn_ctx
.config
.permissions
.permission_profile()
.clone()
.materialize_project_roots_with_workspace_roots(&workspace_roots)
})
.unwrap_or_else(|_| turn_ctx.permission_profile.clone());
let initial_attempt = SandboxAttempt {
sandbox: initial_sandbox,
sandbox_requested,
permissions: &turn_ctx.permission_profile,
permissions: &permissions,
exec_server_permissions: turn_ctx.config.permissions.permission_profile(),
enforce_managed_network: managed_network_active,
manager: &self.sandbox,
sandbox_cwd: &sandbox_policy_cwd,
workspace_roots: workspace_roots.as_slice(),
workspace_roots,
codex_linux_sandbox_exe: turn_ctx.config.codex_linux_sandbox_exe.as_ref(),
use_legacy_landlock,
windows_sandbox_level: turn_ctx.windows_sandbox_level,
@@ -452,12 +470,12 @@ impl ToolOrchestrator {
let retry_attempt = SandboxAttempt {
sandbox: retry_sandbox,
sandbox_requested: retry_sandbox_requested,
permissions: &turn_ctx.permission_profile,
permissions: &permissions,
exec_server_permissions: turn_ctx.config.permissions.permission_profile(),
enforce_managed_network: managed_network_active,
manager: &self.sandbox,
sandbox_cwd: &sandbox_policy_cwd,
workspace_roots: workspace_roots.as_slice(),
workspace_roots,
codex_linux_sandbox_exe: retry_codex_linux_sandbox_exe,
use_legacy_landlock,
windows_sandbox_level: turn_ctx.windows_sandbox_level,
@@ -105,11 +105,7 @@ impl ApplyPatchRuntime {
Some(FileSystemSandboxContext {
permissions: permissions.into(),
cwd: Some(attempt.sandbox_cwd.clone()),
workspace_roots: attempt
.workspace_roots
.iter()
.map(PathUri::from_abs_path)
.collect(),
workspace_roots: attempt.workspace_roots.to_vec(),
windows_sandbox_level: attempt.windows_sandbox_level,
windows_sandbox_private_desktop: attempt.windows_sandbox_private_desktop,
use_legacy_landlock: attempt.use_legacy_landlock,
@@ -223,6 +219,10 @@ impl Approvable<ApplyPatchRequest> for ApplyPatchRuntime {
}
impl ToolRuntime<ApplyPatchRequest, ApplyPatchRuntimeOutput> for ApplyPatchRuntime {
fn turn_environment<'a>(&self, req: &'a ApplyPatchRequest) -> Option<&'a TurnEnvironment> {
Some(&req.turn_environment)
}
fn sandbox_cwd<'a>(&self, req: &'a ApplyPatchRequest) -> Option<&'a PathUri> {
Some(&req.action.cwd)
}
@@ -226,7 +226,7 @@ async fn file_system_sandbox_context_uses_active_attempt() {
enforce_managed_network: false,
manager: &manager,
sandbox_cwd: &sandbox_policy_cwd,
workspace_roots: std::slice::from_ref(&path),
workspace_roots: std::slice::from_ref(&sandbox_policy_cwd),
codex_linux_sandbox_exe: None,
use_legacy_landlock: true,
windows_sandbox_level: WindowsSandboxLevel::RestrictedToken,
@@ -295,7 +295,7 @@ async fn no_sandbox_attempt_has_no_file_system_context() {
enforce_managed_network: false,
manager: &manager,
sandbox_cwd: &sandbox_policy_cwd,
workspace_roots: std::slice::from_ref(&path),
workspace_roots: std::slice::from_ref(&sandbox_policy_cwd),
codex_linux_sandbox_exe: None,
use_legacy_landlock: false,
windows_sandbox_level: WindowsSandboxLevel::Disabled,
@@ -112,7 +112,7 @@ async fn explicit_escalation_prepares_exec_without_managed_network() -> anyhow::
enforce_managed_network: false,
manager: &manager,
sandbox_cwd: &sandbox_policy_cwd,
workspace_roots: std::slice::from_ref(&native_sandbox_policy_cwd),
workspace_roots: std::slice::from_ref(&sandbox_policy_cwd),
codex_linux_sandbox_exe: None,
use_legacy_landlock: false,
windows_sandbox_level: WindowsSandboxLevel::Disabled,
@@ -208,6 +208,10 @@ impl Approvable<ShellRequest> for ShellRuntime {
}
impl ToolRuntime<ShellRequest, ExecToolCallOutput> for ShellRuntime {
fn turn_environment<'a>(&self, req: &'a ShellRequest) -> Option<&'a TurnEnvironment> {
Some(&req.turn_environment)
}
fn network_approval_spec(
&self,
req: &ShellRequest,
@@ -255,6 +255,10 @@ impl Approvable<UnifiedExecRequest> for UnifiedExecRuntime<'_> {
}
impl<'a> ToolRuntime<UnifiedExecRequest, UnifiedExecProcess> for UnifiedExecRuntime<'a> {
fn turn_environment<'b>(&self, req: &'b UnifiedExecRequest) -> Option<&'b TurnEnvironment> {
Some(&req.turn_environment)
}
fn sandbox_cwd<'b>(&self, req: &'b UnifiedExecRequest) -> Option<&'b PathUri> {
Some(&req.sandbox_cwd)
}
+15 -13
View File
@@ -8,6 +8,7 @@ use crate::sandboxing::ExecOptions;
use crate::sandboxing::SandboxPermissions;
use crate::session::session::Session;
use crate::session::turn_context::TurnContext;
use crate::session::turn_context::TurnEnvironment;
use crate::state::SessionServices;
use crate::tools::hook_names::HookToolName;
use crate::tools::network_approval::NetworkApprovalSpec;
@@ -27,7 +28,6 @@ use codex_sandboxing::SandboxType;
use codex_sandboxing::SandboxablePreference;
use codex_sandboxing::policy_transforms::effective_permission_profile;
use codex_tools::ToolName;
use codex_utils_absolute_path::AbsolutePathBuf;
use codex_utils_path_uri::PathUri;
use futures::Future;
use futures::future::BoxFuture;
@@ -395,6 +395,10 @@ pub(crate) enum ToolError {
}
pub(crate) trait ToolRuntime<Req, Out>: Approvable<Req> + Sandboxable {
fn turn_environment<'a>(&self, _req: &'a Req) -> Option<&'a TurnEnvironment> {
None
}
fn network_approval_spec(&self, _req: &Req, _ctx: &ToolCtx) -> Option<NetworkApprovalSpec> {
None
}
@@ -421,7 +425,7 @@ pub(crate) struct SandboxAttempt<'a> {
pub enforce_managed_network: bool,
pub(crate) manager: &'a SandboxManager,
pub(crate) sandbox_cwd: &'a PathUri,
pub(crate) workspace_roots: &'a [AbsolutePathBuf],
pub(crate) workspace_roots: &'a [PathUri],
pub codex_linux_sandbox_exe: Option<&'a std::path::PathBuf>,
pub use_legacy_landlock: bool,
pub windows_sandbox_level: codex_protocol::config_types::WindowsSandboxLevel,
@@ -464,10 +468,15 @@ impl<'a> SandboxAttempt<'a> {
windows_sandbox_private_desktop: self.windows_sandbox_private_desktop,
})
.map_err(CodexErr::from)?;
let workspace_roots = self
.workspace_roots
.iter()
.map(PathUri::to_abs_path)
.collect::<std::io::Result<Vec<_>>>()?;
Ok(crate::sandboxing::ExecRequest::from_sandbox_exec_request(
request,
options,
self.workspace_roots.to_vec(),
workspace_roots,
))
}
@@ -501,21 +510,14 @@ impl<'a> SandboxAttempt<'a> {
windows_sandbox_private_desktop: self.windows_sandbox_private_desktop,
})
.map_err(CodexErr::from)?;
let mut exec_request = crate::sandboxing::ExecRequest::from_sandbox_exec_request(
request,
options,
self.workspace_roots.to_vec(),
);
let mut exec_request =
crate::sandboxing::ExecRequest::from_sandbox_exec_request(request, options, Vec::new());
exec_request.exec_server_managed_network = managed_network;
if self.sandbox_requested {
exec_request.exec_server_sandbox = Some(FileSystemSandboxContext {
permissions: exec_server_permissions.into(),
cwd: Some(exec_request.windows_sandbox_policy_cwd.clone()),
workspace_roots: self
.workspace_roots
.iter()
.map(PathUri::from_abs_path)
.collect(),
workspace_roots: self.workspace_roots.to_vec(),
windows_sandbox_level: self.windows_sandbox_level,
windows_sandbox_private_desktop: self.windows_sandbox_private_desktop,
use_legacy_landlock: self.use_legacy_landlock,
+1 -1
View File
@@ -221,7 +221,7 @@ fn exec_server_env_keeps_command_native_and_carries_sandbox_context() {
enforce_managed_network: true,
manager: &manager,
sandbox_cwd: &cwd_uri,
workspace_roots: std::slice::from_ref(&cwd),
workspace_roots: std::slice::from_ref(&cwd_uri),
codex_linux_sandbox_exe: None,
use_legacy_landlock: false,
windows_sandbox_level: codex_protocol::config_types::WindowsSandboxLevel::Disabled,
+4 -1
View File
@@ -109,6 +109,7 @@ pub fn local(cwd: AbsolutePathBuf) -> TurnEnvironmentSelection {
TurnEnvironmentSelection {
environment_id: codex_exec_server::LOCAL_ENVIRONMENT_ID.to_string(),
cwd: PathUri::from_abs_path(&cwd),
workspace_roots: vec![PathUri::from_abs_path(&cwd)],
}
}
@@ -203,6 +204,7 @@ pub async fn test_env() -> Result<TestEnv> {
let selection = TurnEnvironmentSelection {
environment_id: codex_exec_server::REMOTE_ENVIRONMENT_ID.to_string(),
cwd: cwd_uri.clone(),
workspace_roots: vec![cwd_uri.clone()],
};
let cwd = if remote_env == TestEnvironment::WineExec {
// TODO(anp): Convert `Config::cwd` to `LegacyAppPathString` and remove this
@@ -686,7 +688,8 @@ impl TestCodexBuilder {
.await?
}
(None, None) => {
let environments = thread_manager.default_environment_selections(&config.cwd);
let environments = thread_manager
.default_environment_selections(&config.cwd, &config.workspace_roots);
Box::pin(
thread_manager.start_thread_with_options(StartThreadOptions {
config: config.clone(),
@@ -108,6 +108,7 @@ async fn windows_exec_server_runs_with_native_shell_and_cwd() -> Result<()> {
vec![TurnEnvironmentSelection {
environment_id: REMOTE_ENVIRONMENT_ID.to_string(),
cwd: PathUri::parse("file:///C:/codex-home")?,
workspace_roots: Vec::new(),
}],
);
+2
View File
@@ -705,10 +705,12 @@ async fn multi_environment_thread_loads_every_project_and_keeps_creation_snapsho
TurnEnvironmentSelection {
environment_id: REMOTE_ENVIRONMENT_ID.to_string(),
cwd: PathUri::from_abs_path(&test.config.cwd),
workspace_roots: Vec::new(),
},
TurnEnvironmentSelection {
environment_id: LOCAL_ENVIRONMENT_ID.to_string(),
cwd: PathUri::from_host_native_path(local_root.path())?,
workspace_roots: Vec::new(),
},
],
thread_extension_init: Default::default(),
@@ -1653,6 +1653,7 @@ async fn apply_patch_turn_diff_tracks_local_and_remote_environment_paths() -> Re
TurnEnvironmentSelection {
environment_id: REMOTE_ENVIRONMENT_ID.to_string(),
cwd: PathUri::from_abs_path(&shared_cwd),
workspace_roots: Vec::new(),
},
];
test.codex
@@ -261,6 +261,7 @@ async fn approved_network_host_for_one_environment_still_prompts_in_another() ->
TurnEnvironmentSelection {
environment_id: REMOTE_ENVIRONMENT_ID.to_string(),
cwd: PathUri::from_abs_path(&remote_cwd),
workspace_roots: Vec::new(),
},
];
+6
View File
@@ -302,6 +302,7 @@ async fn explicit_remote_shell_runs_in_remote_cwd() -> Result<()> {
Some(vec![TurnEnvironmentSelection {
environment_id: REMOTE_ENVIRONMENT_ID.to_string(),
cwd: PathUri::from_abs_path(&test.config.cwd),
workspace_roots: Vec::new(),
}]),
)
.await?;
@@ -1134,6 +1135,7 @@ async fn exec_command_routes_to_selected_remote_environment() -> Result<()> {
let remote_selection = TurnEnvironmentSelection {
environment_id: REMOTE_ENVIRONMENT_ID.to_string(),
cwd: PathUri::from_abs_path(&remote_cwd),
workspace_roots: Vec::new(),
};
let multi_env_output = exec_command_routing_output(
&test,
@@ -1290,6 +1292,7 @@ async fn remote_request_permissions_grant_unblocks_later_remote_exec() -> Result
TurnEnvironmentSelection {
environment_id: REMOTE_ENVIRONMENT_ID.to_string(),
cwd: PathUri::from_abs_path(&remote_cwd),
workspace_roots: Vec::new(),
},
],
AskForApproval::OnRequest,
@@ -1431,6 +1434,7 @@ async fn apply_patch_freeform_routes_to_selected_remote_environment() -> Result<
TurnEnvironmentSelection {
environment_id: REMOTE_ENVIRONMENT_ID.to_string(),
cwd: PathUri::from_abs_path(&remote_cwd),
workspace_roots: Vec::new(),
},
]),
)
@@ -1514,6 +1518,7 @@ async fn apply_patch_approvals_are_remembered_per_environment() -> Result<()> {
TurnEnvironmentSelection {
environment_id: REMOTE_ENVIRONMENT_ID.to_string(),
cwd: PathUri::from_abs_path(&remote_cwd),
workspace_roots: Vec::new(),
},
];
let local_patch = format!(
@@ -1715,6 +1720,7 @@ async fn apply_patch_intercepted_exec_command_routes_to_selected_remote_environm
TurnEnvironmentSelection {
environment_id: REMOTE_ENVIRONMENT_ID.to_string(),
cwd: PathUri::from_abs_path(&remote_cwd),
workspace_roots: Vec::new(),
},
]),
)
+1
View File
@@ -590,6 +590,7 @@ async fn view_image_routes_to_selected_remote_environment() -> anyhow::Result<()
let remote_selection = TurnEnvironmentSelection {
environment_id: REMOTE_ENVIRONMENT_ID.to_string(),
cwd: remote_cwd_uri,
workspace_roots: Vec::new(),
};
let relative_call_id = "call-view-image-relative-multi-env";
let absolute_call_id = "call-view-image-absolute-multi-env";
@@ -199,6 +199,7 @@ async fn selected_executor_catalog_follows_step_availability_and_reuses_its_cach
let turn_environment = TurnEnvironmentSelection {
environment_id: "turn-env".to_string(),
cwd: PathUri::parse("file:///workspace").expect("cwd URI"),
workspace_roots: Vec::new(),
};
let available_sections = registry.context_contributors()[0]
.contribute_world_state(WorldStateContributionInput {
@@ -593,6 +594,7 @@ async fn root_qualified_locator_selects_only_the_matching_executor_skill() -> Te
environments: &[TurnEnvironmentSelection {
environment_id: "env-1".to_string(),
cwd: PathUri::parse("file:///workspace").expect("cwd URI"),
workspace_roots: Vec::new(),
}],
ready_selected_capability_roots: &selected_roots,
session_store: &session_store,
+1 -1
View File
@@ -324,7 +324,7 @@ impl MemoryStartupContext {
) -> anyhow::Result<SpawnedConsolidationAgent> {
let environments = self
.thread_manager
.default_environment_selections(&config.cwd);
.default_environment_selections(&config.cwd, &config.workspace_roots);
let NewThread {
thread_id, thread, ..
} = self
+1 -4
View File
@@ -134,6 +134,7 @@ pub fn strip_user_message_prefix(text: &str) -> &str {
pub struct TurnEnvironmentSelection {
pub environment_id: String,
pub cwd: PathUri,
pub workspace_roots: Vec<PathUri>,
}
#[derive(Debug, Clone, PartialEq)]
@@ -453,10 +454,6 @@ pub struct ThreadSettingsOverrides {
/// Updated fallback `cwd` and environments supplied together as a complete pair.
pub environments: Option<TurnEnvironmentSelections>,
/// Updated runtime workspace roots used to materialize symbolic
/// `:workspace_roots` filesystem permissions.
pub workspace_roots: Option<Vec<AbsolutePathBuf>>,
/// Updated profile-defined workspace roots for status summaries and
/// per-turn config reconstruction.
pub profile_workspace_roots: Option<Vec<AbsolutePathBuf>>,