mirror of
https://github.com/openai/codex.git
synced 2026-11-07 20:30:33 +00:00
Preserve requested approval reviewers across policy overrides (#59272)
## Why A model requirement can force `auto_review` even when the requested reviewer is `user`. Persisting or sending that effective value back as an explicit choice loses the original selection, so removing the requirement cannot restore it on resume. ## What changed - Retain `requested_approvals_reviewer` separately from the effective reviewer in configuration and thread settings, with a fallback for older histories. - Restore the requested reviewer on resume while applying current model requirements and reviewer allowlists. - Let ordinary TUI turns, continuations, retries, and forks inherit the server's reviewer instead of echoing its effective value as an explicit request. - Preserve a worker's requested reviewer when reloading it rather than inheriting the sender's selection. ## Testing Extend resume coverage for explicit `auto_review`, removal of a model requirement, and a tightened reviewer allowlist. Update TUI assertions for omitted reviewer overrides and verify worker reload preserves its reviewer. GitOrigin-RevId: d8c9c3a7b89e55e63f0e8ae210d6a87cb5547933
This commit is contained in:
@@ -281,6 +281,7 @@ impl LocalAgentControl {
|
||||
// Preserve the worker's permissions while keeping the current wake configuration.
|
||||
config.permissions = evicted.config.permissions.clone();
|
||||
config.approvals_reviewer = evicted.config.approvals_reviewer;
|
||||
config.requested_approvals_reviewer = evicted.config.requested_approvals_reviewer;
|
||||
}
|
||||
let mut environment_selections = evicted_environments
|
||||
.as_ref()
|
||||
|
||||
@@ -1172,10 +1172,13 @@ async fn cancelled_v2_send_finishes_reload_registration() {
|
||||
.is_some()
|
||||
);
|
||||
|
||||
let mut sender_config = harness.config.clone();
|
||||
sender_config.approvals_reviewer = ApprovalsReviewer::AutoReview;
|
||||
sender_config.requested_approvals_reviewer = Some(ApprovalsReviewer::AutoReview);
|
||||
let followup_request = |message: &str| SendRequest {
|
||||
caller: parent_id,
|
||||
target: AgentTarget::Id(child_id),
|
||||
resume_config: harness.config.clone(),
|
||||
resume_config: sender_config.clone(),
|
||||
input: AgentInput::Message {
|
||||
message: AgentMessage::Plaintext(message.to_string()),
|
||||
mode: MessageDeliveryMode::TriggerTurn,
|
||||
@@ -1223,6 +1226,11 @@ async fn cancelled_v2_send_finishes_reload_registration() {
|
||||
.evicted_environments(child_id)
|
||||
.is_none()
|
||||
);
|
||||
assert_eq!(
|
||||
reloaded.config_snapshot().await.approvals_reviewer,
|
||||
ApprovalsReviewer::User,
|
||||
"reload must preserve the worker's reviewer instead of inheriting its sender's request",
|
||||
);
|
||||
// Evict and reload again with the single child slot. This also catches a leaked
|
||||
// pin or pending residency reservation left behind by the cancelled sender.
|
||||
assert_eq!(
|
||||
@@ -2459,6 +2467,7 @@ async fn spawn_agent_fork_from_paginated_parent_uses_model_context_prefix(
|
||||
service_tier: None,
|
||||
approval_policy: AskForApproval::Never,
|
||||
approvals_reviewer: ApprovalsReviewer::User,
|
||||
requested_approvals_reviewer: None,
|
||||
permission_profile: PermissionProfile::workspace_write(),
|
||||
active_permission_profile: None,
|
||||
cwd: harness.config.cwd.clone(),
|
||||
|
||||
@@ -161,6 +161,8 @@ pub struct CodexThreadSettingsOverrides {
|
||||
pub profile_workspace_roots: Option<Vec<ProfileWorkspaceRoot>>,
|
||||
pub approval_policy: Option<AskForApproval>,
|
||||
pub approvals_reviewer: Option<ApprovalsReviewer>,
|
||||
/// Retained selection when restoring an effective reviewer forced by policy.
|
||||
pub requested_approvals_reviewer: Option<ApprovalsReviewer>,
|
||||
pub sandbox_policy: Option<SandboxPolicy>,
|
||||
pub permission_profile: Option<PermissionProfile>,
|
||||
pub active_permission_profile: Option<ActivePermissionProfile>,
|
||||
@@ -877,6 +879,7 @@ impl CodexThread {
|
||||
profile_workspace_roots,
|
||||
approval_policy,
|
||||
approvals_reviewer,
|
||||
requested_approvals_reviewer,
|
||||
sandbox_policy,
|
||||
permission_profile,
|
||||
active_permission_profile,
|
||||
@@ -890,6 +893,7 @@ impl CodexThread {
|
||||
disabled_plugin_ids,
|
||||
} = overrides;
|
||||
SessionSettingsUpdate {
|
||||
requested_approvals_reviewer,
|
||||
turn_extension_init,
|
||||
step_settings: StepSettingsUpdate {
|
||||
model,
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
//! Resolves the effective Guardian reviewer while retaining the requested choice.
|
||||
|
||||
use codex_config::ConfigRequirements;
|
||||
use codex_protocol::config_types::ApprovalsReviewer;
|
||||
|
||||
/// Resolves the effective reviewer without replacing the retained request.
|
||||
/// Trusted private reviewers omit the model to bypass model-required review.
|
||||
pub(crate) fn resolve_guardian_reviewer(
|
||||
requested: ApprovalsReviewer,
|
||||
model: Option<&str>,
|
||||
requirements: &ConfigRequirements,
|
||||
) -> super::ConstraintResult<ApprovalsReviewer> {
|
||||
let reviewer = if model.is_some_and(|model| requirements.auto_review_required_for_model(model))
|
||||
{
|
||||
ApprovalsReviewer::AutoReview
|
||||
} else if requirements.approvals_reviewer.can_set(&requested).is_ok() {
|
||||
requested
|
||||
} else {
|
||||
requirements.approvals_reviewer.value()
|
||||
};
|
||||
requirements.approvals_reviewer.can_set(&reviewer)?;
|
||||
Ok(reviewer)
|
||||
}
|
||||
@@ -168,6 +168,7 @@ use toml_edit::DocumentMut;
|
||||
|
||||
mod auth_keyring;
|
||||
pub mod edit;
|
||||
mod guardian_policy;
|
||||
mod managed_features;
|
||||
mod metrics;
|
||||
mod network_config;
|
||||
@@ -179,6 +180,7 @@ mod permission_profile_selection;
|
||||
mod permissions;
|
||||
mod requirements;
|
||||
mod resolved_permission_profile;
|
||||
pub(crate) use guardian_policy::resolve_guardian_reviewer;
|
||||
mod runtime_refresh;
|
||||
pub(crate) use runtime_refresh::RuntimeConfigRefresh;
|
||||
#[cfg(test)]
|
||||
@@ -693,6 +695,9 @@ pub struct Config {
|
||||
/// been escalated. This does not disable separate safety checks such as
|
||||
/// ARC.
|
||||
pub approvals_reviewer: ApprovalsReviewer,
|
||||
/// Requested reviewer when managed requirements replace the effective choice.
|
||||
/// `None` uses `approvals_reviewer`, including programmatically constructed configs.
|
||||
pub requested_approvals_reviewer: Option<ApprovalsReviewer>,
|
||||
|
||||
/// enforce_residency means web traffic cannot be routed outside of a
|
||||
/// particular geography. HTTP clients should direct their requests
|
||||
@@ -3993,6 +3998,7 @@ impl Config {
|
||||
let mut approvals_reviewer = approvals_reviewer_override
|
||||
.or(cfg.approvals_reviewer)
|
||||
.unwrap_or(ApprovalsReviewer::User);
|
||||
let requested_approvals_reviewer = approvals_reviewer;
|
||||
if !approvals_reviewer_was_explicit
|
||||
&& let Err(err) = constrained_approvals_reviewer.can_set(&approvals_reviewer)
|
||||
{
|
||||
@@ -4593,6 +4599,9 @@ impl Config {
|
||||
explicit_permission_profile_mode,
|
||||
custom_permission_profiles,
|
||||
approvals_reviewer: constrained_approvals_reviewer.value(),
|
||||
requested_approvals_reviewer: (requested_approvals_reviewer
|
||||
!= constrained_approvals_reviewer.value())
|
||||
.then_some(requested_approvals_reviewer),
|
||||
enforce_residency: enforce_residency.value,
|
||||
notify: cfg.notify,
|
||||
base_instructions,
|
||||
|
||||
@@ -798,6 +798,9 @@ impl Session {
|
||||
"No models are available. Set `model` explicitly or check your model catalog configuration.".to_string(),
|
||||
));
|
||||
}
|
||||
let requested_approvals_reviewer = config
|
||||
.requested_approvals_reviewer
|
||||
.unwrap_or(config.approvals_reviewer);
|
||||
let trusted_guardian_reviewer = crate::guardian::is_basic_session_source(&session_source)
|
||||
&& !matches!(conversation_history, InitialHistory::Resumed(_));
|
||||
if config
|
||||
@@ -824,13 +827,17 @@ impl Session {
|
||||
);
|
||||
}
|
||||
}
|
||||
config
|
||||
.config_layer_stack
|
||||
.requirements()
|
||||
.approvals_reviewer
|
||||
.can_set(&ApprovalsReviewer::AutoReview)
|
||||
.map_err(|err| CodexErr::InvalidRequest(err.to_string()))?;
|
||||
config.approvals_reviewer = ApprovalsReviewer::AutoReview;
|
||||
}
|
||||
let reviewer = crate::config::resolve_guardian_reviewer(
|
||||
requested_approvals_reviewer,
|
||||
(!trusted_guardian_reviewer).then_some(model.as_str()),
|
||||
config.config_layer_stack.requirements(),
|
||||
)
|
||||
.map_err(|err| CodexErr::InvalidRequest(err.to_string()))?;
|
||||
{
|
||||
let config = Arc::make_mut(&mut config);
|
||||
config.requested_approvals_reviewer = Some(requested_approvals_reviewer);
|
||||
config.approvals_reviewer = reviewer;
|
||||
}
|
||||
if allow_provider_model_fallback
|
||||
&& let Some(requested_model) = config.model.as_ref()
|
||||
|
||||
@@ -336,6 +336,9 @@ impl SessionConfiguration {
|
||||
service_tier: self.step_settings.service_tier.clone(),
|
||||
approval_policy: self.step_settings.approval_policy.value(),
|
||||
approvals_reviewer: self.step_settings.approvals_reviewer,
|
||||
requested_approvals_reviewer: self
|
||||
.original_config_do_not_use
|
||||
.requested_approvals_reviewer,
|
||||
permission_profile: self.materialized_permission_profile(environment_selections),
|
||||
active_permission_profile: self.active_permission_profile(),
|
||||
cwd: self.legacy_fallback_cwd.clone(),
|
||||
@@ -369,6 +372,9 @@ impl SessionConfiguration {
|
||||
),
|
||||
approval_policy: Some(self.step_settings.approval_policy.value()),
|
||||
approvals_reviewer: Some(self.step_settings.approvals_reviewer),
|
||||
requested_approvals_reviewer: self
|
||||
.original_config_do_not_use
|
||||
.requested_approvals_reviewer,
|
||||
permission_profile: Some(self.permission_profile()),
|
||||
active_permission_profile: self.active_permission_profile(),
|
||||
windows_sandbox_level: Some(self.windows_sandbox_level),
|
||||
@@ -575,6 +581,17 @@ impl SessionConfiguration {
|
||||
&updates.step_settings,
|
||||
&next_configuration.step_settings_constraints(next_environments),
|
||||
)?);
|
||||
if let Some(reviewer) = updates
|
||||
.requested_approvals_reviewer
|
||||
.or(updates.step_settings.approvals_reviewer)
|
||||
&& next_configuration
|
||||
.original_config_do_not_use
|
||||
.requested_approvals_reviewer
|
||||
!= Some(reviewer)
|
||||
{
|
||||
Arc::make_mut(&mut next_configuration.original_config_do_not_use)
|
||||
.requested_approvals_reviewer = Some(reviewer);
|
||||
}
|
||||
Ok(next_configuration)
|
||||
}
|
||||
|
||||
@@ -624,6 +641,7 @@ pub(crate) struct SessionSettingsCommit {
|
||||
#[derive(Default, Clone)]
|
||||
pub(crate) struct SessionSettingsUpdate {
|
||||
pub(crate) step_settings: StepSettingsUpdate,
|
||||
pub(crate) requested_approvals_reviewer: Option<ApprovalsReviewer>,
|
||||
/// Omission preserves the current data; an empty initializer clears it.
|
||||
pub(crate) turn_extension_init: Option<ExtensionDataInit>,
|
||||
pub(crate) environments: Option<TurnEnvironmentSelections>,
|
||||
|
||||
Reference in New Issue
Block a user