mirror of
https://github.com/openai/codex.git
synced 2026-10-19 16:17:49 +00:00
Distinguish MCP approval cancellation reasons (#54679)
## Why MCP tool calls reported every aborted approval as "user cancelled MCP tool call", including interrupted requests and automatic cancellations in `exec` mode. ## What changed Carry `user`, `interrupted`, or `non_interactive` cancellation reasons through MCP approval metadata and resolution. Mark explicit TUI cancellations as user actions, interrupted requests as interruptions, and automatic `exec` cancellations as non-interactive. Use the reason to report when a call was cancelled before approval or because interactive approval is unavailable in `exec` mode. Preserve the existing cancellation message for clients that omit the reason. ## Testing Update tests to verify that a closed approval response carries the interruption reason and that `exec` cancellation responses include the non-interactive reason. GitOrigin-RevId: be2a5f7fef539b5db2add53b46aea27e2b8a66fc
This commit is contained in:
@@ -94,6 +94,10 @@ use codex_protocol::ThreadId;
|
|||||||
use codex_protocol::items::CollabAgentTool as CoreCollabAgentTool;
|
use codex_protocol::items::CollabAgentTool as CoreCollabAgentTool;
|
||||||
use codex_protocol::items::ModelInvocationContext;
|
use codex_protocol::items::ModelInvocationContext;
|
||||||
use codex_protocol::items::TurnItem as CoreTurnItem;
|
use codex_protocol::items::TurnItem as CoreTurnItem;
|
||||||
|
use codex_protocol::mcp_approval_meta::APPROVAL_KIND_KEY;
|
||||||
|
use codex_protocol::mcp_approval_meta::APPROVAL_KIND_MCP_TOOL_CALL;
|
||||||
|
use codex_protocol::mcp_approval_meta::CANCELLATION_REASON_KEY;
|
||||||
|
use codex_protocol::mcp_approval_meta::McpApprovalCancellationReason;
|
||||||
use codex_protocol::models::AdditionalPermissionProfile as CoreAdditionalPermissionProfile;
|
use codex_protocol::models::AdditionalPermissionProfile as CoreAdditionalPermissionProfile;
|
||||||
use codex_protocol::plan_tool::UpdatePlanArgs;
|
use codex_protocol::plan_tool::UpdatePlanArgs;
|
||||||
use codex_protocol::protocol::Event;
|
use codex_protocol::protocol::Event;
|
||||||
@@ -874,6 +878,11 @@ pub(crate) async fn apply_bespoke_event_handling(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
EventMsg::ElicitationRequest(request) => {
|
EventMsg::ElicitationRequest(request) => {
|
||||||
|
let mcp_tool_approval = matches!(
|
||||||
|
&request.request,
|
||||||
|
codex_protocol::approvals::ElicitationRequest::Form { meta: Some(meta), .. }
|
||||||
|
if meta.get(APPROVAL_KIND_KEY).and_then(serde_json::Value::as_str) == Some(APPROVAL_KIND_MCP_TOOL_CALL)
|
||||||
|
);
|
||||||
let user_verification = matches!(
|
let user_verification = matches!(
|
||||||
&request.request,
|
&request.request,
|
||||||
codex_protocol::approvals::ElicitationRequest::UserVerification { .. }
|
codex_protocol::approvals::ElicitationRequest::UserVerification { .. }
|
||||||
@@ -930,6 +939,7 @@ pub(crate) async fn apply_bespoke_event_handling(
|
|||||||
PendingMcpElicitationResponse {
|
PendingMcpElicitationResponse {
|
||||||
receiver: rx,
|
receiver: rx,
|
||||||
user_verification,
|
user_verification,
|
||||||
|
mcp_tool_approval,
|
||||||
},
|
},
|
||||||
conversation,
|
conversation,
|
||||||
thread_state,
|
thread_state,
|
||||||
@@ -1723,6 +1733,7 @@ async fn on_request_user_input_response(
|
|||||||
struct PendingMcpElicitationResponse {
|
struct PendingMcpElicitationResponse {
|
||||||
receiver: oneshot::Receiver<ClientRequestResult>,
|
receiver: oneshot::Receiver<ClientRequestResult>,
|
||||||
user_verification: bool,
|
user_verification: bool,
|
||||||
|
mcp_tool_approval: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn on_mcp_server_elicitation_response(
|
async fn on_mcp_server_elicitation_response(
|
||||||
@@ -1737,12 +1748,30 @@ async fn on_mcp_server_elicitation_response(
|
|||||||
let response = pending.receiver.await;
|
let response = pending.receiver.await;
|
||||||
resolve_server_request_on_thread_listener(&thread_state, pending_request_id).await;
|
resolve_server_request_on_thread_listener(&thread_state, pending_request_id).await;
|
||||||
drop(permission_guard);
|
drop(permission_guard);
|
||||||
let response = if pending.user_verification {
|
let interrupted = !matches!(&response, Ok(Ok(_)));
|
||||||
|
let mut response = if pending.user_verification {
|
||||||
crate::user_verification_response::from_client_result(response)
|
crate::user_verification_response::from_client_result(response)
|
||||||
} else {
|
} else {
|
||||||
mcp_server_elicitation_response_from_client_result(response)
|
mcp_server_elicitation_response_from_client_result(response)
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if pending.mcp_tool_approval
|
||||||
|
&& interrupted
|
||||||
|
&& response.action == McpServerElicitationAction::Cancel
|
||||||
|
{
|
||||||
|
// Only the synthesized response is ours to attribute. Successful client
|
||||||
|
// replies retain their provenance, including an absent reason.
|
||||||
|
let mut meta = match response.meta.take() {
|
||||||
|
Some(serde_json::Value::Object(meta)) => meta,
|
||||||
|
_ => serde_json::Map::new(),
|
||||||
|
};
|
||||||
|
meta.insert(
|
||||||
|
CANCELLATION_REASON_KEY.to_string(),
|
||||||
|
serde_json::json!(McpApprovalCancellationReason::Interrupted),
|
||||||
|
);
|
||||||
|
response.meta = Some(serde_json::Value::Object(meta));
|
||||||
|
}
|
||||||
|
|
||||||
if let Err(err) = conversation
|
if let Err(err) = conversation
|
||||||
.submit(Op::ResolveElicitation {
|
.submit(Op::ResolveElicitation {
|
||||||
server_name,
|
server_name,
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ use crate::session::session::Session;
|
|||||||
use crate::session::step_context::StepContext;
|
use crate::session::step_context::StepContext;
|
||||||
use crate::session::turn_context::TurnContext;
|
use crate::session::turn_context::TurnContext;
|
||||||
use crate::tools::ApprovalContext;
|
use crate::tools::ApprovalContext;
|
||||||
|
use crate::tools::ApprovalResolution;
|
||||||
use crate::tools::ApprovalResolutionSource;
|
use crate::tools::ApprovalResolutionSource;
|
||||||
use crate::tools::context::ToolCallOrigin;
|
use crate::tools::context::ToolCallOrigin;
|
||||||
use crate::tools::hook_names::HookToolName;
|
use crate::tools::hook_names::HookToolName;
|
||||||
@@ -60,9 +61,11 @@ use codex_protocol::mcp::is_node_repl_backed_connector;
|
|||||||
use codex_protocol::mcp::is_node_repl_backed_server;
|
use codex_protocol::mcp::is_node_repl_backed_server;
|
||||||
use codex_protocol::mcp_approval_meta::APPROVAL_KIND_KEY as MCP_TOOL_APPROVAL_KIND_KEY;
|
use codex_protocol::mcp_approval_meta::APPROVAL_KIND_KEY as MCP_TOOL_APPROVAL_KIND_KEY;
|
||||||
use codex_protocol::mcp_approval_meta::APPROVAL_KIND_MCP_TOOL_CALL as MCP_TOOL_APPROVAL_KIND_MCP_TOOL_CALL;
|
use codex_protocol::mcp_approval_meta::APPROVAL_KIND_MCP_TOOL_CALL as MCP_TOOL_APPROVAL_KIND_MCP_TOOL_CALL;
|
||||||
|
use codex_protocol::mcp_approval_meta::CANCELLATION_REASON_KEY;
|
||||||
use codex_protocol::mcp_approval_meta::CONNECTOR_DESCRIPTION_KEY as MCP_TOOL_APPROVAL_CONNECTOR_DESCRIPTION_KEY;
|
use codex_protocol::mcp_approval_meta::CONNECTOR_DESCRIPTION_KEY as MCP_TOOL_APPROVAL_CONNECTOR_DESCRIPTION_KEY;
|
||||||
use codex_protocol::mcp_approval_meta::CONNECTOR_ID_KEY as MCP_TOOL_APPROVAL_CONNECTOR_ID_KEY;
|
use codex_protocol::mcp_approval_meta::CONNECTOR_ID_KEY as MCP_TOOL_APPROVAL_CONNECTOR_ID_KEY;
|
||||||
use codex_protocol::mcp_approval_meta::CONNECTOR_NAME_KEY as MCP_TOOL_APPROVAL_CONNECTOR_NAME_KEY;
|
use codex_protocol::mcp_approval_meta::CONNECTOR_NAME_KEY as MCP_TOOL_APPROVAL_CONNECTOR_NAME_KEY;
|
||||||
|
use codex_protocol::mcp_approval_meta::McpApprovalCancellationReason;
|
||||||
use codex_protocol::mcp_approval_meta::PERSIST_ALWAYS as MCP_TOOL_APPROVAL_PERSIST_ALWAYS;
|
use codex_protocol::mcp_approval_meta::PERSIST_ALWAYS as MCP_TOOL_APPROVAL_PERSIST_ALWAYS;
|
||||||
use codex_protocol::mcp_approval_meta::PERSIST_KEY as MCP_TOOL_APPROVAL_PERSIST_KEY;
|
use codex_protocol::mcp_approval_meta::PERSIST_KEY as MCP_TOOL_APPROVAL_PERSIST_KEY;
|
||||||
use codex_protocol::mcp_approval_meta::PERSIST_SESSION as MCP_TOOL_APPROVAL_PERSIST_SESSION;
|
use codex_protocol::mcp_approval_meta::PERSIST_SESSION as MCP_TOOL_APPROVAL_PERSIST_SESSION;
|
||||||
@@ -344,7 +347,20 @@ pub(crate) async fn handle_mcp_tool_call(
|
|||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
ReviewDecision::Abort => {
|
ReviewDecision::Abort => {
|
||||||
let message = "user cancelled MCP tool call".to_string();
|
let message = match approval.cancellation_reason {
|
||||||
|
// Older clients do not send provenance. Preserve their
|
||||||
|
// cancellation behavior without assigning them a user reason.
|
||||||
|
Some(McpApprovalCancellationReason::User) | None => {
|
||||||
|
"user cancelled MCP tool call"
|
||||||
|
}
|
||||||
|
Some(McpApprovalCancellationReason::Interrupted) => {
|
||||||
|
"MCP tool call cancelled before approval"
|
||||||
|
}
|
||||||
|
Some(McpApprovalCancellationReason::NonInteractive) => {
|
||||||
|
"MCP tool call cancelled: interactive approval is unavailable in exec mode"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.to_string();
|
||||||
notify_mcp_tool_call_skip(
|
notify_mcp_tool_call_skip(
|
||||||
sess.as_ref(),
|
sess.as_ref(),
|
||||||
turn_context.as_ref(),
|
turn_context.as_ref(),
|
||||||
@@ -1484,6 +1500,7 @@ fn mcp_tool_approval_prompt_options(
|
|||||||
struct McpToolApproval {
|
struct McpToolApproval {
|
||||||
decision: ReviewDecision,
|
decision: ReviewDecision,
|
||||||
source: Option<ApprovalResolutionSource>,
|
source: Option<ApprovalResolutionSource>,
|
||||||
|
cancellation_reason: Option<McpApprovalCancellationReason>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[expect(clippy::too_many_arguments)]
|
#[expect(clippy::too_many_arguments)]
|
||||||
@@ -1544,6 +1561,7 @@ async fn maybe_request_mcp_tool_approval(
|
|||||||
return Some(McpToolApproval {
|
return Some(McpToolApproval {
|
||||||
decision: ReviewDecision::Approved,
|
decision: ReviewDecision::Approved,
|
||||||
source: None,
|
source: None,
|
||||||
|
cancellation_reason: None,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1593,6 +1611,7 @@ async fn maybe_request_mcp_tool_approval(
|
|||||||
Err(ToolError::Codex(_)) => ReviewDecision::Abort,
|
Err(ToolError::Codex(_)) => ReviewDecision::Abort,
|
||||||
},
|
},
|
||||||
source: approval.source,
|
source: approval.source,
|
||||||
|
cancellation_reason: approval.cancellation_reason,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1601,7 +1620,7 @@ pub(crate) async fn request_mcp_tool_user_approval(
|
|||||||
turn_context: &TurnContext,
|
turn_context: &TurnContext,
|
||||||
call_id: &str,
|
call_id: &str,
|
||||||
action: &ApprovalAction,
|
action: &ApprovalAction,
|
||||||
) -> ReviewDecision {
|
) -> ApprovalResolution {
|
||||||
let ApprovalAction::McpToolCall {
|
let ApprovalAction::McpToolCall {
|
||||||
id,
|
id,
|
||||||
server,
|
server,
|
||||||
@@ -1624,9 +1643,13 @@ pub(crate) async fn request_mcp_tool_user_approval(
|
|||||||
};
|
};
|
||||||
|
|
||||||
if *approval_policy == AskForApproval::Never {
|
if *approval_policy == AskForApproval::Never {
|
||||||
return ReviewDecision::denied(
|
return ApprovalResolution {
|
||||||
"MCP tool call requires approval, but approval policy is never",
|
decision: ReviewDecision::denied(
|
||||||
);
|
"MCP tool call requires approval, but approval policy is never",
|
||||||
|
),
|
||||||
|
source: ApprovalResolutionSource::User,
|
||||||
|
cancellation_reason: None,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
let tool_call_mcp_elicitation_enabled = turn_context
|
let tool_call_mcp_elicitation_enabled = turn_context
|
||||||
@@ -1660,7 +1683,7 @@ pub(crate) async fn request_mcp_tool_user_approval(
|
|||||||
.as_ref()
|
.as_ref()
|
||||||
.map(|rendered_template| rendered_template.question.as_str()),
|
.map(|rendered_template| rendered_template.question.as_str()),
|
||||||
);
|
);
|
||||||
let (request_dispatched, decision) = if tool_call_mcp_elicitation_enabled {
|
let (request_dispatched, decision, cancellation_reason) = if tool_call_mcp_elicitation_enabled {
|
||||||
let link_id = sess
|
let link_id = sess
|
||||||
.mcp_tool_approval_metadata(server, id)
|
.mcp_tool_approval_metadata(server, id)
|
||||||
.and_then(|context| context.metadata.link_id);
|
.and_then(|context| context.metadata.link_id);
|
||||||
@@ -1698,17 +1721,33 @@ pub(crate) async fn request_mcp_tool_user_approval(
|
|||||||
let outcome = sess
|
let outcome = sess
|
||||||
.request_mcp_server_elicitation(turn_context, server.clone(), request_id, request)
|
.request_mcp_server_elicitation(turn_context, server.clone(), request_id, request)
|
||||||
.await;
|
.await;
|
||||||
|
// Missing replies are interruptions. A received Cancel needs provenance
|
||||||
|
// from its source; RPC success alone does not establish a user action.
|
||||||
|
let cancellation_reason = match outcome.response.as_ref() {
|
||||||
|
None => Some(McpApprovalCancellationReason::Interrupted),
|
||||||
|
Some(response) if response.action == ElicitationAction::Cancel => response
|
||||||
|
.meta
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|meta| meta.get(CANCELLATION_REASON_KEY))
|
||||||
|
.and_then(|reason| serde_json::from_value(reason.clone()).ok()),
|
||||||
|
Some(_) => None,
|
||||||
|
};
|
||||||
(
|
(
|
||||||
outcome.sent,
|
outcome.sent,
|
||||||
parse_mcp_tool_approval_elicitation_response(outcome.response, &question_id),
|
parse_mcp_tool_approval_elicitation_response(outcome.response, &question_id),
|
||||||
|
cancellation_reason,
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
if turn_context.session_source.is_non_root_agent() {
|
if turn_context.session_source.is_non_root_agent() {
|
||||||
return ReviewDecision::denied(concat!(
|
return ApprovalResolution {
|
||||||
"MCP tool approval via request_user_input requires the root thread. ",
|
decision: ReviewDecision::denied(concat!(
|
||||||
"Ask the parent agent to handle this request. ",
|
"MCP tool approval via request_user_input requires the root thread. ",
|
||||||
"Do not retry the blocked action until the parent confirms the blocker is resolved."
|
"Ask the parent agent to handle this request. ",
|
||||||
));
|
"Do not retry the blocked action until the parent confirms the blocker is resolved."
|
||||||
|
)),
|
||||||
|
source: ApprovalResolutionSource::User,
|
||||||
|
cancellation_reason: None,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
let args = RequestUserInputArgs {
|
let args = RequestUserInputArgs {
|
||||||
questions: vec![question],
|
questions: vec![question],
|
||||||
@@ -1718,12 +1757,28 @@ pub(crate) async fn request_mcp_tool_user_approval(
|
|||||||
let response = sess
|
let response = sess
|
||||||
.request_user_input(turn_context, call_id.to_string(), args)
|
.request_user_input(turn_context, call_id.to_string(), args)
|
||||||
.await;
|
.await;
|
||||||
|
let cancellation_reason = response
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|accepted| accepted.response.answers.get(&question_id))
|
||||||
|
.filter(|answer| {
|
||||||
|
answer
|
||||||
|
.answers
|
||||||
|
.iter()
|
||||||
|
.any(|answer| answer == MCP_TOOL_APPROVAL_CANCEL)
|
||||||
|
})
|
||||||
|
.map(|_| McpApprovalCancellationReason::User)
|
||||||
|
.or_else(|| {
|
||||||
|
response
|
||||||
|
.is_none()
|
||||||
|
.then_some(McpApprovalCancellationReason::Interrupted)
|
||||||
|
});
|
||||||
(
|
(
|
||||||
true,
|
true,
|
||||||
parse_mcp_tool_approval_response(
|
parse_mcp_tool_approval_response(
|
||||||
response.map(|accepted| accepted.response),
|
response.map(|accepted| accepted.response),
|
||||||
&question_id,
|
&question_id,
|
||||||
),
|
),
|
||||||
|
cancellation_reason,
|
||||||
)
|
)
|
||||||
};
|
};
|
||||||
let decision = normalize_approval_decision_for_mode(decision, *approval_mode);
|
let decision = normalize_approval_decision_for_mode(decision, *approval_mode);
|
||||||
@@ -1735,7 +1790,15 @@ pub(crate) async fn request_mcp_tool_user_approval(
|
|||||||
{
|
{
|
||||||
track_mcp_tool_call_elicitation(sess, turn_context, call_id, ElicitationType::Approval);
|
track_mcp_tool_call_elicitation(sess, turn_context, call_id, ElicitationType::Approval);
|
||||||
}
|
}
|
||||||
decision
|
ApprovalResolution {
|
||||||
|
cancellation_reason: if decision == ReviewDecision::Abort {
|
||||||
|
cancellation_reason
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
},
|
||||||
|
decision,
|
||||||
|
source: ApprovalResolutionSource::User,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn session_mcp_tool_approval_key(
|
fn session_mcp_tool_approval_key(
|
||||||
|
|||||||
@@ -2330,7 +2330,11 @@ async fn dispatched_mcp_approval_with_closed_response_is_classified_as_approval(
|
|||||||
*session.active_turn.lock().await = None;
|
*session.active_turn.lock().await = None;
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
approval.await.expect("approval task should complete"),
|
approval.await.expect("approval task should complete"),
|
||||||
ReviewDecision::Abort
|
ApprovalResolution {
|
||||||
|
decision: ReviewDecision::Abort,
|
||||||
|
source: ApprovalResolutionSource::User,
|
||||||
|
cancellation_reason: Some(McpApprovalCancellationReason::Interrupted),
|
||||||
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
item["status"] = serde_json::json!("failed");
|
item["status"] = serde_json::json!("failed");
|
||||||
@@ -3002,6 +3006,7 @@ async fn permission_request_hook_allows_mcp_tool_call() {
|
|||||||
Some(McpToolApproval {
|
Some(McpToolApproval {
|
||||||
decision: ReviewDecision::Approved,
|
decision: ReviewDecision::Approved,
|
||||||
source: Some(ApprovalResolutionSource::Hook),
|
source: Some(ApprovalResolutionSource::Hook),
|
||||||
|
cancellation_reason: None,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
let log = std::fs::read_to_string(log_path).expect("read MCP permission hook log");
|
let log = std::fs::read_to_string(log_path).expect("read MCP permission hook log");
|
||||||
@@ -3079,6 +3084,7 @@ async fn permission_request_hook_uses_hook_tool_name_without_metadata() {
|
|||||||
Some(McpToolApproval {
|
Some(McpToolApproval {
|
||||||
decision: ReviewDecision::Approved,
|
decision: ReviewDecision::Approved,
|
||||||
source: Some(ApprovalResolutionSource::Hook),
|
source: Some(ApprovalResolutionSource::Hook),
|
||||||
|
cancellation_reason: None,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
let log = std::fs::read_to_string(log_path).expect("read MCP permission hook log");
|
let log = std::fs::read_to_string(log_path).expect("read MCP permission hook log");
|
||||||
@@ -3172,6 +3178,7 @@ async fn permission_request_hook_runs_after_remembered_mcp_approval() {
|
|||||||
Some(McpToolApproval {
|
Some(McpToolApproval {
|
||||||
decision: ReviewDecision::Approved,
|
decision: ReviewDecision::Approved,
|
||||||
source: None,
|
source: None,
|
||||||
|
cancellation_reason: None,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
assert!(
|
assert!(
|
||||||
@@ -3276,6 +3283,7 @@ async fn strict_auto_review_forces_guardian_for_mcp_policy_skip() {
|
|||||||
let Some(McpToolApproval {
|
let Some(McpToolApproval {
|
||||||
decision: ReviewDecision::Denied { rejection: message },
|
decision: ReviewDecision::Denied { rejection: message },
|
||||||
source: Some(ApprovalResolutionSource::Guardian),
|
source: Some(ApprovalResolutionSource::Guardian),
|
||||||
|
cancellation_reason: None,
|
||||||
}) = decision
|
}) = decision
|
||||||
else {
|
else {
|
||||||
panic!("guardian-denied MCP approval should carry a rejection message");
|
panic!("guardian-denied MCP approval should carry a rejection message");
|
||||||
@@ -3391,6 +3399,7 @@ async fn assert_mcp_user_approval_persistence(
|
|||||||
Some(McpToolApproval {
|
Some(McpToolApproval {
|
||||||
decision: expected_decision,
|
decision: expected_decision,
|
||||||
source: Some(ApprovalResolutionSource::User),
|
source: Some(ApprovalResolutionSource::User),
|
||||||
|
cancellation_reason: None,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ use codex_protocol::approvals::NetworkApprovalContext;
|
|||||||
use codex_protocol::approvals::NetworkApprovalProtocol;
|
use codex_protocol::approvals::NetworkApprovalProtocol;
|
||||||
use codex_protocol::config_types::ApprovalsReviewer;
|
use codex_protocol::config_types::ApprovalsReviewer;
|
||||||
use codex_protocol::error::CodexErr;
|
use codex_protocol::error::CodexErr;
|
||||||
|
use codex_protocol::mcp_approval_meta::McpApprovalCancellationReason;
|
||||||
use codex_protocol::models::AdditionalPermissionProfile;
|
use codex_protocol::models::AdditionalPermissionProfile;
|
||||||
use codex_protocol::openai_models::ModelInfo;
|
use codex_protocol::openai_models::ModelInfo;
|
||||||
use codex_protocol::protocol::AskForApproval;
|
use codex_protocol::protocol::AskForApproval;
|
||||||
@@ -404,12 +405,14 @@ pub(crate) enum ApprovalResolutionSource {
|
|||||||
pub(crate) struct ApprovalResult {
|
pub(crate) struct ApprovalResult {
|
||||||
pub(crate) result: Result<ReviewDecision, ToolError>,
|
pub(crate) result: Result<ReviewDecision, ToolError>,
|
||||||
pub(crate) source: Option<ApprovalResolutionSource>,
|
pub(crate) source: Option<ApprovalResolutionSource>,
|
||||||
|
pub(crate) cancellation_reason: Option<McpApprovalCancellationReason>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||||
struct ApprovalResolution {
|
pub(crate) struct ApprovalResolution {
|
||||||
decision: ReviewDecision,
|
pub(crate) decision: ReviewDecision,
|
||||||
source: ApprovalResolutionSource,
|
pub(crate) source: ApprovalResolutionSource,
|
||||||
|
pub(crate) cancellation_reason: Option<McpApprovalCancellationReason>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ApprovalResolution {
|
impl ApprovalResolution {
|
||||||
@@ -466,6 +469,7 @@ impl Session {
|
|||||||
return ApprovalResult {
|
return ApprovalResult {
|
||||||
result: Err(ToolError::Rejected(reason.to_string())),
|
result: Err(ToolError::Rejected(reason.to_string())),
|
||||||
source: None,
|
source: None,
|
||||||
|
cancellation_reason: None,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
let is_mcp_tool_call = matches!(&action, ApprovalAction::McpToolCall { .. });
|
let is_mcp_tool_call = matches!(&action, ApprovalAction::McpToolCall { .. });
|
||||||
@@ -490,10 +494,12 @@ impl Session {
|
|||||||
Some(PermissionRequestDecision::Allow) => ApprovalResolution {
|
Some(PermissionRequestDecision::Allow) => ApprovalResolution {
|
||||||
decision: ReviewDecision::Approved,
|
decision: ReviewDecision::Approved,
|
||||||
source: ApprovalResolutionSource::Hook,
|
source: ApprovalResolutionSource::Hook,
|
||||||
|
cancellation_reason: None,
|
||||||
},
|
},
|
||||||
Some(PermissionRequestDecision::Deny { message }) => ApprovalResolution {
|
Some(PermissionRequestDecision::Deny { message }) => ApprovalResolution {
|
||||||
decision: ReviewDecision::denied(message),
|
decision: ReviewDecision::denied(message),
|
||||||
source: ApprovalResolutionSource::Hook,
|
source: ApprovalResolutionSource::Hook,
|
||||||
|
cancellation_reason: None,
|
||||||
},
|
},
|
||||||
None => self.request_reviewer_approval(action, &ctx).await,
|
None => self.request_reviewer_approval(action, &ctx).await,
|
||||||
};
|
};
|
||||||
@@ -502,10 +508,12 @@ impl Session {
|
|||||||
record_resolution(&ctx, &resolution);
|
record_resolution(&ctx, &resolution);
|
||||||
}
|
}
|
||||||
let source = Some(resolution.source);
|
let source = Some(resolution.source);
|
||||||
|
let cancellation_reason = resolution.cancellation_reason;
|
||||||
if is_mcp_tool_call && resolution.decision == ReviewDecision::ApprovedMcpPolicyAmendment {
|
if is_mcp_tool_call && resolution.decision == ReviewDecision::ApprovedMcpPolicyAmendment {
|
||||||
return ApprovalResult {
|
return ApprovalResult {
|
||||||
result: Ok(resolution.decision),
|
result: Ok(resolution.decision),
|
||||||
source,
|
source,
|
||||||
|
cancellation_reason,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
if is_network_approval {
|
if is_network_approval {
|
||||||
@@ -519,6 +527,7 @@ impl Session {
|
|||||||
return ApprovalResult {
|
return ApprovalResult {
|
||||||
result: Ok(resolution.decision),
|
result: Ok(resolution.decision),
|
||||||
source,
|
source,
|
||||||
|
cancellation_reason,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
(ReviewDecision::Abort, ApprovalResolutionSource::Guardian) => {
|
(ReviewDecision::Abort, ApprovalResolutionSource::Guardian) => {
|
||||||
@@ -527,6 +536,7 @@ impl Session {
|
|||||||
"automatic approval review was cancelled".to_string(),
|
"automatic approval review was cancelled".to_string(),
|
||||||
)),
|
)),
|
||||||
source,
|
source,
|
||||||
|
cancellation_reason,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
@@ -535,6 +545,7 @@ impl Session {
|
|||||||
ApprovalResult {
|
ApprovalResult {
|
||||||
result: resolution.into_tool_result(ctx.review_context.turn().model_info()),
|
result: resolution.into_tool_result(ctx.review_context.turn().model_info()),
|
||||||
source,
|
source,
|
||||||
|
cancellation_reason,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -545,14 +556,13 @@ impl Session {
|
|||||||
) -> ApprovalResolution {
|
) -> ApprovalResolution {
|
||||||
if let Some(decision) = self.request_guardian_approval(action.clone(), ctx).await {
|
if let Some(decision) = self.request_guardian_approval(action.clone(), ctx).await {
|
||||||
ApprovalResolution {
|
ApprovalResolution {
|
||||||
|
cancellation_reason: (decision == ReviewDecision::Abort)
|
||||||
|
.then_some(McpApprovalCancellationReason::Interrupted),
|
||||||
decision,
|
decision,
|
||||||
source: ApprovalResolutionSource::Guardian,
|
source: ApprovalResolutionSource::Guardian,
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
ApprovalResolution {
|
self.request_user_approval(&action, ctx).await
|
||||||
decision: self.request_user_approval(&action, ctx).await,
|
|
||||||
source: ApprovalResolutionSource::User,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -667,8 +677,8 @@ impl Session {
|
|||||||
&self,
|
&self,
|
||||||
action: &ApprovalAction,
|
action: &ApprovalAction,
|
||||||
ctx: &ApprovalContext,
|
ctx: &ApprovalContext,
|
||||||
) -> ReviewDecision {
|
) -> ApprovalResolution {
|
||||||
match action {
|
let decision = match action {
|
||||||
ApprovalAction::ExecCommand {
|
ApprovalAction::ExecCommand {
|
||||||
environment_id,
|
environment_id,
|
||||||
command,
|
command,
|
||||||
@@ -760,44 +770,43 @@ impl Session {
|
|||||||
.clone()
|
.clone()
|
||||||
.or_else(|| ctx.approval_reason.clone());
|
.or_else(|| ctx.approval_reason.clone());
|
||||||
if *permissions_preapproved && reason.is_none() {
|
if *permissions_preapproved && reason.is_none() {
|
||||||
return ReviewDecision::Approved;
|
ReviewDecision::Approved
|
||||||
|
} else if reason.is_some() {
|
||||||
|
self.request_patch_approval(
|
||||||
|
ctx.review_context.turn(),
|
||||||
|
ctx.call_id.clone(),
|
||||||
|
changes.as_ref().clone(),
|
||||||
|
reason,
|
||||||
|
/*grant_root*/ None,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
} else {
|
||||||
|
with_cached_approval(
|
||||||
|
&self.services,
|
||||||
|
"apply_patch",
|
||||||
|
action.cache_keys(),
|
||||||
|
|| async {
|
||||||
|
self.request_patch_approval(
|
||||||
|
ctx.review_context.turn(),
|
||||||
|
ctx.call_id.clone(),
|
||||||
|
changes.as_ref().clone(),
|
||||||
|
/*reason*/ None,
|
||||||
|
/*grant_root*/ None,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await
|
||||||
}
|
}
|
||||||
if reason.is_some() {
|
|
||||||
return self
|
|
||||||
.request_patch_approval(
|
|
||||||
ctx.review_context.turn(),
|
|
||||||
ctx.call_id.clone(),
|
|
||||||
changes.as_ref().clone(),
|
|
||||||
reason,
|
|
||||||
/*grant_root*/ None,
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
}
|
|
||||||
with_cached_approval(
|
|
||||||
&self.services,
|
|
||||||
"apply_patch",
|
|
||||||
action.cache_keys(),
|
|
||||||
|| async {
|
|
||||||
self.request_patch_approval(
|
|
||||||
ctx.review_context.turn(),
|
|
||||||
ctx.call_id.clone(),
|
|
||||||
changes.as_ref().clone(),
|
|
||||||
/*reason*/ None,
|
|
||||||
/*grant_root*/ None,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
}
|
}
|
||||||
ApprovalAction::McpToolCall { .. } => {
|
ApprovalAction::McpToolCall { .. } => {
|
||||||
request_mcp_tool_user_approval(
|
return request_mcp_tool_user_approval(
|
||||||
self,
|
self,
|
||||||
ctx.review_context.turn(),
|
ctx.review_context.turn(),
|
||||||
&ctx.call_id,
|
&ctx.call_id,
|
||||||
action,
|
action,
|
||||||
)
|
)
|
||||||
.await
|
.await;
|
||||||
}
|
}
|
||||||
ApprovalAction::NetworkAccess {
|
ApprovalAction::NetworkAccess {
|
||||||
environment_id,
|
environment_id,
|
||||||
@@ -826,6 +835,11 @@ impl Session {
|
|||||||
ApprovalAction::RequestPermissions { .. } => {
|
ApprovalAction::RequestPermissions { .. } => {
|
||||||
unreachable!("permission requests are routed directly to Guardian")
|
unreachable!("permission requests are routed directly to Guardian")
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
ApprovalResolution {
|
||||||
|
decision,
|
||||||
|
source: ApprovalResolutionSource::User,
|
||||||
|
cancellation_reason: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ fn approval_resolution_rejects_denied_network_policy_amendment() {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
source: ApprovalResolutionSource::User,
|
source: ApprovalResolutionSource::User,
|
||||||
|
cancellation_reason: None,
|
||||||
};
|
};
|
||||||
|
|
||||||
assert!(matches!(
|
assert!(matches!(
|
||||||
@@ -27,6 +28,7 @@ fn approval_resolution_rejects_mcp_policy_amendment() {
|
|||||||
let resolution = ApprovalResolution {
|
let resolution = ApprovalResolution {
|
||||||
decision: ReviewDecision::ApprovedMcpPolicyAmendment,
|
decision: ReviewDecision::ApprovedMcpPolicyAmendment,
|
||||||
source: ApprovalResolutionSource::User,
|
source: ApprovalResolutionSource::User,
|
||||||
|
cancellation_reason: None,
|
||||||
};
|
};
|
||||||
|
|
||||||
assert!(matches!(
|
assert!(matches!(
|
||||||
@@ -40,6 +42,7 @@ fn approval_resolution_aborts_turn_when_approval_is_aborted() {
|
|||||||
let resolution = ApprovalResolution {
|
let resolution = ApprovalResolution {
|
||||||
decision: ReviewDecision::Abort,
|
decision: ReviewDecision::Abort,
|
||||||
source: ApprovalResolutionSource::User,
|
source: ApprovalResolutionSource::User,
|
||||||
|
cancellation_reason: None,
|
||||||
};
|
};
|
||||||
|
|
||||||
assert!(matches!(
|
assert!(matches!(
|
||||||
@@ -67,6 +70,7 @@ fn approval_resolution_uses_acting_model_timeout_instructions() {
|
|||||||
let resolution = ApprovalResolution {
|
let resolution = ApprovalResolution {
|
||||||
decision: ReviewDecision::TimedOut,
|
decision: ReviewDecision::TimedOut,
|
||||||
source: ApprovalResolutionSource::Guardian,
|
source: ApprovalResolutionSource::Guardian,
|
||||||
|
cancellation_reason: None,
|
||||||
};
|
};
|
||||||
|
|
||||||
assert!(matches!(
|
assert!(matches!(
|
||||||
@@ -167,11 +171,13 @@ async fn non_utf8_cwd_preserves_approval_routing(
|
|||||||
ApprovalResolution {
|
ApprovalResolution {
|
||||||
decision: ReviewDecision::Approved,
|
decision: ReviewDecision::Approved,
|
||||||
source: ApprovalResolutionSource::User,
|
source: ApprovalResolutionSource::User,
|
||||||
|
cancellation_reason: None,
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
ApprovalResolution {
|
ApprovalResolution {
|
||||||
decision: ReviewDecision::Approved,
|
decision: ReviewDecision::Approved,
|
||||||
source: ApprovalResolutionSource::Guardian,
|
source: ApprovalResolutionSource::Guardian,
|
||||||
|
cancellation_reason: None,
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
assert_eq!(approval.await, expected);
|
assert_eq!(approval.await, expected);
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ use std::borrow::Cow;
|
|||||||
|
|
||||||
use crate::session::turn_context::TurnContext;
|
use crate::session::turn_context::TurnContext;
|
||||||
pub(crate) use approvals::ApprovalContext;
|
pub(crate) use approvals::ApprovalContext;
|
||||||
|
pub(crate) use approvals::ApprovalResolution;
|
||||||
pub(crate) use approvals::ApprovalResolutionSource;
|
pub(crate) use approvals::ApprovalResolutionSource;
|
||||||
use codex_features::Feature;
|
use codex_features::Feature;
|
||||||
use codex_protocol::exec_output::ExecToolCallOutput;
|
use codex_protocol::exec_output::ExecToolCallOutput;
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ use codex_app_server_protocol::ClientRequest;
|
|||||||
use codex_app_server_protocol::ConfigWarningNotification;
|
use codex_app_server_protocol::ConfigWarningNotification;
|
||||||
use codex_app_server_protocol::JSONRPCErrorError;
|
use codex_app_server_protocol::JSONRPCErrorError;
|
||||||
use codex_app_server_protocol::McpServerElicitationAction;
|
use codex_app_server_protocol::McpServerElicitationAction;
|
||||||
|
use codex_app_server_protocol::McpServerElicitationRequest;
|
||||||
use codex_app_server_protocol::McpServerElicitationRequestResponse;
|
use codex_app_server_protocol::McpServerElicitationRequestResponse;
|
||||||
use codex_app_server_protocol::RequestId;
|
use codex_app_server_protocol::RequestId;
|
||||||
use codex_app_server_protocol::ReviewStartParams;
|
use codex_app_server_protocol::ReviewStartParams;
|
||||||
@@ -96,6 +97,10 @@ use codex_protocol::SessionId;
|
|||||||
use codex_protocol::ThreadId;
|
use codex_protocol::ThreadId;
|
||||||
use codex_protocol::config_types::ApprovalsReviewer;
|
use codex_protocol::config_types::ApprovalsReviewer;
|
||||||
use codex_protocol::config_types::SandboxMode;
|
use codex_protocol::config_types::SandboxMode;
|
||||||
|
use codex_protocol::mcp_approval_meta::APPROVAL_KIND_KEY;
|
||||||
|
use codex_protocol::mcp_approval_meta::APPROVAL_KIND_MCP_TOOL_CALL;
|
||||||
|
use codex_protocol::mcp_approval_meta::CANCELLATION_REASON_KEY;
|
||||||
|
use codex_protocol::mcp_approval_meta::McpApprovalCancellationReason;
|
||||||
use codex_protocol::models::ActivePermissionProfile;
|
use codex_protocol::models::ActivePermissionProfile;
|
||||||
use codex_protocol::models::PermissionProfile;
|
use codex_protocol::models::PermissionProfile;
|
||||||
use codex_protocol::protocol::AskForApproval;
|
use codex_protocol::protocol::AskForApproval;
|
||||||
@@ -2002,11 +2007,19 @@ fn resume_lookup_model_providers(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn canceled_mcp_server_elicitation_response() -> Result<Value, String> {
|
fn canceled_mcp_server_elicitation_response(
|
||||||
|
request: &McpServerElicitationRequest,
|
||||||
|
) -> Result<Value, String> {
|
||||||
|
let tool_approval = matches!(request, McpServerElicitationRequest::Form { meta: Some(meta), .. }
|
||||||
|
if meta.get(APPROVAL_KIND_KEY).and_then(Value::as_str) == Some(APPROVAL_KIND_MCP_TOOL_CALL));
|
||||||
serde_json::to_value(McpServerElicitationRequestResponse {
|
serde_json::to_value(McpServerElicitationRequestResponse {
|
||||||
action: McpServerElicitationAction::Cancel,
|
action: McpServerElicitationAction::Cancel,
|
||||||
content: None,
|
content: None,
|
||||||
meta: None,
|
meta: tool_approval.then(|| {
|
||||||
|
serde_json::json!({
|
||||||
|
CANCELLATION_REASON_KEY: McpApprovalCancellationReason::NonInteractive,
|
||||||
|
})
|
||||||
|
}),
|
||||||
})
|
})
|
||||||
.map_err(|err| format!("failed to encode mcp elicitation response: {err}"))
|
.map_err(|err| format!("failed to encode mcp elicitation response: {err}"))
|
||||||
}
|
}
|
||||||
@@ -2077,11 +2090,11 @@ async fn handle_server_request(
|
|||||||
) {
|
) {
|
||||||
let method = server_request_method_name(&request);
|
let method = server_request_method_name(&request);
|
||||||
let handle_result = match request {
|
let handle_result = match request {
|
||||||
ServerRequest::McpServerElicitationRequest { request_id, .. } => {
|
ServerRequest::McpServerElicitationRequest { request_id, params } => {
|
||||||
// Exec auto-cancels elicitation instead of surfacing it
|
// Exec auto-cancels elicitation instead of surfacing it
|
||||||
// interactively. Preserve that behavior for attached subagent
|
// interactively. Preserve that behavior for attached subagent
|
||||||
// threads too so we do not turn a cancel into a decline/error.
|
// threads too so we do not turn a cancel into a decline/error.
|
||||||
match canceled_mcp_server_elicitation_response() {
|
match canceled_mcp_server_elicitation_response(¶ms.request) {
|
||||||
Ok(value) => {
|
Ok(value) => {
|
||||||
resolve_server_request(
|
resolve_server_request(
|
||||||
client,
|
client,
|
||||||
|
|||||||
@@ -462,7 +462,14 @@ fn should_backfill_turn_completed_items_backfills_persisted_summaries_only() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn canceled_mcp_server_elicitation_response_uses_cancel_action() {
|
fn canceled_mcp_server_elicitation_response_uses_cancel_action() {
|
||||||
let value = canceled_mcp_server_elicitation_response()
|
let request = serde_json::from_value(serde_json::json!({
|
||||||
|
"mode": "form",
|
||||||
|
"message": "Allow the tool call?",
|
||||||
|
"requestedSchema": { "type": "object", "properties": {} },
|
||||||
|
"_meta": { APPROVAL_KIND_KEY: APPROVAL_KIND_MCP_TOOL_CALL },
|
||||||
|
}))
|
||||||
|
.expect("valid MCP tool approval request");
|
||||||
|
let value = canceled_mcp_server_elicitation_response(&request)
|
||||||
.expect("mcp elicitation cancel response should serialize");
|
.expect("mcp elicitation cancel response should serialize");
|
||||||
let response: McpServerElicitationRequestResponse =
|
let response: McpServerElicitationRequestResponse =
|
||||||
serde_json::from_value(value).expect("cancel response should deserialize");
|
serde_json::from_value(value).expect("cancel response should deserialize");
|
||||||
@@ -472,7 +479,9 @@ fn canceled_mcp_server_elicitation_response_uses_cancel_action() {
|
|||||||
McpServerElicitationRequestResponse {
|
McpServerElicitationRequestResponse {
|
||||||
action: McpServerElicitationAction::Cancel,
|
action: McpServerElicitationAction::Cancel,
|
||||||
content: None,
|
content: None,
|
||||||
meta: None,
|
meta: Some(serde_json::json!({
|
||||||
|
CANCELLATION_REASON_KEY: McpApprovalCancellationReason::NonInteractive,
|
||||||
|
})),
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,6 +17,18 @@ pub const APPROVALS_REVIEWER_KEY: &str = "approvals_reviewer";
|
|||||||
pub const PERSIST_KEY: &str = "persist";
|
pub const PERSIST_KEY: &str = "persist";
|
||||||
pub const PERSIST_SESSION: &str = "session";
|
pub const PERSIST_SESSION: &str = "session";
|
||||||
pub const PERSIST_ALWAYS: &str = "always";
|
pub const PERSIST_ALWAYS: &str = "always";
|
||||||
|
/// Set where cancellation originates, before user actions and runtime failures
|
||||||
|
/// are flattened into the same elicitation action. Absence means unknown.
|
||||||
|
pub const CANCELLATION_REASON_KEY: &str = "codex_approval_cancellation_reason";
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
|
||||||
|
#[serde(rename_all = "snake_case")]
|
||||||
|
pub enum McpApprovalCancellationReason {
|
||||||
|
User,
|
||||||
|
Interrupted,
|
||||||
|
NonInteractive,
|
||||||
|
}
|
||||||
|
|
||||||
pub const SOURCE_KEY: &str = "source";
|
pub const SOURCE_KEY: &str = "source";
|
||||||
pub const SOURCE_CONNECTOR: &str = "connector";
|
pub const SOURCE_CONNECTOR: &str = "connector";
|
||||||
pub const CONNECTOR_ID_KEY: &str = "connector_id";
|
pub const CONNECTOR_ID_KEY: &str = "connector_id";
|
||||||
|
|||||||
@@ -15,6 +15,8 @@ use codex_protocol::ThreadId;
|
|||||||
use codex_protocol::mcp_approval_meta::APPROVAL_KIND_KEY as APPROVAL_META_KIND_KEY;
|
use codex_protocol::mcp_approval_meta::APPROVAL_KIND_KEY as APPROVAL_META_KIND_KEY;
|
||||||
use codex_protocol::mcp_approval_meta::APPROVAL_KIND_MCP_TOOL_CALL as APPROVAL_META_KIND_MCP_TOOL_CALL;
|
use codex_protocol::mcp_approval_meta::APPROVAL_KIND_MCP_TOOL_CALL as APPROVAL_META_KIND_MCP_TOOL_CALL;
|
||||||
use codex_protocol::mcp_approval_meta::APPROVAL_KIND_TOOL_SUGGESTION as APPROVAL_META_KIND_TOOL_SUGGESTION;
|
use codex_protocol::mcp_approval_meta::APPROVAL_KIND_TOOL_SUGGESTION as APPROVAL_META_KIND_TOOL_SUGGESTION;
|
||||||
|
use codex_protocol::mcp_approval_meta::CANCELLATION_REASON_KEY;
|
||||||
|
use codex_protocol::mcp_approval_meta::McpApprovalCancellationReason;
|
||||||
use codex_protocol::mcp_approval_meta::PERSIST_ALWAYS as APPROVAL_PERSIST_ALWAYS_VALUE;
|
use codex_protocol::mcp_approval_meta::PERSIST_ALWAYS as APPROVAL_PERSIST_ALWAYS_VALUE;
|
||||||
use codex_protocol::mcp_approval_meta::PERSIST_KEY as APPROVAL_PERSIST_KEY;
|
use codex_protocol::mcp_approval_meta::PERSIST_KEY as APPROVAL_PERSIST_KEY;
|
||||||
use codex_protocol::mcp_approval_meta::PERSIST_SESSION as APPROVAL_PERSIST_SESSION_VALUE;
|
use codex_protocol::mcp_approval_meta::PERSIST_SESSION as APPROVAL_PERSIST_SESSION_VALUE;
|
||||||
@@ -175,6 +177,7 @@ pub(crate) struct McpServerElicitationFormRequest {
|
|||||||
message: String,
|
message: String,
|
||||||
approval_display_params: Vec<McpToolApprovalDisplayParam>,
|
approval_display_params: Vec<McpToolApprovalDisplayParam>,
|
||||||
response_mode: McpServerElicitationResponseMode,
|
response_mode: McpServerElicitationResponseMode,
|
||||||
|
is_tool_approval: bool,
|
||||||
fields: Vec<McpServerElicitationField>,
|
fields: Vec<McpServerElicitationField>,
|
||||||
tool_suggestion: Option<ToolSuggestionRequest>,
|
tool_suggestion: Option<ToolSuggestionRequest>,
|
||||||
}
|
}
|
||||||
@@ -330,6 +333,7 @@ impl McpServerElicitationFormRequest {
|
|||||||
message,
|
message,
|
||||||
approval_display_params,
|
approval_display_params,
|
||||||
response_mode,
|
response_mode,
|
||||||
|
is_tool_approval,
|
||||||
fields,
|
fields,
|
||||||
tool_suggestion,
|
tool_suggestion,
|
||||||
})
|
})
|
||||||
@@ -1151,7 +1155,11 @@ impl McpServerElicitationOverlay {
|
|||||||
self.request.request_id.clone(),
|
self.request.request_id.clone(),
|
||||||
McpServerElicitationAction::Cancel,
|
McpServerElicitationAction::Cancel,
|
||||||
/*content*/ None,
|
/*content*/ None,
|
||||||
/*meta*/ None,
|
self.request.is_tool_approval.then(|| {
|
||||||
|
serde_json::json!({
|
||||||
|
CANCELLATION_REASON_KEY: McpApprovalCancellationReason::User,
|
||||||
|
})
|
||||||
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1190,7 +1198,14 @@ impl McpServerElicitationOverlay {
|
|||||||
})),
|
})),
|
||||||
),
|
),
|
||||||
Some(APPROVAL_DECLINE_VALUE) => (McpServerElicitationAction::Decline, None),
|
Some(APPROVAL_DECLINE_VALUE) => (McpServerElicitationAction::Decline, None),
|
||||||
Some(APPROVAL_CANCEL_VALUE) => (McpServerElicitationAction::Cancel, None),
|
Some(APPROVAL_CANCEL_VALUE) => (
|
||||||
|
McpServerElicitationAction::Cancel,
|
||||||
|
self.request.is_tool_approval.then(|| {
|
||||||
|
serde_json::json!({
|
||||||
|
CANCELLATION_REASON_KEY: McpApprovalCancellationReason::User,
|
||||||
|
})
|
||||||
|
}),
|
||||||
|
),
|
||||||
_ => (McpServerElicitationAction::Cancel, None),
|
_ => (McpServerElicitationAction::Cancel, None),
|
||||||
};
|
};
|
||||||
self.app_event_tx.resolve_elicitation(
|
self.app_event_tx.resolve_elicitation(
|
||||||
@@ -1875,6 +1890,7 @@ mod tests {
|
|||||||
message: "Allow this request?".to_string(),
|
message: "Allow this request?".to_string(),
|
||||||
approval_display_params: Vec::new(),
|
approval_display_params: Vec::new(),
|
||||||
response_mode: McpServerElicitationResponseMode::FormContent,
|
response_mode: McpServerElicitationResponseMode::FormContent,
|
||||||
|
is_tool_approval: false,
|
||||||
fields: vec![McpServerElicitationField {
|
fields: vec![McpServerElicitationField {
|
||||||
id: "confirmed".to_string(),
|
id: "confirmed".to_string(),
|
||||||
label: "Confirm".to_string(),
|
label: "Confirm".to_string(),
|
||||||
@@ -1945,6 +1961,7 @@ mod tests {
|
|||||||
message: "Allow this request?".to_string(),
|
message: "Allow this request?".to_string(),
|
||||||
approval_display_params: Vec::new(),
|
approval_display_params: Vec::new(),
|
||||||
response_mode: McpServerElicitationResponseMode::ApprovalAction,
|
response_mode: McpServerElicitationResponseMode::ApprovalAction,
|
||||||
|
is_tool_approval: false,
|
||||||
fields: vec![McpServerElicitationField {
|
fields: vec![McpServerElicitationField {
|
||||||
id: APPROVAL_FIELD_ID.to_string(),
|
id: APPROVAL_FIELD_ID.to_string(),
|
||||||
label: String::new(),
|
label: String::new(),
|
||||||
@@ -2002,6 +2019,7 @@ mod tests {
|
|||||||
message: "Allow this request?".to_string(),
|
message: "Allow this request?".to_string(),
|
||||||
approval_display_params: Vec::new(),
|
approval_display_params: Vec::new(),
|
||||||
response_mode: McpServerElicitationResponseMode::ApprovalAction,
|
response_mode: McpServerElicitationResponseMode::ApprovalAction,
|
||||||
|
is_tool_approval: true,
|
||||||
fields: vec![McpServerElicitationField {
|
fields: vec![McpServerElicitationField {
|
||||||
id: APPROVAL_FIELD_ID.to_string(),
|
id: APPROVAL_FIELD_ID.to_string(),
|
||||||
label: String::new(),
|
label: String::new(),
|
||||||
|
|||||||
Reference in New Issue
Block a user