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 authored and copyberry committed 2026-08-15 01:39:04 +00:00
1 parent eb147c0db3
commit 2ca575026c
7 files changed
+378 -85

No files matched your search

+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(())
}