mirror of
https://github.com/openai/codex.git
synced 2026-04-28 10:21:06 +03:00
6.9 KiB
6.9 KiB
DOs
- Enable elicitation capability: Advertise support with an empty object per spec.
use serde_json::json;
use mcp_types::ClientCapabilities;
let capabilities = ClientCapabilities {
experimental: None,
roots: None,
sampling: None,
// Spec requires an empty object when enabled.
elicitation: Some(json!({})),
};
- Escape commands for display: Quote arguments safely before prompting users.
use shlex;
let escaped_command = shlex::try_join(command.iter().map(|s| s.as_str()))
.unwrap_or_else(|_| command.join(" "));
let message = format!("Allow Codex to run `{}` in {:?}?", escaped_command, cwd);
- Send MCP elicitation requests and await responses off-thread: Return a oneshot receiver from
send_request()andtokio::spawna task to wait for it.
use mcp_types::ElicitRequest;
use serde_json::json;
let params = json!({
"message": message,
"requestedSchema": { "type": "object", "properties": {} },
// Correlation helpers:
"codex_elicitation": "exec-approval",
"codex_mcp_tool_call_id": sub_id.clone(),
"codex_event_id": event.id.clone(),
"codex_command": command,
"codex_cwd": cwd.to_string_lossy(),
});
let rx = outgoing.send_request(ElicitRequest::METHOD, Some(params)).await;
let codex = codex.clone();
let event_id = event.id.clone();
tokio::spawn(async move {
on_exec_approval_response(event_id, rx, codex).await;
});
- Correlate JSON-RPC requests with responses: Store a callback per
RequestId, remove it on response, and notify the waiter.
use std::{collections::HashMap, sync::atomic::{AtomicI64, Ordering}};
use tokio::sync::{mpsc, oneshot, Mutex};
use mcp_types::{RequestId, Result};
struct OutgoingMessageSender {
next_request_id: AtomicI64,
sender: mpsc::Sender<OutgoingMessage>,
request_id_to_callback: Mutex<HashMap<RequestId, oneshot::Sender<Result>>>,
}
impl OutgoingMessageSender {
async fn send_request(&self, method: &str, params: Option<serde_json::Value>)
-> oneshot::Receiver<Result> {
let id = RequestId::Integer(self.next_request_id.fetch_add(1, Ordering::Relaxed));
let (tx, rx) = oneshot::channel();
self.request_id_to_callback.lock().await.insert(id.clone(), tx);
let _ = self.sender.send(OutgoingMessage::Request(OutgoingRequest {
id, method: method.to_string(), params
})).await;
rx
}
async fn notify_client_response(&self, id: RequestId, result: Result) {
if let Some((_id, tx)) = self.request_id_to_callback.lock().await.remove_entry(&id) {
let _ = tx.send(result);
}
}
}
- Make response handling async end-to-end: Let the processor await notifying callbacks.
pub(crate) async fn process_response(&mut self, response: JSONRPCResponse) {
let JSONRPCResponse { id, result, .. } = response;
self.outgoing.notify_client_response(id, result).await;
}
- Ensure hashability for map keys: Derive
HashandEqforRequestId(and other untagged enums used as keys).
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, Hash, Eq)]
#[serde(untagged)]
pub enum RequestId {
String(String),
Integer(i64),
}
- Clone IDs you still need: Avoid moving
sub_idinto a struct if used later.
let submission = Submission {
id: sub_id.clone(),
op: Op::UserInput { items: vec![InputItem::Text { text: prompt, annotations: None }] },
};
- Validate and forward elicitation decisions: Deserialize the client response and submit
Op::ExecApproval.
#[derive(Deserialize)]
struct ExecApprovalResponse { decision: ReviewDecision }
async fn on_exec_approval_response(
event_id: String,
rx: tokio::sync::oneshot::Receiver<mcp_types::Result>,
codex: std::sync::Arc<Codex>,
) {
let value = match rx.await {
Ok(v) => v,
Err(err) => { tracing::error!("request failed: {err:?}"); return; }
};
let resp: ExecApprovalResponse = match serde_json::from_value(value) {
Ok(r) => r,
Err(err) => { tracing::error!("failed to deserialize ExecApprovalResponse: {err}"); return; }
};
if let Err(err) = codex.submit(Op::ExecApproval { id: event_id, decision: resp.decision }).await {
tracing::error!("failed to submit ExecApproval: {err}");
}
}
DON’Ts
- Block the event loop waiting for approval: Don’t
.awaitthe elicitation response inline where events are processed.
// ❌ Avoid blocking here:
// let result = outgoing.send_request(...).await; result.await; // blocks the loop
// ✅ Instead, spawn a task to await:
let rx = outgoing.send_request(...).await;
tokio::spawn(async move { on_exec_approval_response(event_id, rx, codex).await; });
- Forget to remove callbacks on response: Don’t leave entries in the
HashMapand leak memory.
// ❌ Wrong: leaving the sender in the map
// self.request_id_to_callback.lock().await.get(&id).unwrap().send(result);
// ✅ Right: remove then send
if let Some((_id, tx)) = self.request_id_to_callback.lock().await.remove_entry(&id) {
let _ = tx.send(result);
}
- Use non-hashable IDs as map keys: Don’t omit
Hash/Eqon enums stored inHashMap.
// ❌ Missing Hash/Eq will fail at compile time
// ✅ Derive Hash/Eq
#[derive(Hash, Eq, PartialEq, Debug, Clone, Deserialize, Serialize)]
#[serde(untagged)]
enum RequestId { String(String), Integer(i64) }
- Display raw unquoted commands: Don’t show
["git", "commit", "-m", "a b"]asgit commit -m a b.
// ❌ Raw join can be misleading:
let msg = format!("Run {}?", command.join(" "));
// ✅ Quote safely:
let msg = format!("Run `{}`?", shlex::try_join(command.iter().map(|s| s.as_str()))
.unwrap_or_else(|_| command.join(" ")));
- Misdeclare elicitation capability: Don’t set
elicitation: Noneor a non-empty schema object.
// ❌ Wrong:
let capabilities = ClientCapabilities { elicitation: None, /* ... */ };
// ❌ Also wrong (non-empty custom fields):
let capabilities = ClientCapabilities { elicitation: Some(json!({"foo":"bar"})), /* ... */ };
// ✅ Correct (empty object):
let capabilities = ClientCapabilities { elicitation: Some(serde_json::json!({})), /* ... */ };
- Move IDs you still need: Don’t consume
sub_idorevent.idif referenced later.
// ❌ Moved into struct, unusable later:
// let submission = Submission { id: sub_id, /* ... */ }; use(sub_id); // error
// ✅ Clone before move:
let submission = Submission { id: sub_id.clone(), /* ... */ }; use(sub_id);
- Ignore error paths from channels/deserialization: Don’t unwrap; log and return gracefully.
// ❌ Panics on failure:
// let value = rx.await.unwrap();
// let resp: ExecApprovalResponse = serde_json::from_value(value).unwrap();
// ✅ Robust handling:
let value = match rx.await { Ok(v) => v, Err(e) => { tracing::error!("{e:?}"); return; } };
let resp: ExecApprovalResponse = match serde_json::from_value(value) {
Ok(r) => r, Err(e) => { tracing::error!("{e}"); return; }
};