mirror of
https://github.com/openai/codex.git
synced 2026-11-09 00:39:32 +00:00
## Why Model requirements must be evaluated against the permissions that will apply after a settings update. Validating against the current profile can reject a valid model-and-permission update or allow a later permission-only update to invalidate the selected model. ## What changed - Group model, reasoning, service-tier, personality, and approval inputs into `StepSettings`, with a single apply-and-validate path. - Apply permission and environment changes before validating step settings, so auto-review requirements use the proposed effective permissions. - Revalidate existing step settings when permission or environment changes alter their constraints, and commit the update only when the complete candidate is valid. ## Testing Added unit and integration coverage for atomic model/permission updates, permission-only revalidation, environment-derived constraints, and both standalone and turn-start settings operations. GitOrigin-RevId: f50dfbd300484a09acc51ee4607278f300ca1cd9
194 lines
7.5 KiB
Rust
194 lines
7.5 KiB
Rust
use std::collections::HashSet;
|
|
|
|
use codex_exec_server::LOCAL_ENVIRONMENT_ID;
|
|
use codex_exec_server::MAX_SELECTED_CAPABILITY_ROOTS;
|
|
use codex_exec_server::SelectedCapabilityRootsStatus;
|
|
use codex_execpolicy::Policy;
|
|
use codex_protocol::capabilities::CapabilityRootLocation;
|
|
use codex_protocol::error::CodexErr;
|
|
use codex_protocol::error::Result as CodexResult;
|
|
use codex_protocol::protocol::EnvironmentConfig;
|
|
use codex_protocol::protocol::EnvironmentConfigState;
|
|
use codex_protocol::protocol::TurnEnvironmentSelection;
|
|
|
|
use crate::config::ConstraintError;
|
|
use crate::config::ConstraintResult;
|
|
use crate::config::NetworkProxySpec;
|
|
use crate::session::session::Session;
|
|
use crate::session::session::SessionConfiguration;
|
|
use crate::session::session::SessionSettingsUpdate;
|
|
|
|
pub(super) fn validate_environment_selections(
|
|
selections: &[TurnEnvironmentSelection],
|
|
) -> ConstraintResult<()> {
|
|
for selection in selections {
|
|
match &selection.config {
|
|
EnvironmentConfigState::FromThread
|
|
| EnvironmentConfigState::Pending
|
|
| EnvironmentConfigState::Failed(_) => {}
|
|
EnvironmentConfigState::Ready(config) => {
|
|
validate_environment_config(selection, config).map_err(|error| {
|
|
ConstraintError::InvalidValue {
|
|
field_name: "environments",
|
|
candidate: "environment configuration".to_string(),
|
|
allowed: format!("valid environment configuration ({error})"),
|
|
requirement_source: codex_config::RequirementSource::Unknown,
|
|
}
|
|
})?;
|
|
}
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
fn validate_environment_config(
|
|
selection: &TurnEnvironmentSelection,
|
|
config: &EnvironmentConfig,
|
|
) -> CodexResult<()> {
|
|
if let Some(policy) = config.network_policy.as_ref() {
|
|
if selection.environment_id == LOCAL_ENVIRONMENT_ID {
|
|
return Err(CodexErr::InvalidRequest(
|
|
"attachment-owned network policy requires a remote executor".to_string(),
|
|
));
|
|
}
|
|
if config
|
|
.exec_policy
|
|
.as_ref()
|
|
.is_some_and(|policy| !policy.as_ref().network_rules().is_empty())
|
|
{
|
|
return Err(CodexErr::InvalidRequest(
|
|
"environment network restrictions must use network_policy".to_string(),
|
|
));
|
|
}
|
|
// Validate owner policy on its own; controller compatibility is checked at execution.
|
|
NetworkProxySpec::for_environment(
|
|
/*controller*/ None,
|
|
policy,
|
|
config.permission_profile.permission_profile(),
|
|
&Policy::empty(),
|
|
)
|
|
.map_err(|error| {
|
|
CodexErr::InvalidRequest(format!("invalid environment network policy: {error}"))
|
|
})?;
|
|
}
|
|
if config.selected_capability_roots.len() > MAX_SELECTED_CAPABILITY_ROOTS {
|
|
return Err(CodexErr::InvalidRequest(format!(
|
|
"environment readiness contains more than {MAX_SELECTED_CAPABILITY_ROOTS} selected capability roots"
|
|
)));
|
|
}
|
|
if config
|
|
.exec_policy
|
|
.as_ref()
|
|
.is_some_and(|policy| !policy.as_ref().get_allowed_prefixes().is_empty())
|
|
{
|
|
return Err(CodexErr::InvalidRequest(
|
|
"environment command policy cannot contain allow rules".to_string(),
|
|
));
|
|
}
|
|
|
|
let mut root_ids = HashSet::with_capacity(config.selected_capability_roots.len());
|
|
for root in &config.selected_capability_roots {
|
|
let CapabilityRootLocation::Environment { environment_id, .. } = &root.location;
|
|
if root.id.trim().is_empty()
|
|
|| environment_id != &selection.environment_id
|
|
|| !root_ids.insert(root.id.as_str())
|
|
{
|
|
return Err(CodexErr::InvalidRequest(format!(
|
|
"selected capability roots must have unique non-empty IDs and belong to environment `{}`",
|
|
selection.environment_id
|
|
)));
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
impl Session {
|
|
pub(super) fn apply_session_settings(
|
|
&self,
|
|
current: &SessionConfiguration,
|
|
updates: &SessionSettingsUpdate,
|
|
) -> ConstraintResult<SessionConfiguration> {
|
|
let current_environments = self.services.turn_environments.selections();
|
|
if let Some(environments) = &updates.environments
|
|
&& let Some(environment) = environments.environments.iter().find(|environment| {
|
|
environment.config == EnvironmentConfigState::FromThread
|
|
&& current_environments.iter().any(|current| {
|
|
current.environment_id == environment.environment_id
|
|
&& current.config != EnvironmentConfigState::FromThread
|
|
})
|
|
})
|
|
{
|
|
return Err(ConstraintError::InvalidValue {
|
|
field_name: "environments",
|
|
candidate: environment.environment_id.clone(),
|
|
allowed: "owner-provided environment configuration".to_string(),
|
|
requirement_source: codex_config::RequirementSource::Unknown,
|
|
});
|
|
}
|
|
|
|
current.apply(updates, ¤t_environments)
|
|
}
|
|
|
|
pub(crate) async fn environment_ready(
|
|
&self,
|
|
selection: &TurnEnvironmentSelection,
|
|
config: EnvironmentConfig,
|
|
) -> CodexResult<()> {
|
|
validate_environment_config(selection, &config)?;
|
|
self.update_environment_configuration(selection, EnvironmentConfigState::Ready(config))
|
|
.await
|
|
}
|
|
|
|
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;
|
|
if matches!(environment.config, EnvironmentConfigState::Ready(_)) {
|
|
state
|
|
.session_configuration
|
|
.validate(&environments)
|
|
.map_err(|error| CodexErr::InvalidRequest(error.to_string()))?;
|
|
}
|
|
|
|
// 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.inferred_environment_config(),
|
|
);
|
|
Ok(())
|
|
}
|
|
|
|
/// Combines this session's persisted roots with ready environment attachments.
|
|
pub(crate) fn inspect_selected_capability_roots(&self) -> SelectedCapabilityRootsStatus {
|
|
self.services
|
|
.turn_environments
|
|
.inspect_selected_capability_roots(&self.services.selected_capability_roots)
|
|
}
|
|
}
|