mirror of
https://github.com/openai/codex.git
synced 2026-11-10 21:00:47 +00:00
Client-side URL elicitation flow and schema updates
This commit is contained in:
@@ -1779,6 +1779,12 @@
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"elicitation_id": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
@@ -7031,6 +7037,12 @@
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"elicitation_id": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
@@ -8433,4 +8445,4 @@
|
||||
}
|
||||
],
|
||||
"title": "EventMsg"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2840,6 +2840,12 @@
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"elicitation_id": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
@@ -13939,4 +13945,4 @@
|
||||
},
|
||||
"title": "CodexAppServerProtocol",
|
||||
"type": "object"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,4 +3,4 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { JsonValue } from "./serde_json/JsonValue";
|
||||
|
||||
export type ElicitationRequestEvent = { server_name: string, id: string | number, message: string, requested_schema: JsonValue | null, url: string | null, };
|
||||
export type ElicitationRequestEvent = { server_name: string, id: string | number, message: string, requested_schema: JsonValue | null, url: string | null, elicitation_id: string | null, };
|
||||
|
||||
@@ -155,6 +155,9 @@ use crate::instructions::UserInstructions;
|
||||
use crate::mcp::CODEX_APPS_MCP_SERVER_NAME;
|
||||
use crate::mcp::auth::compute_auth_statuses;
|
||||
use crate::mcp::effective_mcp_servers;
|
||||
use crate::mcp::elicitation_url::build_url_elicitation_outcome_from_user_input;
|
||||
use crate::mcp::elicitation_url::build_url_elicitation_request_user_input_args;
|
||||
use crate::mcp::elicitation_url::is_url_elicitation_request;
|
||||
use crate::mcp::is_apps_mcp_gateway_elicitation_flow_active;
|
||||
use crate::mcp::maybe_prompt_and_install_mcp_dependencies;
|
||||
use crate::mcp::with_codex_apps_mcp;
|
||||
@@ -2330,10 +2333,18 @@ impl Session {
|
||||
&self,
|
||||
elicitation: codex_protocol::approvals::ElicitationRequestEvent,
|
||||
) {
|
||||
let response = if let Some((turn_context, cancellation_token)) =
|
||||
let (response, completion_elicitation_id) = if let Some((
|
||||
turn_context,
|
||||
cancellation_token,
|
||||
)) =
|
||||
self.active_turn_context_and_cancellation_token().await
|
||||
{
|
||||
let args = build_mcp_elicitation_request_user_input_args(&elicitation);
|
||||
let is_url_elicitation = is_url_elicitation_request(&elicitation);
|
||||
let args = if is_url_elicitation {
|
||||
build_url_elicitation_request_user_input_args(&elicitation)
|
||||
} else {
|
||||
build_mcp_elicitation_request_user_input_args(&elicitation)
|
||||
};
|
||||
let call_id = format!("mcp_elicitation_request_{}", turn_context.sub_id);
|
||||
let turn_sub_id = turn_context.sub_id.clone();
|
||||
let user_input = tokio::select! {
|
||||
@@ -2347,23 +2358,47 @@ impl Session {
|
||||
}
|
||||
response = self.request_user_input(turn_context.as_ref(), call_id, args) => response,
|
||||
};
|
||||
build_mcp_elicitation_response_from_user_input(user_input, &elicitation)
|
||||
} else {
|
||||
ElicitationResponse {
|
||||
action: ElicitationAction::Cancel,
|
||||
content: None,
|
||||
if is_url_elicitation {
|
||||
let outcome =
|
||||
build_url_elicitation_outcome_from_user_input(user_input, &elicitation);
|
||||
(outcome.response, outcome.completion_elicitation_id)
|
||||
} else {
|
||||
(
|
||||
build_mcp_elicitation_response_from_user_input(user_input, &elicitation),
|
||||
None,
|
||||
)
|
||||
}
|
||||
} else {
|
||||
(
|
||||
ElicitationResponse {
|
||||
action: ElicitationAction::Cancel,
|
||||
content: None,
|
||||
},
|
||||
None,
|
||||
)
|
||||
};
|
||||
|
||||
let server_name = elicitation.server_name.clone();
|
||||
let request_id = protocol_request_id_to_rmcp(&elicitation.id);
|
||||
if let Err(err) = self
|
||||
.resolve_elicitation(elicitation.server_name, request_id, response)
|
||||
.resolve_elicitation(server_name.clone(), request_id, response)
|
||||
.await
|
||||
{
|
||||
warn!(
|
||||
error = %err,
|
||||
"failed to resolve codex_apps MCP elicitation request"
|
||||
);
|
||||
return;
|
||||
}
|
||||
if let Some(elicitation_id) = completion_elicitation_id
|
||||
&& let Err(err) = self
|
||||
.complete_url_elicitation(server_name, elicitation_id)
|
||||
.await
|
||||
{
|
||||
warn!(
|
||||
error = %err,
|
||||
"failed to send codex_apps url elicitation completion notification"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2667,6 +2702,19 @@ impl Session {
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn complete_url_elicitation(
|
||||
&self,
|
||||
server_name: String,
|
||||
elicitation_id: String,
|
||||
) -> anyhow::Result<()> {
|
||||
self.services
|
||||
.mcp_connection_manager
|
||||
.read()
|
||||
.await
|
||||
.complete_url_elicitation(server_name, elicitation_id)
|
||||
.await
|
||||
}
|
||||
|
||||
/// Records input items: always append to conversation history and
|
||||
/// persist these response items to rollout.
|
||||
pub(crate) async fn record_conversation_items(
|
||||
|
||||
@@ -0,0 +1,135 @@
|
||||
use codex_protocol::approvals::ElicitationRequestEvent;
|
||||
use codex_protocol::request_user_input::RequestUserInputAnswer;
|
||||
use codex_protocol::request_user_input::RequestUserInputArgs;
|
||||
use codex_protocol::request_user_input::RequestUserInputQuestion;
|
||||
use codex_protocol::request_user_input::RequestUserInputQuestionOption;
|
||||
use codex_protocol::request_user_input::RequestUserInputResponse;
|
||||
use codex_rmcp_client::ElicitationAction;
|
||||
use codex_rmcp_client::ElicitationResponse;
|
||||
|
||||
const REQUEST_USER_INPUT_NOTE_PREFIX: &str = "user_note: ";
|
||||
const MCP_URL_ELICITATION_DECISION_QUESTION_ID: &str = "mcp_url_elicitation_decision";
|
||||
const MCP_URL_ELICITATION_COMPLETED: &str = "Completed";
|
||||
const MCP_URL_ELICITATION_DECLINE: &str = "Decline";
|
||||
const MCP_URL_ELICITATION_CANCEL: &str = "Cancel";
|
||||
|
||||
pub(crate) fn is_url_elicitation_request(elicitation: &ElicitationRequestEvent) -> bool {
|
||||
elicitation.url.is_some()
|
||||
}
|
||||
|
||||
pub(crate) struct UrlElicitationPromptOutcome {
|
||||
pub(crate) response: ElicitationResponse,
|
||||
pub(crate) completion_elicitation_id: Option<String>,
|
||||
}
|
||||
|
||||
pub(crate) fn build_url_elicitation_request_user_input_args(
|
||||
elicitation: &ElicitationRequestEvent,
|
||||
) -> RequestUserInputArgs {
|
||||
let mut question_lines = vec![elicitation.message.clone()];
|
||||
if let Some(url) = &elicitation.url {
|
||||
question_lines.push(String::new());
|
||||
question_lines.push("Open this URL in your browser to continue:".to_string());
|
||||
question_lines.push(url.clone());
|
||||
question_lines.push(String::new());
|
||||
question_lines.push(
|
||||
"After completing the flow in your browser, return here and choose Completed."
|
||||
.to_string(),
|
||||
);
|
||||
}
|
||||
if elicitation.elicitation_id.is_none() {
|
||||
question_lines.push(String::new());
|
||||
question_lines.push(
|
||||
"This request is missing required URL elicitation metadata, so it cannot be completed safely."
|
||||
.to_string(),
|
||||
);
|
||||
}
|
||||
|
||||
RequestUserInputArgs {
|
||||
questions: vec![RequestUserInputQuestion {
|
||||
id: MCP_URL_ELICITATION_DECISION_QUESTION_ID.to_string(),
|
||||
header: "MCP URL elicitation".to_string(),
|
||||
question: question_lines.join("\n"),
|
||||
is_other: false,
|
||||
is_secret: false,
|
||||
options: Some(vec![
|
||||
RequestUserInputQuestionOption {
|
||||
label: MCP_URL_ELICITATION_COMPLETED.to_string(),
|
||||
description: "You completed the browser flow and want to continue.".to_string(),
|
||||
},
|
||||
RequestUserInputQuestionOption {
|
||||
label: MCP_URL_ELICITATION_DECLINE.to_string(),
|
||||
description: "Decline this URL elicitation request.".to_string(),
|
||||
},
|
||||
RequestUserInputQuestionOption {
|
||||
label: MCP_URL_ELICITATION_CANCEL.to_string(),
|
||||
description: "Cancel this URL elicitation request.".to_string(),
|
||||
},
|
||||
]),
|
||||
}],
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn build_url_elicitation_outcome_from_user_input(
|
||||
response: Option<RequestUserInputResponse>,
|
||||
elicitation: &ElicitationRequestEvent,
|
||||
) -> UrlElicitationPromptOutcome {
|
||||
let Some(response) = response else {
|
||||
return UrlElicitationPromptOutcome {
|
||||
response: ElicitationResponse {
|
||||
action: ElicitationAction::Cancel,
|
||||
content: None,
|
||||
},
|
||||
completion_elicitation_id: None,
|
||||
};
|
||||
};
|
||||
|
||||
let action = response
|
||||
.answers
|
||||
.get(MCP_URL_ELICITATION_DECISION_QUESTION_ID)
|
||||
.and_then(request_user_input_answer_to_url_elicitation_action)
|
||||
.unwrap_or(ElicitationAction::Cancel);
|
||||
|
||||
match action {
|
||||
ElicitationAction::Accept => {
|
||||
let Some(elicitation_id) = elicitation.elicitation_id.clone() else {
|
||||
return UrlElicitationPromptOutcome {
|
||||
response: ElicitationResponse {
|
||||
action: ElicitationAction::Cancel,
|
||||
content: None,
|
||||
},
|
||||
completion_elicitation_id: None,
|
||||
};
|
||||
};
|
||||
UrlElicitationPromptOutcome {
|
||||
response: ElicitationResponse {
|
||||
action: ElicitationAction::Accept,
|
||||
content: None,
|
||||
},
|
||||
completion_elicitation_id: Some(elicitation_id),
|
||||
}
|
||||
}
|
||||
ElicitationAction::Decline | ElicitationAction::Cancel => UrlElicitationPromptOutcome {
|
||||
response: ElicitationResponse {
|
||||
action,
|
||||
content: None,
|
||||
},
|
||||
completion_elicitation_id: None,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn request_user_input_answer_to_url_elicitation_action(
|
||||
answer: &RequestUserInputAnswer,
|
||||
) -> Option<ElicitationAction> {
|
||||
answer.answers.iter().find_map(|entry| {
|
||||
if entry.starts_with(REQUEST_USER_INPUT_NOTE_PREFIX) {
|
||||
return None;
|
||||
}
|
||||
match entry.as_str() {
|
||||
MCP_URL_ELICITATION_COMPLETED => Some(ElicitationAction::Accept),
|
||||
MCP_URL_ELICITATION_DECLINE => Some(ElicitationAction::Decline),
|
||||
MCP_URL_ELICITATION_CANCEL => Some(ElicitationAction::Cancel),
|
||||
_ => None,
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
pub mod auth;
|
||||
pub(crate) mod elicitation_form;
|
||||
pub(crate) mod elicitation_url;
|
||||
mod skill_dependencies;
|
||||
pub(crate) use skill_dependencies::maybe_prompt_and_install_mcp_dependencies;
|
||||
|
||||
|
||||
@@ -309,7 +309,7 @@ impl ElicitationRequestManager {
|
||||
}
|
||||
rmcp::model::NumberOrString::Number(value) => ProtocolRequestId::Integer(value),
|
||||
};
|
||||
let (message, requested_schema, url) = match elicitation {
|
||||
let (message, requested_schema, url, elicitation_id) = match elicitation {
|
||||
CreateElicitationRequestParams::FormElicitationParams {
|
||||
message,
|
||||
requested_schema,
|
||||
@@ -322,10 +322,19 @@ impl ElicitationRequestManager {
|
||||
None
|
||||
},
|
||||
None,
|
||||
None,
|
||||
),
|
||||
CreateElicitationRequestParams::UrlElicitationParams {
|
||||
message, url, ..
|
||||
} => (message, None, mcp_elicitations_enabled.then_some(url)),
|
||||
message,
|
||||
url,
|
||||
elicitation_id,
|
||||
..
|
||||
} => (
|
||||
message,
|
||||
None,
|
||||
mcp_elicitations_enabled.then_some(url),
|
||||
mcp_elicitations_enabled.then_some(elicitation_id),
|
||||
),
|
||||
};
|
||||
let _ = tx_event
|
||||
.send(Event {
|
||||
@@ -336,6 +345,7 @@ impl ElicitationRequestManager {
|
||||
message,
|
||||
requested_schema,
|
||||
url,
|
||||
elicitation_id,
|
||||
}),
|
||||
})
|
||||
.await;
|
||||
@@ -705,6 +715,24 @@ impl McpConnectionManager {
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn complete_url_elicitation(
|
||||
&self,
|
||||
server_name: String,
|
||||
elicitation_id: String,
|
||||
) -> Result<()> {
|
||||
let managed_client = self.client_by_name(&server_name).await?;
|
||||
managed_client
|
||||
.client
|
||||
.send_custom_notification(
|
||||
"notifications/elicitation/complete",
|
||||
Some(serde_json::json!({ "elicitationId": elicitation_id })),
|
||||
)
|
||||
.await
|
||||
.with_context(|| {
|
||||
format!("failed sending url elicitation completion for `{server_name}`")
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) async fn wait_for_server_ready(&self, server_name: &str, timeout: Duration) -> bool {
|
||||
let Some(async_managed_client) = self.clients.get(server_name) else {
|
||||
return false;
|
||||
@@ -1237,7 +1265,7 @@ fn elicitation_capability_for_server(server_name: &str) -> Option<ElicitationCap
|
||||
form: Some(FormElicitationCapability {
|
||||
schema_validation: None,
|
||||
}),
|
||||
url: None,
|
||||
url: Some(UrlElicitationCapability {}),
|
||||
})
|
||||
} else {
|
||||
None
|
||||
@@ -2117,7 +2145,7 @@ mod tests {
|
||||
.boxed()
|
||||
.shared();
|
||||
let approval_policy = Constrained::allow_any(AskForApproval::OnFailure);
|
||||
let mut manager = McpConnectionManager::new_uninitialized(&approval_policy, false );
|
||||
let mut manager = McpConnectionManager::new_uninitialized(&approval_policy, false);
|
||||
let startup_complete = Arc::new(std::sync::atomic::AtomicBool::new(true));
|
||||
manager.clients.insert(
|
||||
CODEX_APPS_MCP_SERVER_NAME.to_string(),
|
||||
@@ -2145,7 +2173,7 @@ mod tests {
|
||||
form: Some(FormElicitationCapability {
|
||||
schema_validation: None
|
||||
}),
|
||||
url: None,
|
||||
url: Some(UrlElicitationCapability {}),
|
||||
})
|
||||
));
|
||||
|
||||
|
||||
@@ -107,6 +107,8 @@ pub struct ElicitationRequestEvent {
|
||||
pub requested_schema: Option<Value>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub url: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub elicitation_id: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq, JsonSchema, TS)]
|
||||
|
||||
@@ -3277,6 +3277,7 @@ async fn mcp_tool_call_begin_elicitation_end_deferred_during_active_stream() {
|
||||
"properties": {},
|
||||
})),
|
||||
url: Some("https://example.com/elicitation".to_string()),
|
||||
elicitation_id: Some("elicitation-url-1".to_string()),
|
||||
}),
|
||||
});
|
||||
chat.handle_codex_event(Event {
|
||||
@@ -3349,6 +3350,7 @@ async fn mcp_tool_call_elicitation_can_be_resolved_after_stream_queue_flush() {
|
||||
"properties": {},
|
||||
})),
|
||||
url: None,
|
||||
elicitation_id: None,
|
||||
}),
|
||||
});
|
||||
chat.handle_codex_event(Event {
|
||||
|
||||
Reference in New Issue
Block a user