mirror of
https://github.com/openai/codex.git
synced 2026-11-10 16:44:38 +00:00
Enforce non-interactive approval policy for Codex delegates (#38205)
## What changed - Require delegated Codex sessions to use the `never` approval policy and reject delegate creation with any prompt-capable policy. - Stop forwarding delegate approval and permission requests to the parent session. Approval-requiring commands and MCP tool calls are denied within the delegate instead. - Skip prompts for missing skill MCP dependencies when approvals are disabled. ## Testing Add coverage for review and Guardian delegates to verify that command escalations, MCP approvals, and skill dependency installation do not prompt or execute approval-gated actions. GitOrigin-RevId: fee96a97624352cd25c904592ba7eebe2ae3fd63
This commit is contained in:
1 parent
3d7f9b4637
commit
95aada11c4
11 files changed
+483
-1199
No files matched your search
@@ -1,61 +1,25 @@
|
||||
use super::*;
|
||||
use crate::environment_selection::TurnEnvironmentState;
|
||||
use crate::mcp_tool_call::MCP_TOOL_APPROVAL_DECLINE_SYNTHETIC;
|
||||
use crate::mcp_tool_call::MCP_TOOL_APPROVAL_QUESTION_ID_PREFIX;
|
||||
use async_channel::bounded;
|
||||
use codex_mcp::CODEX_APPS_MCP_SERVER_NAME;
|
||||
use codex_protocol::config_types::ApprovalsReviewer;
|
||||
use codex_protocol::error::CodexErrorDetails;
|
||||
use codex_protocol::models::NetworkPermissions;
|
||||
use codex_protocol::models::ResponseItem;
|
||||
use codex_protocol::protocol::AgentStatus;
|
||||
use codex_protocol::protocol::AskForApproval;
|
||||
use codex_protocol::protocol::EventMsg;
|
||||
use codex_protocol::protocol::ExecApprovalRequestEvent;
|
||||
use codex_protocol::protocol::GuardianAssessmentAction;
|
||||
use codex_protocol::protocol::GuardianAssessmentStatus;
|
||||
use codex_protocol::protocol::GuardianCommandSource;
|
||||
use codex_protocol::protocol::McpInvocation;
|
||||
use codex_protocol::protocol::McpStartupCompleteEvent;
|
||||
use codex_protocol::protocol::McpStartupStatus;
|
||||
use codex_protocol::protocol::McpStartupUpdateEvent;
|
||||
use codex_protocol::protocol::RawResponseItemEvent;
|
||||
use codex_protocol::protocol::ReviewDecision;
|
||||
use codex_protocol::protocol::TurnAbortReason;
|
||||
use codex_protocol::protocol::TurnAbortedEvent;
|
||||
use codex_protocol::request_permissions::RequestPermissionProfile;
|
||||
use codex_protocol::request_permissions::RequestPermissionsEvent;
|
||||
use codex_protocol::request_permissions::RequestPermissionsResponse;
|
||||
use codex_protocol::request_user_input::RequestUserInputAnswer;
|
||||
use codex_protocol::request_user_input::RequestUserInputEvent;
|
||||
use codex_protocol::request_user_input::RequestUserInputQuestion;
|
||||
use core_test_support::PathBufExt;
|
||||
use core_test_support::test_path_buf;
|
||||
use pretty_assertions::assert_eq;
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::Mutex;
|
||||
use tokio::sync::oneshot;
|
||||
use tokio::sync::watch;
|
||||
use tokio::time::timeout;
|
||||
|
||||
#[tokio::test]
|
||||
async fn dropped_approval_review_fails_closed() {
|
||||
let (tx, rx) = oneshot::channel();
|
||||
drop(tx);
|
||||
|
||||
assert_eq!(
|
||||
receive_approval_review(rx).await,
|
||||
ReviewDecision::denied("automatic approval review could not complete")
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn forward_events_filters_private_events_before_blocked_send_is_cancelled() {
|
||||
let (tx_events, rx_events) = bounded(SUBMISSION_CHANNEL_CAPACITY);
|
||||
let (tx_sub, rx_sub) = bounded(SUBMISSION_CHANNEL_CAPACITY);
|
||||
let (_agent_status_tx, agent_status) = watch::channel(AgentStatus::PendingInit);
|
||||
let (session, ctx, _rx_evt) = crate::session::tests::make_session_and_context_with_rx().await;
|
||||
let io = Arc::new(SessionIo {
|
||||
tx_sub,
|
||||
rx_event: rx_events,
|
||||
@@ -81,11 +45,7 @@ async fn forward_events_filters_private_events_before_blocked_send_is_cancelled(
|
||||
let cancel = CancellationToken::new();
|
||||
let forward = tokio::spawn(forward_events(
|
||||
Arc::clone(&io),
|
||||
Arc::clone(&session),
|
||||
tx_out.clone(),
|
||||
session,
|
||||
ctx,
|
||||
Arc::new(Mutex::new(HashMap::new())),
|
||||
cancel.clone(),
|
||||
));
|
||||
|
||||
@@ -207,6 +167,8 @@ async fn forward_ops_preserves_submission_trace_context() {
|
||||
async fn run_codex_thread_interactive_respects_pre_cancelled_spawn() {
|
||||
let (parent_session, parent_ctx, _rx_events) =
|
||||
crate::session::tests::make_session_and_context_with_rx().await;
|
||||
let mut config = parent_ctx.config.as_ref().clone();
|
||||
config.permissions.approval_policy = Constrained::allow_only(AskForApproval::Never);
|
||||
let cancel_token = CancellationToken::new();
|
||||
cancel_token.cancel();
|
||||
let parent_environments = parent_ctx.environments.clone();
|
||||
@@ -214,7 +176,7 @@ async fn run_codex_thread_interactive_respects_pre_cancelled_spawn() {
|
||||
let result = timeout(
|
||||
Duration::from_secs(/*secs*/ 1),
|
||||
run_codex_thread_interactive(
|
||||
parent_ctx.config.as_ref().clone(),
|
||||
config,
|
||||
Arc::clone(&parent_session.services.auth_manager),
|
||||
Arc::clone(&parent_session.services.models_manager),
|
||||
parent_session,
|
||||
@@ -237,426 +199,35 @@ async fn run_codex_thread_interactive_respects_pre_cancelled_spawn() {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn handle_request_permissions_uses_tool_call_id_for_round_trip() {
|
||||
let (parent_session, mut parent_ctx, rx_events) =
|
||||
crate::session::tests::make_session_and_context_with_rx().await;
|
||||
*parent_session.active_turn.lock().await = Some(crate::state::ActiveTurn::default());
|
||||
let parent_ctx_mut = Arc::get_mut(&mut parent_ctx).expect("single turn context ref");
|
||||
let TurnEnvironmentState::Ready(environment) = &mut parent_ctx_mut.environments.environments[0]
|
||||
else {
|
||||
panic!("expected ready primary environment");
|
||||
};
|
||||
environment.environment_id = "remote".to_string();
|
||||
|
||||
let (tx_sub, rx_sub) = bounded(SUBMISSION_CHANNEL_CAPACITY);
|
||||
let (_tx_events, rx_events_child) = bounded(SUBMISSION_CHANNEL_CAPACITY);
|
||||
let (_agent_status_tx, agent_status) = watch::channel(AgentStatus::PendingInit);
|
||||
let io = Arc::new(SessionIo {
|
||||
tx_sub,
|
||||
rx_event: rx_events_child,
|
||||
agent_status,
|
||||
session_loop_termination: completed_session_loop_termination(),
|
||||
});
|
||||
|
||||
let call_id = "tool-call-1".to_string();
|
||||
let expected_response = RequestPermissionsResponse {
|
||||
permissions: RequestPermissionProfile {
|
||||
network: Some(NetworkPermissions {
|
||||
enabled: Some(true),
|
||||
}),
|
||||
..RequestPermissionProfile::default()
|
||||
},
|
||||
scope: PermissionGrantScope::Turn,
|
||||
strict_auto_review: false,
|
||||
};
|
||||
#[allow(deprecated)]
|
||||
let delegated_cwd = parent_ctx.cwd.join("delegated-cwd");
|
||||
let cancel_token = CancellationToken::new();
|
||||
let request_call_id = call_id.clone();
|
||||
let request_cwd = delegated_cwd.clone();
|
||||
|
||||
let handle = tokio::spawn({
|
||||
let io = Arc::clone(&io);
|
||||
let parent_session = Arc::clone(&parent_session);
|
||||
let parent_ctx = Arc::clone(&parent_ctx);
|
||||
let cancel_token = cancel_token.clone();
|
||||
async move {
|
||||
handle_request_permissions(
|
||||
io.as_ref(),
|
||||
&parent_session,
|
||||
&parent_ctx,
|
||||
RequestPermissionsEvent {
|
||||
call_id: request_call_id,
|
||||
turn_id: "child-turn-1".to_string(),
|
||||
environment_id: Some("remote".to_string()),
|
||||
started_at_ms: 0,
|
||||
reason: Some("need access".to_string()),
|
||||
permissions: RequestPermissionProfile {
|
||||
network: Some(NetworkPermissions {
|
||||
enabled: Some(true),
|
||||
}),
|
||||
..RequestPermissionProfile::default()
|
||||
},
|
||||
cwd: Some(request_cwd),
|
||||
},
|
||||
&cancel_token,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
});
|
||||
|
||||
let request_event = timeout(Duration::from_secs(1), rx_events.recv())
|
||||
.await
|
||||
.expect("request_permissions event timed out")
|
||||
.expect("request_permissions event missing");
|
||||
let EventMsg::RequestPermissions(request) = request_event.msg else {
|
||||
panic!("expected RequestPermissions event");
|
||||
};
|
||||
assert_eq!(request.call_id, call_id.clone());
|
||||
assert_eq!(request.environment_id.as_deref(), Some("remote"));
|
||||
assert_eq!(request.cwd, Some(delegated_cwd));
|
||||
|
||||
parent_session
|
||||
.notify_request_permissions_response(&call_id, expected_response.clone())
|
||||
.await;
|
||||
|
||||
timeout(Duration::from_secs(1), handle)
|
||||
.await
|
||||
.expect("handle_request_permissions hung")
|
||||
.expect("handle_request_permissions join error");
|
||||
|
||||
let submission = timeout(Duration::from_secs(1), rx_sub.recv())
|
||||
.await
|
||||
.expect("request_permissions response timed out")
|
||||
.expect("request_permissions response missing");
|
||||
let Op::RequestPermissionsResponse { id, response } = submission.op else {
|
||||
panic!("expected request permissions response");
|
||||
};
|
||||
assert_eq!(id, call_id);
|
||||
assert_eq!(response, expected_response);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn handle_request_user_input_preserves_non_blocking_flag_for_round_trip() {
|
||||
let (parent_session, parent_ctx, rx_events) =
|
||||
crate::session::tests::make_session_and_context_with_rx().await;
|
||||
*parent_session.active_turn.lock().await = Some(crate::state::ActiveTurn::default());
|
||||
|
||||
let (tx_sub, rx_sub) = bounded(SUBMISSION_CHANNEL_CAPACITY);
|
||||
let (_tx_events, rx_events_child) = bounded(SUBMISSION_CHANNEL_CAPACITY);
|
||||
let (_agent_status_tx, agent_status) = watch::channel(AgentStatus::PendingInit);
|
||||
let io = Arc::new(SessionIo {
|
||||
tx_sub,
|
||||
rx_event: rx_events_child,
|
||||
agent_status,
|
||||
session_loop_termination: completed_session_loop_termination(),
|
||||
});
|
||||
let pending_mcp_invocations = Arc::new(Mutex::new(HashMap::new()));
|
||||
let cancel_token = CancellationToken::new();
|
||||
let child_event_id = "child-request-1".to_string();
|
||||
let parent_user_input_id = parent_ctx.sub_id.clone();
|
||||
let expected_response = RequestUserInputResponse {
|
||||
answers: HashMap::from([(
|
||||
"q1".to_string(),
|
||||
RequestUserInputAnswer {
|
||||
answers: vec!["yes".to_string()],
|
||||
},
|
||||
)]),
|
||||
};
|
||||
|
||||
let handle = tokio::spawn({
|
||||
let io = Arc::clone(&io);
|
||||
let parent_session = Arc::clone(&parent_session);
|
||||
let parent_ctx = Arc::clone(&parent_ctx);
|
||||
let pending_mcp_invocations = Arc::clone(&pending_mcp_invocations);
|
||||
let cancel_token = cancel_token.clone();
|
||||
let child_event_id = child_event_id.clone();
|
||||
async move {
|
||||
handle_request_user_input(
|
||||
io.as_ref(),
|
||||
child_event_id,
|
||||
&parent_session,
|
||||
&parent_ctx,
|
||||
&pending_mcp_invocations,
|
||||
RequestUserInputEvent {
|
||||
call_id: "child-call-1".to_string(),
|
||||
turn_id: "child-turn-1".to_string(),
|
||||
questions: vec![RequestUserInputQuestion {
|
||||
id: "q1".to_string(),
|
||||
header: "Confirm".to_string(),
|
||||
question: "Continue?".to_string(),
|
||||
is_other: false,
|
||||
is_secret: false,
|
||||
options: None,
|
||||
}],
|
||||
is_blocking: false,
|
||||
auto_resolution_ms: None,
|
||||
},
|
||||
&cancel_token,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
});
|
||||
|
||||
let request_event = timeout(Duration::from_secs(1), rx_events.recv())
|
||||
.await
|
||||
.expect("request_user_input event timed out")
|
||||
.expect("request_user_input event missing");
|
||||
let EventMsg::RequestUserInput(request) = request_event.msg else {
|
||||
panic!("expected RequestUserInput event");
|
||||
};
|
||||
assert_eq!(request.call_id, parent_user_input_id);
|
||||
assert_eq!(request.is_blocking, false);
|
||||
|
||||
parent_session
|
||||
.notify_user_input_response(&parent_user_input_id, expected_response.clone())
|
||||
.await;
|
||||
|
||||
timeout(Duration::from_secs(1), handle)
|
||||
.await
|
||||
.expect("handle_request_user_input hung")
|
||||
.expect("handle_request_user_input join error");
|
||||
|
||||
let submission = timeout(Duration::from_secs(1), rx_sub.recv())
|
||||
.await
|
||||
.expect("request_user_input response timed out")
|
||||
.expect("request_user_input response missing");
|
||||
let Op::UserInputAnswer { id, response } = submission.op else {
|
||||
panic!("expected user input answer");
|
||||
};
|
||||
assert_eq!(id, child_event_id);
|
||||
assert_eq!(response, expected_response);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn handle_exec_approval_uses_call_id_for_guardian_review_and_approval_id_for_reply() {
|
||||
let (parent_session, parent_ctx, rx_events) =
|
||||
crate::session::tests::make_session_and_context_with_rx().await;
|
||||
let mut parent_ctx = Arc::try_unwrap(parent_ctx).expect("single turn context ref");
|
||||
let mut config = (*parent_ctx.config).clone();
|
||||
config.approvals_reviewer = ApprovalsReviewer::AutoReview;
|
||||
parent_ctx.config = Arc::new(config);
|
||||
Arc::make_mut(&mut parent_ctx.config)
|
||||
.permissions
|
||||
.approval_policy
|
||||
.set(AskForApproval::OnRequest)
|
||||
.expect("set on-request policy");
|
||||
let parent_ctx = Arc::new(parent_ctx);
|
||||
|
||||
let (tx_sub, rx_sub) = bounded(SUBMISSION_CHANNEL_CAPACITY);
|
||||
let (_tx_events, rx_events_child) = bounded(SUBMISSION_CHANNEL_CAPACITY);
|
||||
let (_agent_status_tx, agent_status) = watch::channel(AgentStatus::PendingInit);
|
||||
let io = Arc::new(SessionIo {
|
||||
tx_sub,
|
||||
rx_event: rx_events_child,
|
||||
agent_status,
|
||||
session_loop_termination: completed_session_loop_termination(),
|
||||
});
|
||||
|
||||
let cancel_token = CancellationToken::new();
|
||||
let handle = tokio::spawn({
|
||||
let io = Arc::clone(&io);
|
||||
let parent_session = Arc::clone(&parent_session);
|
||||
let parent_ctx = Arc::clone(&parent_ctx);
|
||||
let cancel_token = cancel_token.clone();
|
||||
async move {
|
||||
handle_exec_approval(
|
||||
io.as_ref(),
|
||||
"child-turn-1".to_string(),
|
||||
&parent_session,
|
||||
&parent_ctx,
|
||||
ExecApprovalRequestEvent {
|
||||
call_id: "command-item-1".to_string(),
|
||||
plugin_id: Some("sample@openai-curated".to_string()),
|
||||
script_path: Some("scripts/run.py".to_string()),
|
||||
approval_id: Some("callback-approval-1".to_string()),
|
||||
turn_id: "child-turn-1".to_string(),
|
||||
environment_id: Some("remote".to_string()),
|
||||
started_at_ms: 0,
|
||||
command: vec!["rm".to_string(), "-rf".to_string(), "tmp".to_string()],
|
||||
cwd: test_path_buf("/tmp").abs(),
|
||||
reason: Some("unsafe subcommand".to_string()),
|
||||
network_approval_context: None,
|
||||
proposed_execpolicy_amendment: None,
|
||||
proposed_network_policy_amendments: None,
|
||||
additional_permissions: None,
|
||||
available_decisions: Some(vec![
|
||||
ReviewDecision::Approved,
|
||||
ReviewDecision::Abort,
|
||||
]),
|
||||
parsed_cmd: Vec::new(),
|
||||
},
|
||||
&cancel_token,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
});
|
||||
|
||||
let assessment_event = timeout(Duration::from_secs(2), async {
|
||||
loop {
|
||||
let event = rx_events.recv().await.expect("guardian assessment event");
|
||||
if let EventMsg::GuardianAssessment(assessment) = event.msg {
|
||||
return assessment;
|
||||
}
|
||||
}
|
||||
})
|
||||
.await
|
||||
.expect("timed out waiting for guardian assessment");
|
||||
let expected_action = GuardianAssessmentAction::Command {
|
||||
source: GuardianCommandSource::Shell,
|
||||
command: "rm -rf tmp".to_string(),
|
||||
cwd: test_path_buf("/tmp").abs(),
|
||||
};
|
||||
assert!(!assessment_event.id.is_empty());
|
||||
assert_eq!(
|
||||
assessment_event.target_item_id.as_deref(),
|
||||
Some("command-item-1")
|
||||
);
|
||||
assert_eq!(
|
||||
assessment_event.plugin_id.as_deref(),
|
||||
Some("sample@openai-curated")
|
||||
);
|
||||
assert_eq!(
|
||||
assessment_event.script_path.as_deref(),
|
||||
Some("scripts/run.py")
|
||||
);
|
||||
assert_eq!(assessment_event.turn_id, parent_ctx.sub_id);
|
||||
assert_eq!(
|
||||
assessment_event.status,
|
||||
GuardianAssessmentStatus::InProgress
|
||||
);
|
||||
assert_eq!(assessment_event.risk_level, None);
|
||||
assert_eq!(assessment_event.user_authorization, None);
|
||||
assert_eq!(assessment_event.rationale, None);
|
||||
assert_eq!(assessment_event.decision_source, None);
|
||||
assert_eq!(assessment_event.action, expected_action);
|
||||
|
||||
cancel_token.cancel();
|
||||
|
||||
timeout(Duration::from_secs(2), handle)
|
||||
.await
|
||||
.expect("handle_exec_approval hung")
|
||||
.expect("handle_exec_approval join error");
|
||||
|
||||
let submission = timeout(Duration::from_secs(2), rx_sub.recv())
|
||||
.await
|
||||
.expect("exec approval response timed out")
|
||||
.expect("exec approval response missing");
|
||||
let Op::ExecApproval {
|
||||
id,
|
||||
turn_id,
|
||||
decision,
|
||||
} = submission.op
|
||||
else {
|
||||
panic!("expected exec approval");
|
||||
};
|
||||
assert_eq!(id, "callback-approval-1");
|
||||
assert_eq!(turn_id, Some("child-turn-1".to_string()));
|
||||
assert_eq!(decision, ReviewDecision::Abort);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn delegated_mcp_guardian_abort_returns_synthetic_decline_answer() {
|
||||
async fn run_codex_thread_interactive_rejects_approval_policy_that_can_prompt() {
|
||||
let (parent_session, parent_ctx, _rx_events) =
|
||||
crate::session::tests::make_session_and_context_with_rx().await;
|
||||
let mut parent_ctx = Arc::try_unwrap(parent_ctx).expect("single turn context ref");
|
||||
let mut config = (*parent_ctx.config).clone();
|
||||
config.approvals_reviewer = ApprovalsReviewer::AutoReview;
|
||||
parent_ctx.config = Arc::new(config);
|
||||
Arc::make_mut(&mut parent_ctx.config)
|
||||
.permissions
|
||||
.approval_policy
|
||||
.set(AskForApproval::OnRequest)
|
||||
.expect("set on-request policy");
|
||||
let parent_ctx = Arc::new(parent_ctx);
|
||||
let mut config = parent_ctx.config.as_ref().clone();
|
||||
config.permissions.approval_policy = Constrained::allow_any(AskForApproval::OnRequest);
|
||||
let parent_environments = parent_ctx.environments.clone();
|
||||
|
||||
let pending_mcp_invocations = Arc::new(Mutex::new(HashMap::from([(
|
||||
"call-1".to_string(),
|
||||
PendingMcpInvocation {
|
||||
invocation: McpInvocation {
|
||||
server: "custom_server".to_string(),
|
||||
tool: "dangerous_tool".to_string(),
|
||||
arguments: None,
|
||||
},
|
||||
metadata: None,
|
||||
},
|
||||
)])));
|
||||
let cancel_token = CancellationToken::new();
|
||||
cancel_token.cancel();
|
||||
|
||||
let response = maybe_auto_review_mcp_request_user_input(
|
||||
&parent_session,
|
||||
&parent_ctx,
|
||||
&pending_mcp_invocations,
|
||||
&RequestUserInputEvent {
|
||||
call_id: "call-1".to_string(),
|
||||
turn_id: "child-turn-1".to_string(),
|
||||
questions: vec![RequestUserInputQuestion {
|
||||
id: format!("{MCP_TOOL_APPROVAL_QUESTION_ID_PREFIX}_call-1"),
|
||||
header: "Approve app tool call?".to_string(),
|
||||
question: "Allow this app tool?".to_string(),
|
||||
is_other: false,
|
||||
is_secret: false,
|
||||
options: None,
|
||||
}],
|
||||
is_blocking: true,
|
||||
auto_resolution_ms: None,
|
||||
},
|
||||
&cancel_token,
|
||||
let result = run_codex_thread_interactive(
|
||||
config,
|
||||
Arc::clone(&parent_session.services.auth_manager),
|
||||
Arc::clone(&parent_session.services.models_manager),
|
||||
parent_session,
|
||||
parent_ctx,
|
||||
parent_environments,
|
||||
CancellationToken::new(),
|
||||
SubAgentSource::Review,
|
||||
/*initial_history*/ None,
|
||||
crate::session::GitEnrichmentPolicy::Fresh,
|
||||
codex_sandboxing::WindowsSandboxProxySettingsMode::Reconcile,
|
||||
)
|
||||
.await;
|
||||
|
||||
assert_eq!(
|
||||
response,
|
||||
Some(RequestUserInputResponse {
|
||||
answers: HashMap::from([(
|
||||
format!("{MCP_TOOL_APPROVAL_QUESTION_ID_PREFIX}_call-1"),
|
||||
RequestUserInputAnswer {
|
||||
answers: vec![MCP_TOOL_APPROVAL_DECLINE_SYNTHETIC.to_string()],
|
||||
},
|
||||
)]),
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn delegated_mcp_user_reviewer_returns_none_without_metadata() {
|
||||
let (parent_session, parent_ctx, _rx_events) =
|
||||
crate::session::tests::make_session_and_context_with_rx().await;
|
||||
let pending_mcp_invocations = Arc::new(Mutex::new(HashMap::from([(
|
||||
"call-1".to_string(),
|
||||
PendingMcpInvocation {
|
||||
invocation: McpInvocation {
|
||||
server: CODEX_APPS_MCP_SERVER_NAME.to_string(),
|
||||
tool: "dangerous_tool".to_string(),
|
||||
arguments: None,
|
||||
},
|
||||
metadata: None,
|
||||
},
|
||||
)])));
|
||||
let cancel_token = CancellationToken::new();
|
||||
|
||||
let event = RequestUserInputEvent {
|
||||
call_id: "call-1".to_string(),
|
||||
turn_id: "child-turn-1".to_string(),
|
||||
questions: vec![RequestUserInputQuestion {
|
||||
id: format!("{MCP_TOOL_APPROVAL_QUESTION_ID_PREFIX}_call-1"),
|
||||
header: "Approve app tool call?".to_string(),
|
||||
question: "Allow this app tool?".to_string(),
|
||||
is_other: false,
|
||||
is_secret: false,
|
||||
options: None,
|
||||
}],
|
||||
is_blocking: true,
|
||||
auto_resolution_ms: None,
|
||||
};
|
||||
let response = maybe_auto_review_mcp_request_user_input(
|
||||
&parent_session,
|
||||
&parent_ctx,
|
||||
&pending_mcp_invocations,
|
||||
&event,
|
||||
&cancel_token,
|
||||
)
|
||||
.await;
|
||||
assert_eq!(response, None);
|
||||
assert!(matches!(
|
||||
result,
|
||||
Err(err)
|
||||
if matches!(
|
||||
err.details(),
|
||||
CodexErrorDetails::InvalidRequest(message)
|
||||
if message == "Codex delegates require approval policy `never`"
|
||||
)
|
||||
));
|
||||
}
|
||||
Reference in new issue
Block a user