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:
ioanbudea-OAI
2026-11-04 00:31:24 +00:00
committed by copyberry
parent 1685f2d621
commit c141c31323
22 changed files with 158 additions and 36 deletions
+1
View File
@@ -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()
+10 -1
View File
@@ -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(),
+4
View File
@@ -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)
}
+9
View File
@@ -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,
+14 -7
View File
@@ -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()
+18
View File
@@ -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>,