Enforce required environment skills before model inference (#51157)

## What changed

- Accept per-environment `skills.required` lists through `environment/add` and `environments.toml`.
- Before inference, require each named skill to be enabled and supplied by the selected environment. Missing skills fail the turn before a model request, including when environment setup fails.
- Stop applying an environment's requirements after it is deselected. Exempt isolated Guardian reviewers, which have no skill catalog.

## Testing

Add tests for available, missing, disabled, and wrong-environment skills, failed environment setup, recovery after deselection, and propagation of TOML requirements to the correct environment.

GitOrigin-RevId: 302ced4774697d2f414f745fe0535e71f747eda2
This commit is contained in:
Adam Perry @ OpenAI
2026-10-05 18:54:17 +00:00
committed by copyberry
parent c9253c4977
commit 42312d4ff4
33 changed files with 615 additions and 78 deletions
@@ -7,6 +7,7 @@ use std::time::Duration;
use anyhow::Result;
use bytes::Bytes;
use codex_config::ScopedSkillsConfig;
use futures::future::BoxFuture;
use pretty_assertions::assert_eq;
use tokio::net::TcpListener;
@@ -321,8 +322,11 @@ async fn registration_snapshot_changes_only_after_same_key_replacement_is_instal
};
let new = Executor::start_with_identity(validator.clone(), old.target.identity.clone()).await?;
let (client, registry) = old.client()?;
let environment =
crate::Environment::remote_with_client(client.clone(), /*local_runtime_paths*/ None);
let environment = crate::Environment::remote_with_client(
client.clone(),
/*local_runtime_paths*/ None,
ScopedSkillsConfig::default(),
);
assert_eq!(environment.cached_executor_registration_id(), None);
let original = client.get().await?;
assert_eq!(
@@ -364,8 +368,11 @@ async fn registration_renewal_recovers_the_session_and_running_process() -> Resu
)
.await?;
let (client, registry) = old.client()?;
let environment =
crate::Environment::remote_with_client(client.clone(), /*local_runtime_paths*/ None);
let environment = crate::Environment::remote_with_client(
client.clone(),
/*local_runtime_paths*/ None,
ScopedSkillsConfig::default(),
);
let original = client.get().await?;
let session_id = original.session_id();
let process =
@@ -450,8 +457,11 @@ async fn same_key_replacement_cannot_resume_a_missing_session() -> Result<()> {
let replacement =
Executor::start_with_identity(Validator::default(), old.target.identity.clone()).await?;
let (client, registry) = old.client()?;
let environment =
crate::Environment::remote_with_client(client.clone(), /*local_runtime_paths*/ None);
let environment = crate::Environment::remote_with_client(
client.clone(),
/*local_runtime_paths*/ None,
ScopedSkillsConfig::default(),
);
let original = client.get().await?;
let process = original
.register_session(&ProcessId::from("old-process"))
@@ -488,8 +498,11 @@ async fn stale_refresh_lookup_does_not_retire_a_renewed_session() -> Result<()>
)
.await?;
let (client, registry) = old.client()?;
let environment =
crate::Environment::remote_with_client(client.clone(), /*local_runtime_paths*/ None);
let environment = crate::Environment::remote_with_client(
client.clone(),
/*local_runtime_paths*/ None,
ScopedSkillsConfig::default(),
);
let original = client.get().await?;
let release = registry.block_next_lookup();
let refreshing = client.refresh_connection();