mirror of
https://github.com/openai/codex.git
synced 2026-11-13 14:46:14 +00:00
Interrupt cyber model turns after one Guardian denial (#37190)
## What changed - Add a Guardian circuit-breaker policy for models whose catalog specialty is `cyber`. - Interrupt those models after the first Guardian denial while retaining the existing denial thresholds for other models. - Centralize the `cyber` model-specialty identifier for use across core, protocol, app-server, and TUI code. ## Testing - Cover the cyber-specific threshold in the circuit-breaker unit tests. - Verify end to end that a denied command is not executed and the turn is interrupted after one denial. GitOrigin-RevId: fad24c9e7aa280432580db1f0e39d13535779710
This commit is contained in:
1 parent
92b83e226d
commit
f141dc77f0
9 files changed
+200
-23
No files matched your search
@@ -16,6 +16,7 @@ use codex_features::Feature;
|
||||
use codex_login::CodexAuth;
|
||||
use codex_protocol::ThreadId;
|
||||
use codex_protocol::config_types::ApprovalsReviewer;
|
||||
use codex_protocol::openai_models::MODEL_SPECIALTY_CYBER;
|
||||
use codex_protocol::openai_models::ModelsResponse;
|
||||
use codex_protocol::protocol::AskForApproval;
|
||||
use codex_protocol::protocol::EventMsg;
|
||||
@@ -23,6 +24,7 @@ use codex_protocol::protocol::Op;
|
||||
use codex_protocol::protocol::RolloutItem;
|
||||
use codex_protocol::protocol::RolloutLine;
|
||||
use codex_protocol::protocol::SandboxPolicy;
|
||||
use codex_protocol::protocol::TurnAbortReason;
|
||||
use codex_protocol::user_input::UserInput;
|
||||
use core_test_support::fs_wait;
|
||||
use core_test_support::responses::ev_assistant_message;
|
||||
@@ -748,6 +750,127 @@ async fn guardian_denial_rejects_tool_call_with_rationale() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn cyber_model_guardian_denial_interrupts_turn_immediately() -> Result<()> {
|
||||
skip_if_no_network!(Ok(()));
|
||||
skip_if_sandbox!(Ok(()));
|
||||
skip_if_wine_exec!(
|
||||
Ok(()),
|
||||
"Guardian approval actions require host-native paths"
|
||||
);
|
||||
|
||||
let server = start_mock_server().await;
|
||||
let approval_policy = AskForApproval::OnRequest;
|
||||
let sandbox_policy = SandboxPolicy::WorkspaceWrite {
|
||||
writable_roots: vec![],
|
||||
network_access: false,
|
||||
exclude_tmpdir_env_var: true,
|
||||
exclude_slash_tmp: true,
|
||||
};
|
||||
let sandbox_policy_for_config = sandbox_policy.clone();
|
||||
|
||||
let mut builder = test_codex()
|
||||
.with_model_info_override("gpt-5.4", |model| {
|
||||
model.model_specialty = Some(MODEL_SPECIALTY_CYBER.to_string());
|
||||
})
|
||||
.with_config(move |config| {
|
||||
config.permissions.approval_policy = Constrained::allow_any(approval_policy);
|
||||
config
|
||||
.set_legacy_sandbox_policy(sandbox_policy_for_config)
|
||||
.expect("set sandbox policy");
|
||||
});
|
||||
let test = builder.build_with_auto_env(&server).await?;
|
||||
|
||||
let output_file = test.cwd.path().join("cyber-guardian-denied.txt");
|
||||
let command = format!("printf should-not-run > {}", output_file.display());
|
||||
let tool_args = json!({
|
||||
"cmd": command,
|
||||
"yield_time_ms": 1_000_u64,
|
||||
"sandbox_permissions": SandboxPermissions::RequireEscalated,
|
||||
"justification": "Exercise immediate Guardian interruption for cyber models.",
|
||||
});
|
||||
let responses = mount_sse_sequence(
|
||||
&server,
|
||||
vec![
|
||||
sse(vec![
|
||||
ev_response_created("resp-cyber-parent-tool-denied"),
|
||||
ev_function_call(
|
||||
"exec-cyber-call-denied",
|
||||
"exec_command",
|
||||
&serde_json::to_string(&tool_args)?,
|
||||
),
|
||||
ev_completed("resp-cyber-parent-tool-denied"),
|
||||
]),
|
||||
sse(vec![
|
||||
ev_response_created("resp-cyber-guardian-denied"),
|
||||
ev_assistant_message(
|
||||
"msg-cyber-guardian-denied",
|
||||
&json!({
|
||||
"risk_level": "high",
|
||||
"user_authorization": "low",
|
||||
"outcome": "deny",
|
||||
"rationale": "The requested command has unacceptable test risk.",
|
||||
})
|
||||
.to_string(),
|
||||
),
|
||||
ev_completed("resp-cyber-guardian-denied"),
|
||||
]),
|
||||
],
|
||||
)
|
||||
.await;
|
||||
|
||||
test.codex
|
||||
.submit(Op::UserInput {
|
||||
items: vec![UserInput::Text {
|
||||
text: "run a command that Guardian should deny for a cyber model".into(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
final_output_json_schema: None,
|
||||
responsesapi_client_metadata: None,
|
||||
additional_context: Default::default(),
|
||||
thread_settings: codex_protocol::protocol::ThreadSettingsOverrides {
|
||||
approval_policy: Some(approval_policy),
|
||||
approvals_reviewer: Some(ApprovalsReviewer::AutoReview),
|
||||
sandbox_policy: Some(sandbox_policy),
|
||||
..Default::default()
|
||||
},
|
||||
})
|
||||
.await?;
|
||||
|
||||
let warning = wait_for_event(&test.codex, |event| {
|
||||
matches!(
|
||||
event,
|
||||
EventMsg::GuardianWarning(warning)
|
||||
if warning.message.contains("too many approval requests")
|
||||
)
|
||||
})
|
||||
.await;
|
||||
let EventMsg::GuardianWarning(warning) = warning else {
|
||||
unreachable!("wait_for_event returned a non-warning event")
|
||||
};
|
||||
assert!(
|
||||
warning
|
||||
.message
|
||||
.contains("1 consecutive, 1 in the last 50 reviews")
|
||||
);
|
||||
|
||||
let aborted = wait_for_event(&test.codex, |event| {
|
||||
matches!(event, EventMsg::TurnAborted(_))
|
||||
})
|
||||
.await;
|
||||
let EventMsg::TurnAborted(aborted) = aborted else {
|
||||
unreachable!("wait_for_event returned a non-abort event")
|
||||
};
|
||||
assert_eq!(aborted.reason, TurnAbortReason::Interrupted);
|
||||
assert_eq!(responses.requests().len(), 2);
|
||||
assert!(
|
||||
!output_file.exists(),
|
||||
"Guardian-denied cyber-model command unexpectedly executed"
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn guardian_review_session_does_not_inherit_legacy_notify() -> Result<()> {
|
||||
skip_if_no_network!(Ok(()));
|
||||
|
||||
Reference in new issue
Block a user