mirror of
https://github.com/openai/codex.git
synced 2026-11-15 21:50:46 +00:00
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:
committed by
copyberry
parent
c9253c4977
commit
42312d4ff4
@@ -7,6 +7,7 @@ use std::sync::Mutex;
|
||||
use std::sync::RwLock;
|
||||
|
||||
use arc_swap::ArcSwapOption;
|
||||
use codex_config::ScopedSkillsConfig;
|
||||
use codex_http_client::HttpClientFactory;
|
||||
use codex_http_client::OutboundProxyPolicy;
|
||||
use codex_protocol::capabilities::CapabilityRootLocation;
|
||||
@@ -33,6 +34,7 @@ use crate::environment_config::read_environment_config;
|
||||
use crate::environment_provider::DefaultEnvironmentProvider;
|
||||
use crate::environment_provider::EnvironmentDefault;
|
||||
use crate::environment_provider::EnvironmentProvider;
|
||||
use crate::environment_provider::EnvironmentProviderEntry;
|
||||
use crate::environment_provider::EnvironmentProviderSnapshot;
|
||||
use crate::environment_provider::normalize_exec_server_url;
|
||||
use crate::environment_toml::environment_provider_from_codex_home;
|
||||
@@ -245,6 +247,7 @@ impl EnvironmentManager {
|
||||
},
|
||||
manager.local_runtime_paths.clone(),
|
||||
manager.http_client_factory.clone(),
|
||||
ScopedSkillsConfig::default(),
|
||||
));
|
||||
manager.insert_environment(REMOTE_ENVIRONMENT_ID.to_string(), environment)?;
|
||||
Ok(manager)
|
||||
@@ -299,7 +302,12 @@ impl EnvironmentManager {
|
||||
} else {
|
||||
None
|
||||
};
|
||||
for (id, transport) in environments {
|
||||
for EnvironmentProviderEntry {
|
||||
id,
|
||||
transport,
|
||||
skills,
|
||||
} in environments
|
||||
{
|
||||
if id.is_empty() {
|
||||
return Err(ExecServerError::Protocol(
|
||||
"environment id cannot be empty".to_string(),
|
||||
@@ -314,6 +322,7 @@ impl EnvironmentManager {
|
||||
transport,
|
||||
/*local_runtime_paths*/ None,
|
||||
http_client_factory.clone(),
|
||||
skills,
|
||||
);
|
||||
if environment_map
|
||||
.insert(id.clone(), Arc::new(environment))
|
||||
@@ -477,6 +486,7 @@ impl EnvironmentManager {
|
||||
connect_timeout,
|
||||
http_headers: HashMap::new(),
|
||||
},
|
||||
ScopedSkillsConfig::default(),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -489,15 +499,17 @@ impl EnvironmentManager {
|
||||
&self,
|
||||
environment_id: String,
|
||||
options: RemoteEnvironmentOptions,
|
||||
skills: ScopedSkillsConfig,
|
||||
) -> Result<(), ExecServerError> {
|
||||
validate_environment_id(&environment_id)?;
|
||||
let transport = options.into_transport_params()?;
|
||||
let environment = Arc::new(Environment::remote_with_transport(
|
||||
let environment = Environment::remote_with_transport(
|
||||
transport,
|
||||
self.local_runtime_paths.clone(),
|
||||
self.http_client_factory.clone(),
|
||||
));
|
||||
self.insert_environment(environment_id, environment)
|
||||
skills,
|
||||
);
|
||||
self.insert_environment(environment_id, Arc::new(environment))
|
||||
}
|
||||
|
||||
/// Returns the stable environment for an ID, creating it as pending when absent.
|
||||
@@ -538,6 +550,7 @@ impl EnvironmentManager {
|
||||
})),
|
||||
self.local_runtime_paths.clone(),
|
||||
self.http_client_factory.clone(),
|
||||
ScopedSkillsConfig::default(),
|
||||
);
|
||||
environment.provisioning_status_tx = Some(provisioning_status_tx);
|
||||
Ok(environment)
|
||||
@@ -665,6 +678,7 @@ fn optional_environment_value(name: &str) -> Option<String> {
|
||||
/// paths used by filesystem helpers.
|
||||
#[derive(Clone)]
|
||||
pub struct Environment {
|
||||
skills: ScopedSkillsConfig,
|
||||
remote_client: Option<LazyRemoteExecServerClient>,
|
||||
ready_info: Arc<ArcSwapOption<EnvironmentReadyInfo>>,
|
||||
// No sender means an ordinary environment. A provisioned environment retains a sender whose
|
||||
@@ -683,6 +697,7 @@ impl Environment {
|
||||
pub fn default_for_tests() -> Self {
|
||||
Self {
|
||||
remote_client: None,
|
||||
skills: ScopedSkillsConfig::default(),
|
||||
ready_info: Arc::new(ArcSwapOption::empty()),
|
||||
provisioning_status_tx: None,
|
||||
startup_task: Arc::new(Mutex::new(None)),
|
||||
@@ -748,6 +763,7 @@ impl Environment {
|
||||
),
|
||||
local_runtime_paths,
|
||||
http_client_factory,
|
||||
ScopedSkillsConfig::default(),
|
||||
),
|
||||
None => match local_runtime_paths {
|
||||
Some(local_runtime_paths) => Self::local(local_runtime_paths, http_client_factory),
|
||||
@@ -762,6 +778,7 @@ impl Environment {
|
||||
) -> Self {
|
||||
Self {
|
||||
remote_client: None,
|
||||
skills: ScopedSkillsConfig::default(),
|
||||
ready_info: Arc::new(ArcSwapOption::empty()),
|
||||
provisioning_status_tx: None,
|
||||
startup_task: Arc::new(Mutex::new(None)),
|
||||
@@ -780,14 +797,16 @@ impl Environment {
|
||||
remote_transport: ExecServerTransportParams,
|
||||
local_runtime_paths: Option<ExecServerRuntimeOptions>,
|
||||
http_client_factory: HttpClientFactory,
|
||||
skills: ScopedSkillsConfig,
|
||||
) -> Self {
|
||||
let client = LazyRemoteExecServerClient::new(remote_transport, http_client_factory);
|
||||
Self::remote_with_client(client, local_runtime_paths)
|
||||
Self::remote_with_client(client, local_runtime_paths, skills)
|
||||
}
|
||||
|
||||
pub(crate) fn remote_with_client(
|
||||
client: LazyRemoteExecServerClient,
|
||||
local_runtime_paths: Option<ExecServerRuntimeOptions>,
|
||||
skills: ScopedSkillsConfig,
|
||||
) -> Self {
|
||||
let exec_backend: Arc<dyn ExecBackend> = Arc::new(RemoteProcess::new(client.clone()));
|
||||
let filesystem: Arc<dyn ExecutorFileSystem> =
|
||||
@@ -795,6 +814,7 @@ impl Environment {
|
||||
|
||||
Self {
|
||||
remote_client: Some(client.clone()),
|
||||
skills,
|
||||
ready_info: Arc::new(ArcSwapOption::empty()),
|
||||
provisioning_status_tx: None,
|
||||
startup_task: Arc::new(Mutex::new(None)),
|
||||
@@ -805,6 +825,11 @@ impl Environment {
|
||||
}
|
||||
}
|
||||
|
||||
/// Skill names required whenever this registration is selected by a turn.
|
||||
pub fn required_skills(&self) -> &[String] {
|
||||
&self.skills.required
|
||||
}
|
||||
|
||||
pub fn is_remote(&self) -> bool {
|
||||
self.remote_client.is_some()
|
||||
}
|
||||
@@ -1177,7 +1202,9 @@ mod tests {
|
||||
use crate::client_api::ExecServerTransportParams;
|
||||
use crate::client_api::StdioExecServerCommand;
|
||||
use crate::environment_provider::EnvironmentDefault;
|
||||
use crate::environment_provider::EnvironmentProviderEntry;
|
||||
use crate::environment_provider::EnvironmentProviderSnapshot;
|
||||
use codex_config::ScopedSkillsConfig;
|
||||
use codex_http_client::HttpClientFactory;
|
||||
use codex_http_client::OutboundProxyPolicy;
|
||||
use codex_utils_path_uri::PathUri;
|
||||
@@ -1354,10 +1381,11 @@ mod tests {
|
||||
#[tokio::test]
|
||||
async fn environment_manager_builds_from_snapshot() {
|
||||
let snapshot = EnvironmentProviderSnapshot {
|
||||
environments: vec![(
|
||||
REMOTE_ENVIRONMENT_ID.to_string(),
|
||||
prepared_websocket_environment(),
|
||||
)],
|
||||
environments: vec![EnvironmentProviderEntry {
|
||||
id: REMOTE_ENVIRONMENT_ID.to_string(),
|
||||
transport: prepared_websocket_environment(),
|
||||
skills: Default::default(),
|
||||
}],
|
||||
default: EnvironmentDefault::EnvironmentId(REMOTE_ENVIRONMENT_ID.to_string()),
|
||||
include_local: false,
|
||||
};
|
||||
@@ -1385,7 +1413,11 @@ mod tests {
|
||||
#[tokio::test]
|
||||
async fn environment_manager_rejects_empty_environment_id() {
|
||||
let snapshot = EnvironmentProviderSnapshot {
|
||||
environments: vec![("".to_string(), prepared_websocket_environment())],
|
||||
environments: vec![EnvironmentProviderEntry {
|
||||
id: "".to_string(),
|
||||
transport: prepared_websocket_environment(),
|
||||
skills: Default::default(),
|
||||
}],
|
||||
default: EnvironmentDefault::Disabled,
|
||||
include_local: false,
|
||||
};
|
||||
@@ -1405,10 +1437,11 @@ mod tests {
|
||||
#[tokio::test]
|
||||
async fn environment_manager_rejects_provider_supplied_local_environment() {
|
||||
let snapshot = EnvironmentProviderSnapshot {
|
||||
environments: vec![(
|
||||
LOCAL_ENVIRONMENT_ID.to_string(),
|
||||
prepared_websocket_environment(),
|
||||
)],
|
||||
environments: vec![EnvironmentProviderEntry {
|
||||
id: LOCAL_ENVIRONMENT_ID.to_string(),
|
||||
transport: prepared_websocket_environment(),
|
||||
skills: Default::default(),
|
||||
}],
|
||||
default: EnvironmentDefault::Disabled,
|
||||
include_local: false,
|
||||
};
|
||||
@@ -1428,7 +1461,11 @@ mod tests {
|
||||
#[tokio::test]
|
||||
async fn environment_manager_uses_explicit_provider_default() {
|
||||
let snapshot = EnvironmentProviderSnapshot {
|
||||
environments: vec![("devbox".to_string(), prepared_websocket_environment())],
|
||||
environments: vec![EnvironmentProviderEntry {
|
||||
id: "devbox".to_string(),
|
||||
transport: prepared_websocket_environment(),
|
||||
skills: Default::default(),
|
||||
}],
|
||||
default: EnvironmentDefault::EnvironmentId("devbox".to_string()),
|
||||
include_local: true,
|
||||
};
|
||||
@@ -1450,7 +1487,11 @@ mod tests {
|
||||
#[tokio::test]
|
||||
async fn environment_manager_disables_provider_default() {
|
||||
let snapshot = EnvironmentProviderSnapshot {
|
||||
environments: vec![("devbox".to_string(), prepared_websocket_environment())],
|
||||
environments: vec![EnvironmentProviderEntry {
|
||||
id: "devbox".to_string(),
|
||||
transport: prepared_websocket_environment(),
|
||||
skills: Default::default(),
|
||||
}],
|
||||
default: EnvironmentDefault::Disabled,
|
||||
include_local: true,
|
||||
};
|
||||
@@ -1474,7 +1515,11 @@ mod tests {
|
||||
#[tokio::test]
|
||||
async fn environment_manager_rejects_unknown_provider_default() {
|
||||
let snapshot = EnvironmentProviderSnapshot {
|
||||
environments: vec![("devbox".to_string(), prepared_websocket_environment())],
|
||||
environments: vec![EnvironmentProviderEntry {
|
||||
id: "devbox".to_string(),
|
||||
transport: prepared_websocket_environment(),
|
||||
skills: Default::default(),
|
||||
}],
|
||||
default: EnvironmentDefault::EnvironmentId("missing".to_string()),
|
||||
include_local: true,
|
||||
};
|
||||
@@ -1650,6 +1695,7 @@ mod tests {
|
||||
},
|
||||
/*local_runtime_paths*/ None,
|
||||
legacy_http_client_factory(),
|
||||
ScopedSkillsConfig::default(),
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
@@ -1672,7 +1718,11 @@ mod tests {
|
||||
};
|
||||
let manager = EnvironmentManager::from_snapshot(
|
||||
EnvironmentProviderSnapshot {
|
||||
environments: vec![("stdio".to_string(), transport)],
|
||||
environments: vec![EnvironmentProviderEntry {
|
||||
id: "stdio".to_string(),
|
||||
transport,
|
||||
skills: Default::default(),
|
||||
}],
|
||||
default: EnvironmentDefault::Disabled,
|
||||
include_local: false,
|
||||
},
|
||||
@@ -1703,7 +1753,11 @@ mod tests {
|
||||
};
|
||||
let manager = EnvironmentManager::from_snapshot(
|
||||
EnvironmentProviderSnapshot {
|
||||
environments: vec![("stdio".to_string(), transport)],
|
||||
environments: vec![EnvironmentProviderEntry {
|
||||
id: "stdio".to_string(),
|
||||
transport,
|
||||
skills: Default::default(),
|
||||
}],
|
||||
default: EnvironmentDefault::Disabled,
|
||||
include_local: false,
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user