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
@@ -10,6 +10,7 @@ use crate::ExecServerClientConnectOptions;
use crate::ExecServerError;
use crate::client::LazyRemoteExecServerClient;
use axum::extract::ws::WebSocket;
use codex_config::ScopedSkillsConfig;
use codex_http_client::HttpClientFactory;
impl EnvironmentManager {
@@ -29,7 +30,9 @@ impl EnvironmentManager {
let client =
LazyRemoteExecServerClient::from_connected(client, http_client_factory.clone());
let environment = Arc::new(Environment::remote_with_client(
client, /*local_runtime_paths*/ None,
client,
/*local_runtime_paths*/ None,
ScopedSkillsConfig::default(),
));
Ok(Self {
default_environment: Some(environment_id.clone()),
@@ -1,6 +1,7 @@
use std::collections::HashMap;
use std::time::Duration;
use codex_config::ScopedSkillsConfig;
use codex_http_client::HttpClientFactory;
use codex_http_client::OutboundProxyPolicy;
use futures::SinkExt;
@@ -59,6 +60,7 @@ fn trusted_headers_require_tls_for_non_loopback_destinations() {
"customer-session".to_string(),
)]),
},
ScopedSkillsConfig::default(),
)
.expect_err("trusted headers must not be sent over an insecure remote connection");
@@ -86,6 +88,7 @@ fn duplicate_case_insensitive_websocket_headers_fail_before_registration() {
("x-session-id".to_string(), "second-session".to_string()),
]),
},
ScopedSkillsConfig::default(),
)
.expect_err("duplicate header names must fail regardless of case");
@@ -110,6 +113,7 @@ fn invalid_websocket_header_names_fail_before_registration() {
connect_timeout: None,
http_headers: HashMap::from([("bad header".to_string(), "value".to_string())]),
},
ScopedSkillsConfig::default(),
)
.expect_err("invalid header name should fail");
@@ -137,6 +141,7 @@ fn invalid_websocket_header_values_fail_before_registration() {
"customer\nspoofed".to_string(),
)]),
},
ScopedSkillsConfig::default(),
)
.expect_err("invalid header value should fail");
@@ -162,6 +167,7 @@ fn websocket_controlled_headers_fail_before_registration() {
connect_timeout: None,
http_headers: HashMap::from([(header.to_string(), "overridden".to_string())]),
},
ScopedSkillsConfig::default(),
)
.expect_err("connection-controlled header should fail");
@@ -228,6 +234,7 @@ async fn trusted_headers_are_sent_on_initial_websocket_and_session_reconnect() {
),
]),
},
ScopedSkillsConfig::default(),
)
.expect("environment with routing headers should register");