Support pending environment attachment configuration (#38684)

## Why

An environment connection can be available before its owner has supplied the
configuration for a particular thread attachment. Threads need to start without
blocking while ensuring turns do not use that attachment prematurely.

## What changed

- Accept `Pending` environment configuration and resolve each attachment only
  after both its shared executor connection and owner configuration are ready.
- Add a `Failed` configuration state and `environment_failed` callback so an
  owner can fail one thread's attachment without affecting other threads.
- Keep pending and failed attachments out of capability-root inspection and turn
  environments, and allow failed attachments to recover through a ready update.
- Apply owner configuration before waking a waiting turn so its permission
  profile, login-shell policy, capability roots, and tools are immediately
  consistent.

## Testing

Add an integration test covering non-blocking thread startup, independent ready
and failed callbacks, waiting-turn resumption, installed capability and tool
configuration, and recovery from failure.

GitOrigin-RevId: d587e2025d584c867d782d470b18bf5a1a27b76c
This commit is contained in:
sayan-oai
2026-08-15 01:39:04 +00:00
committed by copyberry
parent eb147c0db3
commit 2ca575026c
7 changed files with 378 additions and 85 deletions
+41 -12
View File
@@ -19,12 +19,9 @@ pub(super) fn validate_environment_selections(
) -> 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::FromThread
| EnvironmentConfigState::Pending
| EnvironmentConfigState::Failed(_) => {}
EnvironmentConfigState::Ready(config) => {
validate_environment_config(selection, config)?;
}
@@ -74,14 +71,46 @@ impl Session {
config: EnvironmentConfig,
) -> CodexResult<()> {
validate_environment_config(selection, &config)?;
self.update_environment_configuration(selection, EnvironmentConfigState::Ready(config))
.await
}
// grab session lock so installation can't race w/ thread settings updates
let _state = self.state.lock().await;
self.services
.turn_environments
.environment_ready(selection, config)?;
// mark mcp runtime for refresh because available capabilities could've changed
pub(crate) async fn environment_failed(
&self,
selection: &TurnEnvironmentSelection,
error: String,
) -> CodexResult<()> {
self.update_environment_configuration(selection, EnvironmentConfigState::Failed(error))
.await
}
async fn update_environment_configuration(
&self,
selection: &TurnEnvironmentSelection,
config: EnvironmentConfigState,
) -> CodexResult<()> {
// Serialize owner callbacks with ordinary thread settings updates.
let state = self.state.lock().await;
let mut environments = self.services.turn_environments.selections();
let Some(environment) = environments.iter_mut().find(|environment| {
environment.environment_id == selection.environment_id
&& environment.cwd == selection.cwd
&& environment.workspace_roots == selection.workspace_roots
}) else {
return Err(CodexErr::InvalidRequest(format!(
"environment `{}` is not selected on this thread with the requested workspace",
selection.environment_id
)));
};
environment.config = config;
// Invalidate MCP before installed configuration can wake a waiting turn.
self.mark_mcp_runtime_dirty();
self.services.turn_environments.update_selections(
&environments,
&state.session_configuration.turn_environment_config(),
);
Ok(())
}
+3 -3
View File
@@ -1588,6 +1588,9 @@ impl Session {
previous_permission_profile != updated_permission_profile;
let mcp_inputs_changed =
self.mcp_inputs_differ(&state.session_configuration, &updated, &updates);
if mcp_inputs_changed {
self.mark_mcp_runtime_dirty();
}
let environment_config = updated.turn_environment_config();
if let Some(environments) = &updates.environments {
self.services
@@ -1601,9 +1604,6 @@ impl Session {
state.session_configuration = updated;
let new_config = notify_config_contributors
.then(|| self.build_effective_session_config(&state.session_configuration));
if mcp_inputs_changed {
self.mark_mcp_runtime_dirty();
}
(
previous_config,
new_config,
+3 -3
View File
@@ -693,6 +693,9 @@ impl Session {
Ok(next) => {
let mcp_inputs_changed =
self.mcp_inputs_differ(&state.session_configuration, &next, &updates);
if mcp_inputs_changed {
self.mark_mcp_runtime_dirty();
}
let previous_permission_profile =
state.session_configuration.permission_profile();
let next_permission_profile = next.permission_profile();
@@ -712,9 +715,6 @@ impl Session {
.turn_environments
.update_thread_config(&environment_config);
}
if mcp_inputs_changed {
self.mark_mcp_runtime_dirty();
}
state.session_configuration = next.clone();
let new_config = notify_config_contributors
.then(|| self.build_effective_session_config(&state.session_configuration));