Skip Guardian scoring in User approval mode (#42256)

## What changed

- Skip Guardian prewarming and asynchronous scoring when `approvalsReviewer` is
  `"user"`, including when the reviewer changes during an active turn.
- Automatically accept ordinary `node_repl.js` execution confirmations in this
  mode while continuing to surface sensitive-action checks and requests for user
  input.
- Cover transitions between User approval, automatic review, and Full Access in
  the app-server integration tests.

GitOrigin-RevId: 599c8ebb672728b9bf3d64c34377ddec230325f8
This commit is contained in:
jif
2026-09-02 10:32:22 +00:00
committed by copyberry
parent bdfd769640
commit a0dcfe2ada
7 changed files with 182 additions and 54 deletions
@@ -40,6 +40,7 @@ use codex_login::AgentIdentityAuthPolicy;
use codex_login::AuthManager;
use codex_model_provider::create_model_provider;
use codex_protocol::ThreadId;
use codex_protocol::config_types::ApprovalsReviewer;
use codex_protocol::mcp::is_node_repl_backed_server;
use codex_protocol::mcp::is_node_repl_backed_tool;
use codex_protocol::openai_models::ModelInfo;
@@ -214,16 +215,18 @@ impl ThreadLifecycleContributor<Config> for GuardianV2Extension {
.insert(TrustedSkillRoots::from_config(input.config));
input.thread_store.insert(guardian_v2_enabled);
// Keep the sampler available if a later turn leaves Full Access, but do
// not open Guardian connections while Full Access is selected.
if !has_full_access(
input.config.permissions.approval_policy.value(),
&input.config.permissions.effective_permission_profile(),
input
.environments
.iter()
.map(|environment| &environment.config),
) {
// Keep the sampler available for later automatic review, but do not
// prewarm while User approval mode or Full Access is selected.
if input.config.approvals_reviewer == ApprovalsReviewer::AutoReview
&& !has_full_access(
input.config.permissions.approval_policy.value(),
&input.config.permissions.effective_permission_profile(),
input
.environments
.iter()
.map(|environment| &environment.config),
)
{
tokio::spawn(async move {
sampler.prewarm().await;
});
@@ -493,10 +496,11 @@ impl GuardianV2Extension {
return;
}
};
// Read current permissions, not the startup config: existing threads can
// enter Full Access after the sampler has already been initialized.
// Use the live reviewer, not the startup config or per-app reviewer overrides.
let snapshot = thread.config_snapshot().await;
if snapshot.full_access {
if snapshot.full_access
|| thread.approvals_reviewer_for_turn(input.turn_id).await == ApprovalsReviewer::User
{
// A skipped call invalidates older scores, including ones still in flight.
score_progress
.latest_failed_tool_call
@@ -34,6 +34,7 @@ use codex_login::ExternalAuthRefreshContext;
use codex_model_provider_info::ModelProviderInfo;
use codex_protocol::ResponseItemId;
use codex_protocol::ThreadId;
use codex_protocol::config_types::ApprovalsReviewer;
use codex_protocol::models::ContentItem;
use codex_protocol::models::FunctionCallOutputContentItem;
use codex_protocol::models::FunctionCallOutputPayload;
@@ -115,7 +116,10 @@ async fn installed_extension_warms_connections_without_blocking_thread_start() -
skip_if_no_network!(Ok(()));
let thread_server = responses::start_mock_server().await;
let test = test_codex().build_with_auto_env(&thread_server).await?;
let test = test_codex()
.with_config(|config| config.approvals_reviewer = ApprovalsReviewer::AutoReview)
.build_with_auto_env(&thread_server)
.await?;
let mut connections = vec![
WebSocketConnectionConfig {
requests: Vec::new(),
@@ -172,7 +176,10 @@ async fn installed_extension_reconnects_after_auth_refresh() -> Result<()> {
skip_if_no_network!(Ok(()));
let thread_server = responses::start_mock_server().await;
let test = test_codex().build_with_auto_env(&thread_server).await?;
let test = test_codex()
.with_config(|config| config.approvals_reviewer = ApprovalsReviewer::AutoReview)
.build_with_auto_env(&thread_server)
.await?;
let events = vec![
ev_assistant_message("sample", "low"),
ev_completed("response-1"),
@@ -903,7 +910,10 @@ async fn sample_configured_conversation_history_with_source(
.policy = Some(TEST_CATALOG_GUARDIAN_POLICY.to_owned());
})
.with_model("gpt-5.5")
.with_config(move |config| config.guardian_policy_config = guardian_policy)
.with_config(move |config| {
config.approvals_reviewer = ApprovalsReviewer::AutoReview;
config.guardian_policy_config = guardian_policy;
})
.with_pre_build_hook(move |home| {
std::fs::write(home.join("config.toml"), guardian_config)
.expect("Guardian v2 configuration should be written");
@@ -2171,6 +2181,7 @@ async fn contributor_skips_required_models_in_standard_scope() -> Result<()> {
let test = test_codex()
.with_home(Arc::clone(&initial.home))
.with_config(move |config| {
config.approvals_reviewer = ApprovalsReviewer::AutoReview;
config.config_layer_stack = config_layer_stack;
config
.features
@@ -2806,6 +2817,7 @@ async fn contributor_reuses_the_latest_compatible_parent_compaction() -> Result<
let thread_server = responses::start_mock_server().await;
let test = test_codex()
.with_config(|config| config.approvals_reviewer = ApprovalsReviewer::AutoReview)
.with_pre_build_hook(|home| {
std::fs::write(
home.join("config.toml"),