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
@@ -2587,6 +2587,7 @@ mod tests {
let environment_add = ClientRequest::EnvironmentAdd {
request_id: request_id(),
params: v2::EnvironmentAddParams {
skills: None,
auth_bearer_token: None,
environment_id: "remote-a".to_string(),
exec_server_url: "ws://127.0.0.1:8765".to_string(),
@@ -3841,6 +3842,7 @@ mod tests {
let request = ClientRequest::EnvironmentAdd {
request_id: RequestId::Integer(9),
params: v2::EnvironmentAddParams {
skills: None,
auth_bearer_token: Some("private-executor-token".into()),
environment_id: "remote-a".to_string(),
exec_server_url: "ws://127.0.0.1:8765".to_string(),
@@ -3856,7 +3858,8 @@ mod tests {
"environmentId": "remote-a",
"execServerUrl": "ws://127.0.0.1:8765",
"connectTimeoutMs": 300000,
"authBearerToken": "private-executor-token"
"authBearerToken": "private-executor-token",
"skills": null
}
}),
serde_json::to_value(&request)?,
@@ -4381,6 +4384,7 @@ mod tests {
let request = ClientRequest::EnvironmentAdd {
request_id: RequestId::Integer(1),
params: v2::EnvironmentAddParams {
skills: None,
auth_bearer_token: None,
environment_id: "remote-a".to_string(),
exec_server_url: "ws://127.0.0.1:8765".to_string(),
@@ -47,6 +47,9 @@ pub struct EnvironmentAddParams {
#[ts(type = "number | null")]
#[ts(optional = nullable)]
pub connect_timeout_ms: Option<u64>,
/// Required skills supplied by this environment, checked before model inference.
#[ts(optional = nullable)]
pub skills: Option<EnvironmentSkillsParams>,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
@@ -54,6 +57,15 @@ pub struct EnvironmentAddParams {
#[ts(export_to = "v2/")]
pub struct EnvironmentAddResponse {}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
pub struct EnvironmentSkillsParams {
/// Exact catalog names that must be available from this environment.
#[ts(optional = nullable)]
pub required: Option<Vec<String>>,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(rename_all = "camelCase", export_to = "v2/")]