mirror of
https://github.com/openai/codex.git
synced 2026-11-13 22:47:20 +00:00
Honor model requirements in Guardian computer-use scoring (#42422)
## Why Computer-use-only Guardian scoring should follow the active model's REPL auto-review requirement, including when the model changes within a live thread. ## What changed - Run computer-use scoring and fast approval decisions only when the active model sets `node_repl_auto_review_required`. - Invalidate prior or in-flight scores when a model switch skips scoring, so switching back to a reviewed model cannot revive a stale decision. ## Testing - Cover model switches for both `node_repl` and `cua_repl` MCP servers. - Verify skipped scoring and stale-score rejection across requirement changes. GitOrigin-RevId: 46aec4d017bea8f135b435bcd769b87369b8ce95
This commit is contained in:
1 parent
1d741742c5
commit
38ba8cdceb
5 files changed
+209
-3
No files matched your search
@@ -280,6 +280,9 @@ impl ApprovalReviewContributor for GuardianV2Extension {
|
||||
.get("server")
|
||||
.and_then(serde_json::Value::as_str)
|
||||
.is_some_and(is_node_repl_backed_server)
|
||||
|| !thread_store
|
||||
.get::<ModelInfo>()
|
||||
.is_some_and(|model| model.node_repl_auto_review_required)
|
||||
{
|
||||
record_fast_decision(extension_metrics.as_deref(), "deferred", "out_of_scope");
|
||||
return None;
|
||||
@@ -498,16 +501,21 @@ impl GuardianV2Extension {
|
||||
};
|
||||
// Use the live reviewer, not the startup config or per-app reviewer overrides.
|
||||
let snapshot = thread.config_snapshot().await;
|
||||
let parent_model = input.thread_store.get::<ModelInfo>();
|
||||
if snapshot.full_access
|
||||
|| thread.approvals_reviewer_for_turn(input.turn_id).await == ApprovalsReviewer::User
|
||||
|| (guardian_config.review_scope == GuardianV2ReviewScope::ComputerUseOnly
|
||||
&& !parent_model
|
||||
.as_ref()
|
||||
.is_some_and(|model| model.node_repl_auto_review_required))
|
||||
{
|
||||
// A skipped call invalidates older scores, including ones still in flight.
|
||||
// A skipped call invalidates older scores, including ones still in flight
|
||||
// when switching to a model that does not require REPL review.
|
||||
score_progress
|
||||
.latest_failed_tool_call
|
||||
.fetch_max(tool_call_index, Ordering::Release);
|
||||
return;
|
||||
}
|
||||
let parent_model = input.thread_store.get::<ModelInfo>();
|
||||
// Computer-use-only scores cannot approve other tools for required models.
|
||||
if guardian_config.review_scope != GuardianV2ReviewScope::ComputerUseOnly
|
||||
&& parent_model.as_ref().is_some_and(|model| {
|
||||
|
||||
@@ -47,6 +47,7 @@ use codex_protocol::models::MessagePhase;
|
||||
use codex_protocol::models::ReasoningItemReasoningSummary;
|
||||
use codex_protocol::openai_models::GuardianV2ModelConfig;
|
||||
use codex_protocol::openai_models::GuardianV2TranscriptModelConfig;
|
||||
use codex_protocol::openai_models::ModelInfo;
|
||||
use codex_protocol::openai_models::ReasoningEffort;
|
||||
use codex_protocol::protocol::EventMsg;
|
||||
use codex_protocol::protocol::Op;
|
||||
@@ -229,6 +230,13 @@ async fn installed_extension_reconnects_after_auth_refresh() -> Result<()> {
|
||||
let progress = thread_store
|
||||
.get::<GuardianV2ScoreProgress>()
|
||||
.expect("Guardian v2 should initialize");
|
||||
let mut model = test
|
||||
.thread_manager
|
||||
.get_models_manager()
|
||||
.get_model_info("gpt-5.5", &config.to_models_manager_config())
|
||||
.await;
|
||||
model.node_repl_auto_review_required = true;
|
||||
thread_store.insert(model);
|
||||
let turn_store = ExtensionData::new("turn-1");
|
||||
let tool_name = ToolName::namespaced("mcp__node_repl__", "js");
|
||||
let payload = ToolPayload::Function {
|
||||
@@ -550,6 +558,14 @@ async fn computer_use_only_scores_cannot_approve_other_actions() -> Result<()> {
|
||||
|
||||
let fixture = GuardianFailureFixture::new().await?;
|
||||
let thread_store = fixture.test.codex.thread_extension_data();
|
||||
let mut model = fixture
|
||||
.test
|
||||
.thread_manager
|
||||
.get_models_manager()
|
||||
.get_model_info("gpt-5.5", &fixture.test.config.to_models_manager_config())
|
||||
.await;
|
||||
model.node_repl_auto_review_required = true;
|
||||
thread_store.insert(model);
|
||||
let mut config = thread_store
|
||||
.get::<crate::async_scorer::config::GuardianV2Config>()
|
||||
.expect("Guardian v2 should have initialized")
|
||||
@@ -687,6 +703,41 @@ async fn computer_use_only_scores_cannot_approve_other_actions() -> Result<()> {
|
||||
]
|
||||
);
|
||||
|
||||
let mut model = thread_store.get::<ModelInfo>().unwrap().as_ref().clone();
|
||||
model.node_repl_auto_review_required = false;
|
||||
thread_store.insert(model.clone());
|
||||
fixture
|
||||
.score_tool(ToolName::namespaced("mcp__node_repl__", "js"))
|
||||
.await;
|
||||
assert!(
|
||||
progress.latest_failed_tool_call.load(Ordering::Acquire)
|
||||
> progress.latest_scored_tool_call.load(Ordering::Acquire)
|
||||
);
|
||||
for required in [false, true] {
|
||||
model.node_repl_auto_review_required = required;
|
||||
thread_store.insert(model.clone());
|
||||
// Also reject a low score published by an older, in-flight classifier.
|
||||
thread_store.insert(SecurityRiskScore {
|
||||
scores: BTreeMap::from([("action_risk".to_owned(), 0.0)]),
|
||||
call_id: None,
|
||||
action: None,
|
||||
sampled_at: None,
|
||||
});
|
||||
assert_eq!(
|
||||
fixture
|
||||
.registry
|
||||
.fast_approval_decision(
|
||||
&fixture.session_store,
|
||||
thread_store,
|
||||
r#"{"tool":"mcp_tool_call","server":"node_repl","tool_name":"js"}"#,
|
||||
/*extension_metrics*/ None,
|
||||
)
|
||||
.await,
|
||||
None,
|
||||
"switching back to a reviewed model must not revive a skipped score"
|
||||
);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user