Carry environment config in turn selections (#38521)

## What changed

- Add `EnvironmentConfigState` to `TurnEnvironmentSelection` so each environment attachment can inherit thread configuration or carry ready configuration for its login-shell policy and selected capability roots.
- Apply ready configuration during thread creation and thread-settings updates while reusing the existing environment connection.
- Validate selection configuration before applying settings, rejecting unsupported pending state and invalid capability roots.

## Testing

- Cover attachment-scoped capability roots, ready configuration at thread startup and turn updates, and rejection of invalid selections without changing existing settings.

GitOrigin-RevId: 86cf8f2148fdaa746ea0dd12890ab7b1577d7587
This commit is contained in:
sayan-oai
2026-08-14 07:52:38 +00:00
committed by copyberry
parent 86b1123ff6
commit fdbab67c66
28 changed files with 282 additions and 79 deletions
@@ -442,6 +442,7 @@ use codex_protocol::protocol::ConversationSpeechParams;
use codex_protocol::protocol::ConversationStartParams;
use codex_protocol::protocol::ConversationStartTransport;
use codex_protocol::protocol::ConversationTextParams;
use codex_protocol::protocol::EnvironmentConfigState;
use codex_protocol::protocol::EventMsg;
#[cfg(test)]
use codex_protocol::protocol::GitInfo as CoreGitInfo;
@@ -637,6 +638,7 @@ fn resolve_turn_environment_selections(
environment_id,
cwd,
workspace_roots,
config: EnvironmentConfigState::FromThread,
});
}
thread_manager
+1
View File
@@ -112,6 +112,7 @@ pub use codex_protocol::error::Result as CodexResult;
pub use codex_protocol::models::PermissionProfile;
pub use codex_protocol::openai_models::ModelPreset;
pub use codex_protocol::protocol::AskForApproval;
pub use codex_protocol::protocol::EnvironmentConfigState;
pub use codex_protocol::protocol::EventMsg;
pub use codex_protocol::protocol::Op;
pub use codex_protocol::protocol::SessionConfiguredEvent;
+2
View File
@@ -23,6 +23,7 @@ use codex_exec_server::RemoveOptions;
use codex_extension_api::UserInstructions;
use codex_features::Feature;
use codex_protocol::models::PermissionProfile;
use codex_protocol::protocol::EnvironmentConfigState;
use codex_protocol::protocol::TurnEnvironmentSelection;
use codex_utils_absolute_path::AbsolutePathBuf;
use codex_utils_path_uri::PathUri;
@@ -337,6 +338,7 @@ fn resolved_local_environments<const N: usize>(
environment_id: environment_id.to_string(),
cwd: PathUri::from_abs_path(&cwd),
workspace_roots: Vec::new(),
config: EnvironmentConfigState::FromThread,
},
Arc::new(
Environment::create_for_tests(/*exec_server_url*/ None)
+65 -22
View File
@@ -17,6 +17,7 @@ use codex_protocol::capabilities::SelectedCapabilityRoot;
use codex_protocol::error::CodexErr;
use codex_protocol::error::Result as CodexResult;
use codex_protocol::protocol::EnvironmentConfig;
use codex_protocol::protocol::EnvironmentConfigState;
use codex_protocol::protocol::EnvironmentConnectionEvent;
use codex_protocol::protocol::Event;
use codex_protocol::protocol::EventMsg;
@@ -46,6 +47,7 @@ pub(crate) fn default_thread_environment_selections(
environment_id,
cwd: PathUri::from_abs_path(cwd),
workspace_roots: workspace_roots.iter().map(PathUri::from_abs_path).collect(),
config: EnvironmentConfigState::FromThread,
})
.collect()
}
@@ -80,17 +82,25 @@ pub(crate) struct StartingTurnEnvironment {
}
impl SelectedTurnEnvironment {
fn has_installed_environment_config(&self) -> bool {
self.config.selected_capability_roots.is_some()
}
// Thread settings still own permissions, but cannot overwrite shell policy
// that was installed specifically for this environment attachment.
fn update_thread_config(&mut self, config: &TurnEnvironmentConfig) {
if !self.has_installed_environment_config() {
self.config.allow_login_shell = config.allow_login_shell;
}
self.config.permission_profile = config.permission_profile.clone();
fn apply_configuration(
&mut self,
selection_config: EnvironmentConfigState,
thread_config: &TurnEnvironmentConfig,
) {
self.config = match &selection_config {
EnvironmentConfigState::FromThread => thread_config.clone(),
EnvironmentConfigState::Ready(config) => TurnEnvironmentConfig {
allow_login_shell: config.allow_login_shell,
// temp read from thread_config; will go away once perms on EnvironmentConfig,
// then we can just assign passed config directly
permission_profile: thread_config.permission_profile.clone(),
selected_capability_roots: Some(config.selected_capability_roots.clone()),
},
EnvironmentConfigState::Pending => {
unreachable!("pending environment configuration is not supported yet")
}
};
self.selection.config = selection_config;
}
}
@@ -156,7 +166,10 @@ impl ThreadEnvironments {
};
// Child threads get their own settings, but inherit any
// environment-owned policy that was already installed.
inherited_environment.update_thread_config(&thread_environment_config);
inherited_environment.apply_configuration(
inherited_environment.selection.config.clone(),
&thread_environment_config,
);
Some(inherited_environment)
})
.collect();
@@ -182,13 +195,18 @@ impl ThreadEnvironments {
if !seen_environment_ids.insert(selected_environment.environment_id.as_str()) {
continue;
}
if let Some(environment) = previous
.iter()
.find(|environment| environment.selection == *selected_environment)
&& !matches!(environment.resolution.clone().now_or_never(), Some(Err(_)))
if let Some(environment) = previous.iter().find(|environment| {
let previous = &environment.selection;
previous.environment_id == selected_environment.environment_id
&& previous.cwd == selected_environment.cwd
&& previous.workspace_roots == selected_environment.workspace_roots
}) && !matches!(environment.resolution.clone().now_or_never(), Some(Err(_)))
{
let mut environment = environment.clone();
environment.update_thread_config(thread_environment_config);
environment.apply_configuration(
selected_environment.config.clone(),
thread_environment_config,
);
next.push(environment);
continue;
}
@@ -224,13 +242,18 @@ impl ThreadEnvironments {
.remote_handle();
drop(tokio::spawn(resolution_task));
let resolution = resolution.boxed().shared();
next.push(SelectedTurnEnvironment {
let mut selected = SelectedTurnEnvironment {
selection: selected_environment.clone(),
config: thread_environment_config.clone(),
environment,
connection_events_task,
resolution,
});
};
selected.apply_configuration(
selected_environment.config.clone(),
thread_environment_config,
);
next.push(selected);
}
let removed_connection_tasks = previous
.iter()
@@ -290,7 +313,7 @@ impl ThreadEnvironments {
.iter()
.map(|environment| {
let mut environment = environment.clone();
environment.update_thread_config(config);
environment.apply_configuration(environment.selection.config.clone(), config);
environment
})
.collect();
@@ -315,8 +338,8 @@ impl ThreadEnvironments {
)));
};
environment.config.allow_login_shell = config.allow_login_shell;
environment.config.selected_capability_roots = Some(config.selected_capability_roots);
let thread_config = environment.config.clone();
environment.apply_configuration(EnvironmentConfigState::Ready(config), &thread_config);
self.environments.store(Arc::new(environments));
Ok(())
}
@@ -787,6 +810,7 @@ mod tests {
environment_id: REMOTE_ENVIRONMENT_ID.to_string(),
cwd: cwd_uri.clone(),
workspace_roots: vec![cwd_uri],
config: EnvironmentConfigState::FromThread,
}]
);
}
@@ -820,11 +844,13 @@ url = "ws://127.0.0.1:8765"
environment_id: LOCAL_ENVIRONMENT_ID.to_string(),
cwd: cwd_uri.clone(),
workspace_roots: vec![cwd_uri.clone()],
config: EnvironmentConfigState::FromThread,
},
TurnEnvironmentSelection {
environment_id: REMOTE_ENVIRONMENT_ID.to_string(),
cwd: cwd_uri.clone(),
workspace_roots: vec![cwd_uri],
config: EnvironmentConfigState::FromThread,
},
]
);
@@ -870,6 +896,7 @@ url = "ws://127.0.0.1:8765"
environment_id: LOCAL_ENVIRONMENT_ID.to_string(),
cwd: PathUri::from_abs_path(&cwd),
workspace_roots: Vec::new(),
config: EnvironmentConfigState::FromThread,
}],
&expected_config,
);
@@ -890,6 +917,7 @@ url = "ws://127.0.0.1:8765"
environment_id: LOCAL_ENVIRONMENT_ID.to_string(),
cwd: cwd_uri.clone(),
workspace_roots: Vec::new(),
config: EnvironmentConfigState::FromThread,
};
let resolved = resolve_turn_environments(
@@ -900,6 +928,7 @@ url = "ws://127.0.0.1:8765"
environment_id: LOCAL_ENVIRONMENT_ID.to_string(),
cwd: cwd_uri.join("other").expect("other cwd URI"),
workspace_roots: Vec::new(),
config: EnvironmentConfigState::FromThread,
},
],
)
@@ -921,6 +950,7 @@ url = "ws://127.0.0.1:8765"
environment_id: "local".to_string(),
cwd: selected_cwd_uri,
workspace_roots: Vec::new(),
config: EnvironmentConfigState::FromThread,
}],
)
.await;
@@ -960,6 +990,7 @@ url = "ws://127.0.0.1:8765"
environment_id: LOCAL_ENVIRONMENT_ID.to_string(),
cwd: cwd_uri.clone(),
workspace_roots: Vec::new(),
config: EnvironmentConfigState::FromThread,
};
let resolved = resolve_turn_environments(
@@ -969,6 +1000,7 @@ url = "ws://127.0.0.1:8765"
environment_id: "missing".to_string(),
cwd: cwd_uri,
workspace_roots: Vec::new(),
config: EnvironmentConfigState::FromThread,
},
local.clone(),
],
@@ -997,6 +1029,7 @@ url = "ws://127.0.0.1:8765"
environment_id: REMOTE_ENVIRONMENT_ID.to_string(),
cwd: PathUri::from_abs_path(&AbsolutePathBuf::current_dir().expect("cwd")),
workspace_roots: Vec::new(),
config: EnvironmentConfigState::FromThread,
};
let environments = Arc::new(ThreadEnvironments::new(
manager,
@@ -1056,11 +1089,13 @@ url = "ws://127.0.0.1:8765"
environment_id: REMOTE_ENVIRONMENT_ID.to_string(),
cwd: cwd.clone(),
workspace_roots: Vec::new(),
config: EnvironmentConfigState::FromThread,
};
let local = TurnEnvironmentSelection {
environment_id: LOCAL_ENVIRONMENT_ID.to_string(),
cwd,
workspace_roots: Vec::new(),
config: EnvironmentConfigState::FromThread,
};
let turn_environments = ThreadEnvironments::new(
manager,
@@ -1163,6 +1198,7 @@ url = "ws://127.0.0.1:8765"
environment_id: REMOTE_ENVIRONMENT_ID.to_string(),
cwd: PathUri::from_abs_path(&AbsolutePathBuf::current_dir().expect("cwd")),
workspace_roots: Vec::new(),
config: EnvironmentConfigState::FromThread,
};
let environments = ThreadEnvironments::new(
Arc::clone(&manager),
@@ -1248,6 +1284,7 @@ url = "ws://127.0.0.1:8765"
environment_id: REMOTE_ENVIRONMENT_ID.to_string(),
cwd: PathUri::from_abs_path(&cwd),
workspace_roots: Vec::new(),
config: EnvironmentConfigState::FromThread,
};
let (tx_event, rx_event) = async_channel::unbounded();
let environments = Arc::new(ThreadEnvironments::new(
@@ -1337,6 +1374,7 @@ url = "ws://127.0.0.1:8765"
environment_id: REMOTE_ENVIRONMENT_ID.to_string(),
cwd: PathUri::from_abs_path(&cwd),
workspace_roots: Vec::new(),
config: EnvironmentConfigState::FromThread,
};
let inherited_environment = Arc::new(
Environment::create_for_tests(Some("ws://127.0.0.1:8765".to_string()))
@@ -1391,6 +1429,7 @@ url = "ws://127.0.0.1:8765"
environment_id: LOCAL_ENVIRONMENT_ID.to_string(),
cwd: PathUri::from_abs_path(&cwd),
workspace_roots: Vec::new(),
config: EnvironmentConfigState::FromThread,
};
let root = |id: &str| SelectedCapabilityRoot {
id: id.to_string(),
@@ -1482,6 +1521,7 @@ url = "ws://127.0.0.1:8765"
environment_id: LOCAL_ENVIRONMENT_ID.to_string(),
cwd: cwd_uri.clone(),
workspace_roots: Vec::new(),
config: EnvironmentConfigState::FromThread,
}],
)
.await;
@@ -1496,6 +1536,7 @@ url = "ws://127.0.0.1:8765"
environment_id: REMOTE_ENVIRONMENT_ID.to_string(),
cwd: remote_cwd_uri.clone(),
workspace_roots: Vec::new(),
config: EnvironmentConfigState::FromThread,
},
remote_environment.clone(),
/*shell*/ None,
@@ -1509,6 +1550,7 @@ url = "ws://127.0.0.1:8765"
environment_id: REMOTE_ENVIRONMENT_ID.to_string(),
cwd: remote_cwd_uri,
workspace_roots: Vec::new(),
config: EnvironmentConfigState::FromThread,
},
remote_environment,
/*shell*/ None,
@@ -1535,6 +1577,7 @@ url = "ws://127.0.0.1:8765"
environment_id: LOCAL_ENVIRONMENT_ID.to_string(),
cwd: PathUri::from_abs_path(&cwd),
workspace_roots: Vec::new(),
config: EnvironmentConfigState::FromThread,
},
config: test_environment_config(),
resolution: futures::future::pending().boxed().shared(),
+2
View File
@@ -26,6 +26,7 @@ use codex_hooks::HooksConfig;
use codex_model_provider::create_model_provider;
use codex_protocol::models::PermissionProfile;
use codex_protocol::protocol::AskForApproval;
use codex_protocol::protocol::EnvironmentConfigState;
use codex_protocol::protocol::EventMsg;
use codex_protocol::protocol::GranularApprovalConfig;
use codex_protocol::protocol::McpInvocation;
@@ -1111,6 +1112,7 @@ async fn mcp_sandbox_cwd_uses_matching_server_environment_uri() -> anyhow::Resul
environment_id: "remote".to_string(),
cwd: secondary_cwd.clone(),
workspace_roots: Vec::new(),
config: EnvironmentConfigState::FromThread,
},
environment,
/*shell*/ None,
+47 -19
View File
@@ -6,6 +6,7 @@ use codex_protocol::capabilities::CapabilityRootLocation;
use codex_protocol::error::CodexErr;
use codex_protocol::error::Result as CodexResult;
use codex_protocol::protocol::EnvironmentConfig;
use codex_protocol::protocol::EnvironmentConfigState;
use codex_protocol::protocol::TurnEnvironmentSelection;
use crate::config::ConstraintResult;
@@ -13,6 +14,51 @@ use crate::session::session::Session;
use crate::session::session::SessionConfiguration;
use crate::session::session::SessionSettingsUpdate;
pub(super) fn validate_environment_selections(
selections: &[TurnEnvironmentSelection],
) -> CodexResult<()> {
for selection in selections {
match &selection.config {
EnvironmentConfigState::FromThread => {}
EnvironmentConfigState::Pending => {
return Err(CodexErr::InvalidRequest(
"pending environment configuration is not supported yet".to_string(),
));
}
EnvironmentConfigState::Ready(config) => {
validate_environment_config(selection, config)?;
}
}
}
Ok(())
}
fn validate_environment_config(
selection: &TurnEnvironmentSelection,
config: &EnvironmentConfig,
) -> CodexResult<()> {
if config.selected_capability_roots.len() > MAX_SELECTED_CAPABILITY_ROOTS {
return Err(CodexErr::InvalidRequest(format!(
"environment readiness contains more than {MAX_SELECTED_CAPABILITY_ROOTS} selected capability roots"
)));
}
let mut root_ids = HashSet::with_capacity(config.selected_capability_roots.len());
for root in &config.selected_capability_roots {
let CapabilityRootLocation::Environment { environment_id, .. } = &root.location;
if root.id.trim().is_empty()
|| environment_id != &selection.environment_id
|| !root_ids.insert(root.id.as_str())
{
return Err(CodexErr::InvalidRequest(format!(
"selected capability roots must have unique non-empty IDs and belong to environment `{}`",
selection.environment_id
)));
}
}
Ok(())
}
impl Session {
pub(super) fn apply_session_settings(
&self,
@@ -27,25 +73,7 @@ impl Session {
selection: &TurnEnvironmentSelection,
config: EnvironmentConfig,
) -> CodexResult<()> {
if config.selected_capability_roots.len() > MAX_SELECTED_CAPABILITY_ROOTS {
return Err(CodexErr::InvalidRequest(format!(
"environment readiness contains more than {MAX_SELECTED_CAPABILITY_ROOTS} selected capability roots"
)));
}
let mut root_ids = HashSet::with_capacity(config.selected_capability_roots.len());
for root in &config.selected_capability_roots {
let CapabilityRootLocation::Environment { environment_id, .. } = &root.location;
if root.id.trim().is_empty()
|| environment_id != &selection.environment_id
|| !root_ids.insert(root.id.as_str())
{
return Err(CodexErr::InvalidRequest(format!(
"selected capability roots must have unique non-empty IDs and belong to environment `{}`",
selection.environment_id
)));
}
}
validate_environment_config(selection, &config)?;
// grab session lock so installation can't race w/ thread settings updates
let _state = self.state.lock().await;
+1 -1
View File
@@ -709,7 +709,7 @@ impl Session {
user_shell_override,
};
session_configuration
.validate_auto_review_requirement(&environment_selections)
.validate(&environment_selections)
.map_err(|err| CodexErr::InvalidRequest(err.to_string()))?;
// Generate a unique ID for the lifetime of this session.
+17 -2
View File
@@ -243,7 +243,22 @@ impl SessionConfiguration {
}
}
pub(super) fn validate_auto_review_requirement(
pub(super) fn validate(
&self,
environments: &[TurnEnvironmentSelection],
) -> ConstraintResult<()> {
self.validate_auto_review_requirement(environments)?;
super::environment::validate_environment_selections(environments).map_err(|error| {
ConstraintError::InvalidValue {
field_name: "environments",
candidate: "environment configuration".to_string(),
allowed: format!("valid selected capability roots ({error})"),
requirement_source: codex_config::RequirementSource::Unknown,
}
})
}
fn validate_auto_review_requirement(
&self,
environments: &[TurnEnvironmentSelection],
) -> ConstraintResult<()> {
@@ -463,7 +478,7 @@ impl SessionConfiguration {
.map_or(current_environments, |environments| {
environments.environments.as_slice()
});
next_configuration.validate_auto_review_requirement(next_environments)?;
next_configuration.validate(next_environments)?;
Ok(next_configuration)
}
+8
View File
@@ -66,6 +66,7 @@ use codex_protocol::permissions::FileSystemPath;
use codex_protocol::permissions::FileSystemSandboxEntry;
use codex_protocol::permissions::FileSystemSandboxPolicy;
use codex_protocol::permissions::FileSystemSpecialPath;
use codex_protocol::protocol::EnvironmentConfigState;
use codex_protocol::protocol::ErrorEvent;
use codex_protocol::protocol::SandboxPolicy;
use codex_protocol::protocol::TurnEnvironmentSelections;
@@ -6644,6 +6645,7 @@ async fn request_permissions_tool_resolves_relative_paths_against_selected_envir
environment_id: "remote".to_string(),
cwd: PathUri::from_abs_path(&environment_cwd),
workspace_roots: Vec::new(),
config: EnvironmentConfigState::FromThread,
},
current_environment.environment,
current_environment.shell,
@@ -7243,6 +7245,7 @@ async fn primary_environment_uses_first_turn_environment() {
environment_id: "second".to_string(),
cwd: second_cwd_uri.clone(),
workspace_roots: Vec::new(),
config: EnvironmentConfigState::FromThread,
},
Arc::clone(&first_environment.environment),
/*shell*/ None,
@@ -8654,6 +8657,7 @@ async fn conflicting_ready_environment_root_ids_keep_first_location() {
environment_id: environment_id.clone(),
cwd: local_environment.cwd().clone(),
workspace_roots: local_environment.workspace_roots().to_vec(),
config: EnvironmentConfigState::FromThread,
},
environment,
local_environment.shell.clone(),
@@ -8913,6 +8917,7 @@ async fn record_context_updates_emits_environment_item_for_cwd_changes() {
environment_id: environment.selection.environment_id,
cwd: PathUri::from_abs_path(&cwd),
workspace_roots: Vec::new(),
config: EnvironmentConfigState::FromThread,
},
environment.environment,
environment.shell,
@@ -8972,6 +8977,7 @@ async fn record_context_updates_use_environment_permission_profile_and_workspace
environment_id: environment.selection.environment_id,
cwd,
workspace_roots: vec![PathUri::from_abs_path(&workspace_root)],
config: EnvironmentConfigState::FromThread,
},
environment.environment,
environment.shell,
@@ -9047,6 +9053,7 @@ async fn record_context_updates_omits_environment_item_when_disabled() {
environment_id: environment.selection.environment_id,
cwd: PathUri::from_abs_path(&test_path_buf("/new-repo").abs()),
workspace_roots: Vec::new(),
config: EnvironmentConfigState::FromThread,
},
environment.environment,
environment.shell,
@@ -9515,6 +9522,7 @@ async fn turn_context_item_stores_local_cwd() {
environment_id: "remote".to_string(),
cwd,
workspace_roots: Vec::new(),
config: EnvironmentConfigState::FromThread,
},
environment.environment,
environment.shell,
@@ -25,6 +25,7 @@ use codex_protocol::models::ReasoningItemReasoningSummary;
use codex_protocol::models::ResponseItem;
use codex_protocol::openai_models::ModelsResponse;
use codex_protocol::protocol::AgentMessageEvent;
use codex_protocol::protocol::EnvironmentConfigState;
use codex_protocol::protocol::InternalSessionSource;
use codex_protocol::protocol::SessionMeta;
use codex_protocol::protocol::SessionMetaLine;
@@ -1143,6 +1144,7 @@ async fn resume_and_fork_do_not_restore_thread_environments_from_rollout() {
environment_id: "local".to_string(),
cwd: PathUri::from_abs_path(&selected_cwd),
workspace_roots: Vec::new(),
config: EnvironmentConfigState::FromThread,
}];
let default_cwd = config.cwd.clone();
let mut source_config = config.clone();
@@ -25,6 +25,7 @@ use crate::tools::handlers::ShellCommandHandler;
use crate::tools::hook_names::HookToolName;
use crate::tools::registry::CoreToolRuntime;
use crate::turn_diff_tracker::TurnDiffTracker;
use codex_protocol::protocol::EnvironmentConfigState;
use codex_protocol::protocol::TurnEnvironmentSelection;
use codex_shell_command::is_safe_command::is_known_safe_command;
use codex_shell_command::powershell::try_find_powershell_executable_blocking;
@@ -108,6 +109,7 @@ async fn shell_command_handler_to_exec_params_uses_selected_environment() {
environment_id: "selected-environment".to_string(),
cwd: PathUri::from_abs_path(&selected_cwd),
workspace_roots: Vec::new(),
config: EnvironmentConfigState::FromThread,
},
Arc::clone(
&turn_context
@@ -7,6 +7,7 @@ use codex_protocol::models::AdditionalPermissionProfile;
use codex_protocol::models::FileSystemPermissions;
use codex_protocol::models::PermissionProfile;
use codex_protocol::permissions::NetworkSandboxPolicy;
use codex_protocol::protocol::EnvironmentConfigState;
use codex_protocol::protocol::GranularApprovalConfig;
use codex_protocol::protocol::TurnEnvironmentSelection;
use codex_sandboxing::SandboxManager;
@@ -23,6 +24,7 @@ fn test_turn_environment(environment_id: &str) -> crate::session::turn_context::
environment_id: environment_id.to_string(),
cwd: PathUri::from_abs_path(&std::env::temp_dir().abs()),
workspace_roots: Vec::new(),
config: EnvironmentConfigState::FromThread,
},
std::sync::Arc::new(codex_exec_server::Environment::default_for_tests()),
/*shell*/ None,
@@ -4,6 +4,7 @@ use crate::session::turn_context::TurnEnvironmentConfig;
use crate::tools::approvals::ApprovalCacheKey;
use codex_exec_server::Environment;
use codex_protocol::models::PermissionProfile;
use codex_protocol::protocol::EnvironmentConfigState;
use codex_protocol::protocol::TurnEnvironmentSelection;
use codex_utils_path_uri::PathUri;
use pretty_assertions::assert_eq;
@@ -20,6 +21,7 @@ async fn approval_key_uses_path_uri_and_includes_environment_id() {
environment_id: "remote".to_string(),
cwd: PathUri::from_abs_path(&cwd),
workspace_roots: Vec::new(),
config: EnvironmentConfigState::FromThread,
},
Arc::new(Environment::default_for_tests()),
/*shell*/ None,
@@ -520,6 +520,7 @@ mod tests {
use codex_exec_server::Environment;
use codex_exec_server::LOCAL_ENVIRONMENT_ID;
use codex_protocol::models::PermissionProfile;
use codex_protocol::protocol::EnvironmentConfigState;
use codex_protocol::protocol::TurnEnvironmentSelection;
use codex_tools::ZshForkConfig;
use codex_utils_absolute_path::AbsolutePathBuf;
@@ -535,6 +536,7 @@ mod tests {
environment_id: LOCAL_ENVIRONMENT_ID.to_string(),
cwd,
workspace_roots: Vec::new(),
config: EnvironmentConfigState::FromThread,
},
Arc::new(Environment::default_for_tests()),
/*shell*/ None,
@@ -21,6 +21,7 @@ use codex_protocol::openai_models::ConfigShellToolType;
use codex_protocol::openai_models::InputModality;
use codex_protocol::openai_models::ToolMode;
use codex_protocol::openai_models::WebSearchToolType;
use codex_protocol::protocol::EnvironmentConfigState;
use codex_protocol::protocol::MultiAgentVersion;
use codex_protocol::protocol::SessionSource;
use codex_protocol::protocol::SubAgentSource;
@@ -907,6 +908,7 @@ async fn zsh_fork_unified_exec_keeps_shell_parameter_when_remote_environment_ava
environment_id: "remote".to_string(),
cwd: remote_cwd,
workspace_roots: Vec::new(),
config: EnvironmentConfigState::FromThread,
},
Arc::new(
codex_exec_server::Environment::create_for_tests(Some(
+4
View File
@@ -47,6 +47,7 @@ use codex_protocol::models::PermissionProfile;
use codex_protocol::openai_models::ModelInfo;
use codex_protocol::openai_models::ModelsResponse;
use codex_protocol::protocol::AskForApproval;
use codex_protocol::protocol::EnvironmentConfigState;
use codex_protocol::protocol::EventMsg;
use codex_protocol::protocol::RealtimeConversationVersion as RealtimeWsVersion;
use codex_protocol::protocol::SandboxPolicy;
@@ -118,6 +119,7 @@ pub fn local(cwd: AbsolutePathBuf) -> 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)],
config: EnvironmentConfigState::FromThread,
}
}
@@ -150,6 +152,7 @@ impl TestEnv {
environment_id: codex_exec_server::REMOTE_ENVIRONMENT_ID.to_string(),
cwd: PathUri::from_abs_path(&cwd),
workspace_roots: vec![PathUri::from_abs_path(&cwd)],
config: EnvironmentConfigState::FromThread,
},
None => local(cwd.clone()),
};
@@ -218,6 +221,7 @@ pub async fn test_env() -> Result<TestEnv> {
environment_id: codex_exec_server::REMOTE_ENVIRONMENT_ID.to_string(),
cwd: cwd_uri.clone(),
workspace_roots: vec![cwd_uri.clone()],
config: EnvironmentConfigState::FromThread,
};
let cwd = if remote_env == TestEnvironment::WineExec {
// TODO(anp): Convert `Config::cwd` to `LegacyAppPathString` and remove this
@@ -9,6 +9,7 @@ use codex_protocol::config_types::ModeKind;
use codex_protocol::config_types::Settings;
use codex_protocol::models::PermissionProfile;
use codex_protocol::protocol::AskForApproval;
use codex_protocol::protocol::EnvironmentConfigState;
use codex_protocol::protocol::EventMsg;
use codex_protocol::protocol::ExecCommandStatus;
use codex_protocol::protocol::TurnEnvironmentSelection;
@@ -115,6 +116,7 @@ async fn windows_exec_server_runs_with_native_shell_and_cwd() -> Result<()> {
environment_id: REMOTE_ENVIRONMENT_ID.to_string(),
cwd: cwd.clone(),
workspace_roots: vec![cwd],
config: EnvironmentConfigState::FromThread,
}
}],
);
+5
View File
@@ -10,6 +10,7 @@ use codex_features::Feature;
use codex_history::RolloutItem;
use codex_history::RolloutLine;
use codex_home::CodexHomeUserInstructionsProvider;
use codex_protocol::protocol::EnvironmentConfigState;
use codex_protocol::protocol::EventMsg;
use codex_protocol::protocol::Op;
use codex_protocol::protocol::TurnEnvironmentSelection;
@@ -658,11 +659,13 @@ async fn multi_environment_project_instructions_share_one_byte_budget() -> Resul
environment_id: REMOTE_ENVIRONMENT_ID.to_string(),
cwd: PathUri::from_abs_path(&test.config.cwd),
workspace_roots: vec![PathUri::from_abs_path(&test.config.cwd)],
config: EnvironmentConfigState::FromThread,
},
TurnEnvironmentSelection {
environment_id: LOCAL_ENVIRONMENT_ID.to_string(),
cwd: PathUri::from_host_native_path(local_root.path())?,
workspace_roots: vec![PathUri::from_host_native_path(local_root.path())?],
config: EnvironmentConfigState::FromThread,
},
]),
..StartThreadOptions::new(test.config.clone())
@@ -736,11 +739,13 @@ async fn multi_environment_thread_loads_every_project_and_keeps_creation_snapsho
environment_id: REMOTE_ENVIRONMENT_ID.to_string(),
cwd: PathUri::from_abs_path(&test.config.cwd),
workspace_roots: vec![PathUri::from_abs_path(&test.config.cwd)],
config: EnvironmentConfigState::FromThread,
},
TurnEnvironmentSelection {
environment_id: LOCAL_ENVIRONMENT_ID.to_string(),
cwd: PathUri::from_host_native_path(local_root.path())?,
workspace_roots: vec![PathUri::from_host_native_path(local_root.path())?],
config: EnvironmentConfigState::FromThread,
},
]),
..StartThreadOptions::new(test.config.clone())
@@ -31,6 +31,7 @@ use codex_protocol::permissions::FileSystemSandboxPolicy;
use codex_protocol::permissions::FileSystemSpecialPath;
use codex_protocol::permissions::NetworkSandboxPolicy;
use codex_protocol::protocol::AskForApproval;
use codex_protocol::protocol::EnvironmentConfigState;
use codex_protocol::protocol::EventMsg;
use codex_protocol::protocol::SandboxPolicy;
use codex_protocol::protocol::ThreadSettingsOverrides;
@@ -1886,6 +1887,7 @@ async fn apply_patch_turn_diff_tracks_local_and_remote_environment_paths() -> Re
environment_id: REMOTE_ENVIRONMENT_ID.to_string(),
cwd: PathUri::from_abs_path(&shared_cwd),
workspace_roots: vec![PathUri::from_abs_path(&shared_cwd)],
config: EnvironmentConfigState::FromThread,
},
];
test.codex
@@ -23,6 +23,7 @@ use codex_protocol::config_types::ModeKind;
use codex_protocol::config_types::Settings;
use codex_protocol::models::PermissionProfile;
use codex_protocol::protocol::AskForApproval;
use codex_protocol::protocol::EnvironmentConfigState;
use codex_protocol::protocol::EventMsg;
use codex_protocol::protocol::ThreadSettingsOverrides;
use codex_protocol::protocol::TurnEnvironmentSelection;
@@ -217,6 +218,7 @@ async fn model_visible_environment_context_preserves_foreign_workspace_roots() -
environment_id: LOCAL_ENVIRONMENT_ID.to_string(),
cwd: PathUri::from_abs_path(&test.config.cwd),
workspace_roots: vec![foreign_root],
config: EnvironmentConfigState::FromThread,
}],
)),
..Default::default()
@@ -9,6 +9,7 @@ use codex_features::Feature;
use codex_protocol::models::PermissionProfile;
use codex_protocol::permissions::NetworkSandboxPolicy;
use codex_protocol::protocol::AskForApproval;
use codex_protocol::protocol::EnvironmentConfigState;
use codex_protocol::protocol::EventMsg;
use codex_protocol::protocol::ThreadSettingsOverrides;
use codex_protocol::protocol::TurnEnvironmentSelection;
@@ -190,11 +191,13 @@ async fn two_exec_servers_isolate_workspace_write_roots() -> Result<()> {
environment_id: FIRST_ENVIRONMENT_ID.to_string(),
cwd: first_workspace_uri.clone(),
workspace_roots: vec![first_workspace_uri],
config: EnvironmentConfigState::FromThread,
},
TurnEnvironmentSelection {
environment_id: SECOND_ENVIRONMENT_ID.to_string(),
cwd: second_workspace_uri.clone(),
workspace_roots: vec![second_workspace_uri],
config: EnvironmentConfigState::FromThread,
},
];
let permission_profile = PermissionProfile::workspace_write_with(
@@ -19,6 +19,7 @@ use codex_protocol::models::NetworkPermissions;
use codex_protocol::models::PermissionProfile;
use codex_protocol::permissions::NetworkSandboxPolicy;
use codex_protocol::protocol::AskForApproval;
use codex_protocol::protocol::EnvironmentConfigState;
use codex_protocol::protocol::EventMsg;
use codex_protocol::protocol::ExecApprovalRequestEvent;
use codex_protocol::protocol::GuardianAssessmentStatus;
@@ -1837,6 +1838,7 @@ async fn approved_network_host_for_one_environment_still_prompts_in_another() ->
environment_id: REMOTE_ENVIRONMENT_ID.to_string(),
cwd: PathUri::from_abs_path(&remote_cwd),
workspace_roots: vec![PathUri::from_abs_path(&remote_cwd)],
config: EnvironmentConfigState::FromThread,
},
];
+85 -35
View File
@@ -56,6 +56,7 @@ use codex_protocol::permissions::NetworkSandboxPolicy;
use codex_protocol::protocol::ApplyPatchApprovalRequestEvent;
use codex_protocol::protocol::AskForApproval;
use codex_protocol::protocol::ENVIRONMENTS_INSTRUCTIONS_OPEN_TAG;
use codex_protocol::protocol::EnvironmentConfigState;
use codex_protocol::protocol::EventMsg;
use codex_protocol::protocol::Op;
use codex_protocol::protocol::ReviewDecision;
@@ -415,6 +416,7 @@ async fn explicit_remote_shell_runs_in_remote_cwd() -> Result<()> {
environment_id: REMOTE_ENVIRONMENT_ID.to_string(),
cwd: PathUri::from_abs_path(&test.config.cwd),
workspace_roots: vec![PathUri::from_abs_path(&test.config.cwd)],
config: EnvironmentConfigState::FromThread,
}]),
)
.await?;
@@ -693,6 +695,7 @@ async fn deferred_executor_promotes_primary_environment_when_startup_completes()
environment_id: REMOTE_ENVIRONMENT_ID.to_string(),
cwd: PathUri::from_abs_path(&test.config.cwd),
workspace_roots: vec![PathUri::from_abs_path(&test.config.cwd)],
config: EnvironmentConfigState::FromThread,
};
test.submit_turn_with_environments(
@@ -1005,36 +1008,69 @@ async fn shared_executor_keeps_ready_capability_roots_scoped_to_each_attachment(
},
};
for config in [
EnvironmentConfigState::Pending,
EnvironmentConfigState::Ready(EnvironmentConfig {
allow_login_shell: false,
selected_capability_roots: vec![root("duplicate"), root("duplicate")],
}),
] {
let invalid_selection = TurnEnvironmentSelection {
config,
..selection.clone()
};
assert!(
test.codex
.preview_thread_settings_overrides(CodexThreadSettingsOverrides {
environments: Some(TurnEnvironmentSelections::new(
test.config.cwd.clone(),
vec![invalid_selection],
)),
..Default::default()
})
.await
.is_err()
);
assert_eq!(
test.codex.environment_selections().await,
vec![selection.clone()]
);
}
let mut second_thread_init = ExtensionDataInit::new();
second_thread_init.insert(vec![root("startup-root")]);
let second = test
.thread_manager
.start_thread(StartThreadOptions {
environments: Some(vec![selection.clone()]),
environments: Some(vec![TurnEnvironmentSelection {
config: EnvironmentConfigState::Ready(EnvironmentConfig {
allow_login_shell: true,
selected_capability_roots: vec![root("startup-root"), root("second-root")],
}),
..selection.clone()
}]),
thread_extension_init: second_thread_init,
..StartThreadOptions::new(test.config.clone())
})
.await?;
test.codex
.environment_ready(
&selection,
EnvironmentConfig {
allow_login_shell: false,
selected_capability_roots: vec![root("first-root")],
},
)
.await?;
second
.thread
.environment_ready(
&selection,
EnvironmentConfig {
allow_login_shell: true,
selected_capability_roots: vec![root("startup-root"), root("second-root")],
},
)
.await?;
submit_thread_settings(
&test.codex,
ThreadSettingsOverrides {
environments: Some(TurnEnvironmentSelections::new(
test.config.cwd.clone(),
vec![TurnEnvironmentSelection {
config: EnvironmentConfigState::Ready(EnvironmentConfig {
allow_login_shell: false,
selected_capability_roots: vec![root("first-root")],
}),
..selection.clone()
}],
)),
..Default::default()
},
)
.await?;
assert_eq!(
test.codex.inspect_selected_capability_roots().ready_roots,
@@ -1071,24 +1107,27 @@ async fn shared_executor_keeps_ready_capability_roots_scoped_to_each_attachment(
.into_iter()
.enumerate()
{
let mut request = TurnInputRequest::user_input(vec![UserInput::Text {
text: prompt.to_string(),
text_elements: Vec::new(),
}]);
if index == 2 {
test.codex
.environment_ready(
&selection,
EnvironmentConfig {
allow_login_shell: false,
selected_capability_roots: vec![root("first-updated-root")],
},
)
.await?;
request = request.with_thread_settings(ThreadSettingsOverrides {
environments: Some(TurnEnvironmentSelections::new(
test.config.cwd.clone(),
vec![TurnEnvironmentSelection {
config: EnvironmentConfigState::Ready(EnvironmentConfig {
allow_login_shell: false,
selected_capability_roots: vec![root("first-updated-root")],
}),
..selection.clone()
}],
)),
..Default::default()
});
}
thread
.start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text {
text: prompt.to_string(),
text_elements: Vec::new(),
}]))
.await?;
thread.start_or_steer_turn(request).await?;
wait_for_event(thread, |event| matches!(event, EventMsg::TurnComplete(_))).await;
}
@@ -1288,6 +1327,7 @@ async fn ready_before_selection_exposes_remote_tools_and_capability_context_afte
environment_id: REMOTE_ENVIRONMENT_ID.to_string(),
cwd: PathUri::from_abs_path(&test.config.cwd),
workspace_roots: vec![PathUri::from_abs_path(&test.config.cwd)],
config: EnvironmentConfigState::FromThread,
}]),
)
.await?;
@@ -1390,6 +1430,7 @@ async fn deferred_executor_stays_pending_after_materialization() -> Result<()> {
environment_id: REMOTE_ENVIRONMENT_ID.to_string(),
cwd: PathUri::from_abs_path(&test.config.cwd),
workspace_roots: vec![PathUri::from_abs_path(&test.config.cwd)],
config: EnvironmentConfigState::FromThread,
}],
)),
..Default::default()
@@ -1514,6 +1555,7 @@ async fn deferred_executor_spawn_agent_inherits_ready_step_environments(
environment_id: REMOTE_ENVIRONMENT_ID.to_string(),
cwd: PathUri::from_abs_path(&test.config.cwd),
workspace_roots: vec![PathUri::from_abs_path(&test.config.cwd)],
config: EnvironmentConfigState::FromThread,
};
let expected_environments = vec![remote_selection, local(test.config.cwd.clone())];
let mut created_threads = test.thread_manager.subscribe_thread_created();
@@ -1646,6 +1688,7 @@ async fn deferred_executor_guardian_uses_newly_ready_step_environment() -> Resul
environment_id: REMOTE_ENVIRONMENT_ID.to_string(),
cwd: PathUri::from_abs_path(&remote_cwd),
workspace_roots: vec![PathUri::from_abs_path(&remote_cwd)],
config: EnvironmentConfigState::FromThread,
};
let permission_profile = PermissionProfile::from_runtime_permissions(
&FileSystemSandboxPolicy::restricted(vec![
@@ -2142,6 +2185,7 @@ async fn exec_command_routes_to_selected_remote_environment() -> Result<()> {
environment_id: REMOTE_ENVIRONMENT_ID.to_string(),
cwd: PathUri::from_abs_path(&remote_cwd),
workspace_roots: vec![PathUri::from_abs_path(&remote_cwd)],
config: EnvironmentConfigState::FromThread,
};
let multi_env_output = exec_command_routing_output(
&test,
@@ -2280,11 +2324,13 @@ async fn remote_exec_materializes_target_roots_before_sandbox_selection() -> Res
environment_id: LOCAL_ENVIRONMENT_ID.to_string(),
cwd: PathUri::from_abs_path(&local_cwd.path().abs()),
workspace_roots: Vec::new(),
config: EnvironmentConfigState::FromThread,
},
TurnEnvironmentSelection {
environment_id: REMOTE_ENVIRONMENT_ID.to_string(),
cwd: remote_cwd_uri.clone(),
workspace_roots: vec![remote_cwd_uri.clone()],
config: EnvironmentConfigState::FromThread,
},
],
)),
@@ -2453,6 +2499,7 @@ async fn remote_request_permissions_grant_unblocks_later_remote_exec() -> Result
environment_id: REMOTE_ENVIRONMENT_ID.to_string(),
cwd: PathUri::from_abs_path(&remote_cwd),
workspace_roots: vec![PathUri::from_abs_path(&remote_cwd)],
config: EnvironmentConfigState::FromThread,
},
],
AskForApproval::OnRequest,
@@ -2595,6 +2642,7 @@ async fn apply_patch_freeform_routes_to_selected_remote_environment() -> Result<
environment_id: REMOTE_ENVIRONMENT_ID.to_string(),
cwd: PathUri::from_abs_path(&remote_cwd),
workspace_roots: vec![PathUri::from_abs_path(&remote_cwd)],
config: EnvironmentConfigState::FromThread,
},
]),
)
@@ -2679,6 +2727,7 @@ async fn apply_patch_approvals_are_remembered_per_environment() -> Result<()> {
environment_id: REMOTE_ENVIRONMENT_ID.to_string(),
cwd: PathUri::from_abs_path(&remote_cwd),
workspace_roots: vec![PathUri::from_abs_path(&remote_cwd)],
config: EnvironmentConfigState::FromThread,
},
];
let local_patch = format!(
@@ -2881,6 +2930,7 @@ async fn apply_patch_intercepted_exec_command_routes_to_selected_remote_environm
environment_id: REMOTE_ENVIRONMENT_ID.to_string(),
cwd: PathUri::from_abs_path(&remote_cwd),
workspace_roots: vec![PathUri::from_abs_path(&remote_cwd)],
config: EnvironmentConfigState::FromThread,
},
]),
)
@@ -17,6 +17,7 @@ use codex_protocol::permissions::FileSystemSandboxPolicy;
use codex_protocol::permissions::FileSystemSpecialPath;
use codex_protocol::permissions::NetworkSandboxPolicy;
use codex_protocol::protocol::AskForApproval;
use codex_protocol::protocol::EnvironmentConfigState;
use codex_protocol::protocol::EventMsg;
use codex_protocol::protocol::Op;
use codex_protocol::protocol::ReviewDecision;
@@ -704,6 +705,7 @@ timeout = 900
PathUri::parse("file:///D:/other-workspace")
.expect("valid Windows workspace root"),
],
config: EnvironmentConfigState::FromThread,
}],
)
}),
+2
View File
@@ -29,6 +29,7 @@ use codex_protocol::permissions::FileSystemSandboxEntry;
use codex_protocol::permissions::FileSystemSandboxPolicy;
use codex_protocol::permissions::NetworkSandboxPolicy;
use codex_protocol::protocol::AskForApproval;
use codex_protocol::protocol::EnvironmentConfigState;
use codex_protocol::protocol::EventMsg;
use codex_protocol::protocol::ThreadSettingsOverrides;
use codex_protocol::protocol::TurnEnvironmentSelection;
@@ -748,6 +749,7 @@ async fn view_image_routes_to_selected_remote_environment() -> anyhow::Result<()
environment_id: REMOTE_ENVIRONMENT_ID.to_string(),
cwd: remote_cwd_uri.clone(),
workspace_roots: vec![remote_cwd_uri],
config: EnvironmentConfigState::FromThread,
};
let relative_call_id = "call-view-image-relative-multi-env";
let absolute_call_id = "call-view-image-absolute-multi-env";
@@ -37,6 +37,7 @@ use codex_otel::THREAD_SKILLS_KEPT_TOTAL_METRIC;
use codex_otel::THREAD_SKILLS_TRUNCATED_METRIC;
use codex_protocol::capabilities::CapabilityRootLocation;
use codex_protocol::capabilities::SelectedCapabilityRoot;
use codex_protocol::protocol::EnvironmentConfigState;
use codex_protocol::protocol::Event;
use codex_protocol::protocol::SKILLS_INSTRUCTIONS_CLOSE_TAG;
use codex_protocol::protocol::SKILLS_INSTRUCTIONS_OPEN_TAG;
@@ -1031,6 +1032,7 @@ async fn selected_executor_catalog_follows_step_availability_and_reuses_its_cach
environment_id: "turn-env".to_string(),
cwd: PathUri::parse("file:///workspace").expect("cwd URI"),
workspace_roots: Vec::new(),
config: EnvironmentConfigState::FromThread,
};
let available_sections = registry.context_contributors()[0]
.contribute_world_state(WorldStateContributionInput {
@@ -1752,6 +1754,7 @@ async fn root_qualified_locator_selects_only_the_matching_executor_skill() -> Te
environment_id: "env-1".to_string(),
cwd: PathUri::parse("file:///workspace").expect("cwd URI"),
workspace_roots: Vec::new(),
config: EnvironmentConfigState::FromThread,
}],
ready_selected_capability_roots: &selected_roots,
executor_capability_discovery: None,
+11
View File
@@ -1,5 +1,16 @@
use crate::capabilities::SelectedCapabilityRoot;
/// Configuration supplied for a thread's selected environment.
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum EnvironmentConfigState {
/// Preserve the existing thread-derived environment configuration.
FromThread,
/// The owner will supply environment configuration later.
Pending,
/// The owner supplied configuration for this environment attachment.
Ready(EnvironmentConfig),
}
/// Resolved configuration supplied by the owner of a thread/environment attachment.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct EnvironmentConfig {
+2
View File
@@ -91,6 +91,7 @@ pub use crate::approvals::NetworkApprovalProtocol;
pub use crate::approvals::NetworkPolicyAmendment;
pub use crate::approvals::NetworkPolicyRuleAction;
pub use crate::environment::EnvironmentConfig;
pub use crate::environment::EnvironmentConfigState;
pub use crate::legacy_events::HasLegacyEvent;
pub use crate::permissions::FileSystemAccessMode;
pub use crate::permissions::FileSystemPath;
@@ -146,6 +147,7 @@ pub struct TurnEnvironmentSelection {
pub environment_id: String,
pub cwd: PathUri,
pub workspace_roots: Vec<PathUri>,
pub config: EnvironmentConfigState,
}
#[derive(Debug, Clone, PartialEq)]