mirror of
https://github.com/openai/codex.git
synced 2026-11-13 06:45:07 +00:00
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:
1 parent
bdfd769640
commit
a0dcfe2ada
7 files changed
+182
-54
No files matched your search
@@ -23,8 +23,10 @@ use axum::routing::get;
|
||||
use axum::routing::post;
|
||||
use codex_app_server_protocol::ApprovalsReviewer;
|
||||
use codex_app_server_protocol::AskForApproval;
|
||||
use codex_app_server_protocol::ClientRequest;
|
||||
use codex_app_server_protocol::ItemCompletedNotification;
|
||||
use codex_app_server_protocol::ItemGuardianApprovalReviewStartedNotification;
|
||||
use codex_app_server_protocol::McpServerElicitationRequest;
|
||||
use codex_app_server_protocol::McpToolCallStatus;
|
||||
use codex_app_server_protocol::SandboxMode;
|
||||
use codex_app_server_protocol::ServerRequest;
|
||||
@@ -39,6 +41,9 @@ use codex_app_server_protocol::ThreadRollbackParams;
|
||||
use codex_app_server_protocol::ThreadRollbackResponse;
|
||||
use codex_app_server_protocol::ThreadStartParams;
|
||||
use codex_app_server_protocol::TurnCompletedNotification;
|
||||
use codex_app_server_protocol::TurnSettingsUpdateParams;
|
||||
use codex_app_server_protocol::TurnSettingsUpdateResponse;
|
||||
use codex_app_server_protocol::TurnSettingsUpdateStatus;
|
||||
use codex_app_server_protocol::TurnStartParams;
|
||||
use codex_app_server_protocol::TurnStartResponse;
|
||||
use codex_app_server_protocol::TurnStatus;
|
||||
@@ -161,6 +166,7 @@ struct MockResponsesState {
|
||||
transcript_content: TranscriptContent,
|
||||
mcp_server_name: Option<&'static str>,
|
||||
mcp_tool_sequence: Option<&'static [&'static str]>,
|
||||
mcp_messages: Mutex<Vec<&'static str>>,
|
||||
root_worker: bool,
|
||||
root_user_restriction: bool,
|
||||
root_user_input_restriction: bool,
|
||||
@@ -448,7 +454,13 @@ async fn parent_response(
|
||||
|| (state.root_worker || state.root_user_restriction) && request_number == 3
|
||||
{
|
||||
let call_id = format!("guardian-action-{request_number}");
|
||||
let mut message = format!("guardian-{request_number}");
|
||||
let mut message = state
|
||||
.mcp_messages
|
||||
.lock()
|
||||
.expect("MCP messages lock")
|
||||
.get(request_number)
|
||||
.map(|message| (*message).to_string())
|
||||
.unwrap_or_else(|| format!("guardian-{request_number}"));
|
||||
if request_number == 0 && matches!(state.review_outcome, ReviewOutcome::Deny) {
|
||||
message.push_str(&"x".repeat(2_000));
|
||||
}
|
||||
@@ -1817,11 +1829,11 @@ async fn first_cua_review_does_not_wait_for_initial_score(
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn full_access_skips_guardian_until_review_is_enabled() -> Result<()> {
|
||||
async fn user_approval_skips_async_guardian_without_changing_other_modes() -> Result<()> {
|
||||
skip_if_no_network!(Ok(()));
|
||||
let state = Arc::new(MockResponsesState {
|
||||
mcp_server_name: Some("node_repl"),
|
||||
mcp_tool_sequence: Some(&["js", "js"]),
|
||||
mcp_tool_sequence: Some(&["js", "js", "js"]),
|
||||
..Default::default()
|
||||
});
|
||||
let listener = TcpListener::bind("127.0.0.1:0").await?;
|
||||
@@ -1832,15 +1844,15 @@ async fn full_access_skips_guardian_until_review_is_enabled() -> Result<()> {
|
||||
let responses_server = tokio::spawn(async move {
|
||||
let _ = axum::serve(listener, router).await;
|
||||
});
|
||||
// Exercise strict, sensitive CUA confirmations using the existing MCP server.
|
||||
// Mix ordinary execution, a user-input form, and a sensitive check.
|
||||
let (mcp_url, mcp_server) =
|
||||
start_mcp_server_with_tools(&["js"], /*sensitive_action*/ Some(true)).await?;
|
||||
start_mcp_server_with_tools(&["js"], /*sensitive_action*/ None).await?;
|
||||
let codex_home = TempDir::new()?;
|
||||
MockResponsesConfig::new(&responses_url)
|
||||
.with_model(MODEL)
|
||||
.with_provider_config("supports_websockets = false")
|
||||
.with_approval_policy("never")
|
||||
.with_root_config("approvals_reviewer = \"auto_review\"")
|
||||
.with_approval_policy("on-request")
|
||||
.with_root_config("approvals_reviewer = \"user\"")
|
||||
.with_extra_config(&format!(
|
||||
"[mcp_servers.node_repl]\nurl = \"{mcp_url}/mcp\"\ndefault_tools_approval_mode = \"auto\"\n\n[features.guardianv2]\nenabled = true"
|
||||
))
|
||||
@@ -1856,7 +1868,8 @@ async fn full_access_skips_guardian_until_review_is_enabled() -> Result<()> {
|
||||
.await?;
|
||||
let thread = app_server
|
||||
.start_thread(ThreadStartParams {
|
||||
approval_policy: Some(AskForApproval::Never),
|
||||
approval_policy: Some(AskForApproval::OnRequest),
|
||||
approvals_reviewer: Some(ApprovalsReviewer::User),
|
||||
sandbox: Some(SandboxMode::DangerFullAccess),
|
||||
..Default::default()
|
||||
})
|
||||
@@ -1865,26 +1878,82 @@ async fn full_access_skips_guardian_until_review_is_enabled() -> Result<()> {
|
||||
|
||||
// Leave async scoring pending so reviewed mode must use synchronous Guardian.
|
||||
state.allow_guardian_review.notify_one();
|
||||
for approval_policy in [AskForApproval::Never, AskForApproval::OnRequest] {
|
||||
use ApprovalsReviewer::AutoReview;
|
||||
use ApprovalsReviewer::User;
|
||||
use AskForApproval::Never;
|
||||
use AskForApproval::OnRequest;
|
||||
for (approval_policy, reviewer, live_reviewer, expected_reviews) in [
|
||||
(OnRequest, User, None, 1),
|
||||
(Never, User, None, 0),
|
||||
(Never, AutoReview, None, 0),
|
||||
(OnRequest, User, Some(AutoReview), 2),
|
||||
(OnRequest, AutoReview, Some(User), 2),
|
||||
(OnRequest, AutoReview, None, 3),
|
||||
(OnRequest, User, None, 1),
|
||||
] {
|
||||
let reviews_before = state.guardian_reviews.load(Ordering::SeqCst);
|
||||
*state.mcp_messages.lock().expect("MCP messages lock") = vec![
|
||||
if approval_policy == OnRequest {
|
||||
"confirm"
|
||||
} else {
|
||||
"guardian-0"
|
||||
},
|
||||
"guardian-1",
|
||||
"sensitive",
|
||||
];
|
||||
state.parent_requests.store(0, Ordering::SeqCst);
|
||||
app_server.clear_message_buffer();
|
||||
let completed = timeout(
|
||||
TIMEOUT,
|
||||
app_server.start_turn_and_wait_for_completion(TurnStartParams {
|
||||
let request_id = app_server
|
||||
.send_turn_start_request(TurnStartParams {
|
||||
thread_id: thread.id.clone(),
|
||||
input: vec![UserInput::Text {
|
||||
text: USER_CONTEXT.to_owned(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
approval_policy: Some(approval_policy),
|
||||
approvals_reviewer: Some(reviewer),
|
||||
..Default::default()
|
||||
}),
|
||||
)
|
||||
.await??;
|
||||
})
|
||||
.await?;
|
||||
let started: TurnStartResponse =
|
||||
timeout(TIMEOUT, app_server.read_response(request_id)).await??;
|
||||
if approval_policy == OnRequest {
|
||||
let request =
|
||||
timeout(TIMEOUT, app_server.read_stream_until_request_message()).await??;
|
||||
let ServerRequest::McpServerElicitationRequest { request_id, params } = request else {
|
||||
panic!("expected user input, got {request:?}");
|
||||
};
|
||||
assert!(
|
||||
matches!(params.request, McpServerElicitationRequest::Form { message, .. }
|
||||
if message == "Allow this request?")
|
||||
);
|
||||
if let Some(reviewer) = live_reviewer {
|
||||
let updated: TurnSettingsUpdateResponse = app_server
|
||||
.request(|request_id| ClientRequest::TurnSettingsUpdate {
|
||||
request_id,
|
||||
params: TurnSettingsUpdateParams {
|
||||
thread_id: thread.id.clone(),
|
||||
turn_id: started.turn.id.clone(),
|
||||
approvals_reviewer: Some(reviewer),
|
||||
..Default::default()
|
||||
},
|
||||
})
|
||||
.await?;
|
||||
assert_eq!(updated.status, TurnSettingsUpdateStatus::Applied);
|
||||
}
|
||||
app_server
|
||||
.send_response(
|
||||
request_id,
|
||||
json!({ "action": "accept", "content": { "confirmed": true } }),
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
let completed: TurnCompletedNotification =
|
||||
timeout(TIMEOUT, app_server.read_notification("turn/completed")).await??;
|
||||
assert_eq!(completed.turn.status, TurnStatus::Completed);
|
||||
|
||||
let mut tool_results = Vec::new();
|
||||
while tool_results.len() < 2 {
|
||||
while tool_results.len() < 3 {
|
||||
let completed: ItemCompletedNotification =
|
||||
timeout(TIMEOUT, app_server.read_notification("item/completed")).await??;
|
||||
if let ThreadItem::McpToolCall {
|
||||
@@ -1899,22 +1968,28 @@ async fn full_access_skips_guardian_until_review_is_enabled() -> Result<()> {
|
||||
}
|
||||
assert_eq!(
|
||||
tool_results,
|
||||
(0..2)
|
||||
(0..3)
|
||||
.map(|index| (
|
||||
McpToolCallStatus::Completed,
|
||||
Some(vec![
|
||||
json!({ "type": "text", "text": format!("echo: guardian-{index}") })
|
||||
]),
|
||||
Some(vec![json!({ "type": "text", "text": match index {
|
||||
0 if approval_policy == OnRequest => "accepted".to_string(),
|
||||
2 => "echo: sensitive".to_string(),
|
||||
_ => format!("echo: guardian-{index}"),
|
||||
} })]),
|
||||
None,
|
||||
))
|
||||
.collect::<Vec<_>>()
|
||||
);
|
||||
if approval_policy == AskForApproval::Never {
|
||||
assert_eq!(state.guardian_reviews.load(Ordering::SeqCst), 0);
|
||||
assert_eq!(state.luna_connections.load(Ordering::SeqCst), 0);
|
||||
} else {
|
||||
assert_eq!(
|
||||
state.guardian_reviews.load(Ordering::SeqCst),
|
||||
reviews_before + expected_reviews
|
||||
);
|
||||
if approval_policy == OnRequest
|
||||
&& (reviewer == AutoReview || live_reviewer == Some(AutoReview))
|
||||
{
|
||||
wait_for_luna_request(&state, /*index*/ 0).await?;
|
||||
assert_eq!(state.guardian_reviews.load(Ordering::SeqCst), 2);
|
||||
} else if reviews_before <= 1 {
|
||||
assert_eq!(state.luna_connections.load(Ordering::SeqCst), 0);
|
||||
}
|
||||
}
|
||||
app_server.shutdown_gracefully().await?;
|
||||
|
||||
@@ -1369,7 +1369,10 @@ impl ServerHandler for ToolAppsMcpServer {
|
||||
// Match the execution approval emitted by the real Node REPL server.
|
||||
approval_meta["connector_id"] = json!("node_repl");
|
||||
}
|
||||
if let Some(sensitive_action) = self.sensitive_action {
|
||||
if let Some(sensitive_action) = self
|
||||
.sensitive_action
|
||||
.or((message == "sensitive").then_some(true))
|
||||
{
|
||||
approval_meta["codex_sensitive_action"] = json!(sensitive_action);
|
||||
}
|
||||
let result = context
|
||||
|
||||
Reference in new issue
Block a user