mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
[2/4] Implement executor HTTP request runner (#18582)
### Why Remote streamable HTTP MCP needs the executor to perform ordinary HTTP requests on the executor side. This keeps network placement aligned with `experimental_environment = "remote"` without adding MCP-specific executor APIs. ### What - Add an executor-side `http/request` runner backed by `reqwest`. - Validate request method and URL scheme, preserving the transport boundary at plain HTTP. - Return buffered responses for ordinary calls and emit ordered `http/request/bodyDelta` notifications for streaming responses. - Register the request handler in the exec-server router. - Document the runner entrypoint, conversion helpers, body-stream bridge, notification sender, timeout behavior, and new integration-test helpers. - Add exec-server integration tests with the existing websocket harness and a local TCP HTTP peer for buffered and streamed responses, with comments spelling out what each test proves and its setup/exercise/assert phases. ### Stack 1. #18581 protocol 2. #18582 runner 3. #18583 RMCP client 4. #18584 manager wiring and local/remote coverage ### Verification - `just fmt` - `cargo check -p codex-exec-server -p codex-rmcp-client --tests` - `cargo check -p codex-core --test all` compile-only - `git diff --check` - Online full CI is running from the `full-ci` branch, including the remote Rust test job. Co-authored-by: Codex <noreply@openai.com> --------- Co-authored-by: Codex <noreply@openai.com>
This commit is contained in:
@@ -4,8 +4,16 @@ use std::sync::atomic::AtomicBool;
|
||||
use std::sync::atomic::Ordering;
|
||||
|
||||
use codex_app_server_protocol::JSONRPCErrorError;
|
||||
use codex_app_server_protocol::RequestId;
|
||||
use serde_json::to_value;
|
||||
use std::collections::HashSet;
|
||||
use tokio::sync::Mutex;
|
||||
use tokio_util::sync::CancellationToken;
|
||||
use tokio_util::task::TaskTracker;
|
||||
|
||||
use crate::ExecServerRuntimePaths;
|
||||
use crate::client::http_client::ExecutorHttpRequestRunner;
|
||||
use crate::client::http_client::ExecutorPendingHttpBodyStream;
|
||||
use crate::protocol::ExecParams;
|
||||
use crate::protocol::ExecResponse;
|
||||
use crate::protocol::FsCopyParams;
|
||||
@@ -22,6 +30,7 @@ use crate::protocol::FsRemoveParams;
|
||||
use crate::protocol::FsRemoveResponse;
|
||||
use crate::protocol::FsWriteFileParams;
|
||||
use crate::protocol::FsWriteFileResponse;
|
||||
use crate::protocol::HttpRequestParams;
|
||||
use crate::protocol::InitializeParams;
|
||||
use crate::protocol::InitializeResponse;
|
||||
use crate::protocol::ReadParams;
|
||||
@@ -31,6 +40,8 @@ use crate::protocol::TerminateResponse;
|
||||
use crate::protocol::WriteParams;
|
||||
use crate::protocol::WriteResponse;
|
||||
use crate::rpc::RpcNotificationSender;
|
||||
use crate::rpc::internal_error;
|
||||
use crate::rpc::invalid_params;
|
||||
use crate::rpc::invalid_request;
|
||||
use crate::server::file_system_handler::FileSystemHandler;
|
||||
use crate::server::session_registry::SessionHandle;
|
||||
@@ -40,6 +51,9 @@ pub(crate) struct ExecServerHandler {
|
||||
session_registry: Arc<SessionRegistry>,
|
||||
notifications: RpcNotificationSender,
|
||||
session: StdMutex<Option<SessionHandle>>,
|
||||
active_body_stream_ids: Mutex<HashSet<String>>,
|
||||
background_task_shutdown: CancellationToken,
|
||||
background_tasks: TaskTracker,
|
||||
file_system: FileSystemHandler,
|
||||
initialize_requested: AtomicBool,
|
||||
initialized: AtomicBool,
|
||||
@@ -55,6 +69,9 @@ impl ExecServerHandler {
|
||||
session_registry,
|
||||
notifications,
|
||||
session: StdMutex::new(None),
|
||||
active_body_stream_ids: Mutex::new(HashSet::new()),
|
||||
background_task_shutdown: CancellationToken::new(),
|
||||
background_tasks: TaskTracker::new(),
|
||||
file_system: FileSystemHandler::new(runtime_paths),
|
||||
initialize_requested: AtomicBool::new(false),
|
||||
initialized: AtomicBool::new(false),
|
||||
@@ -62,6 +79,9 @@ impl ExecServerHandler {
|
||||
}
|
||||
|
||||
pub(crate) async fn shutdown(&self) {
|
||||
self.background_task_shutdown.cancel();
|
||||
self.background_tasks.close();
|
||||
self.background_tasks.wait().await;
|
||||
if let Some(session) = self.session() {
|
||||
session.detach().await;
|
||||
}
|
||||
@@ -147,6 +167,47 @@ impl ExecServerHandler {
|
||||
session.process().terminate(params).await
|
||||
}
|
||||
|
||||
pub(crate) async fn http_request(
|
||||
self: &Arc<Self>,
|
||||
request_id: RequestId,
|
||||
params: HttpRequestParams,
|
||||
) -> Result<(), JSONRPCErrorError> {
|
||||
self.require_initialized_for("http")?;
|
||||
let stream_response = params.stream_response;
|
||||
let http_request_id = params.request_id.clone();
|
||||
if stream_response {
|
||||
self.reserve_http_body_stream(&http_request_id).await?;
|
||||
}
|
||||
let response = ExecutorHttpRequestRunner::new(params.timeout_ms)?
|
||||
.run(params)
|
||||
.await;
|
||||
if response.is_err() && stream_response {
|
||||
self.release_http_body_stream(&http_request_id).await;
|
||||
}
|
||||
let (response, mut pending_stream) = response?;
|
||||
let result = match to_value(response) {
|
||||
Ok(result) => result,
|
||||
Err(err) => {
|
||||
if let Some(pending_stream) = pending_stream.take() {
|
||||
self.release_http_body_stream(&pending_stream.request_id)
|
||||
.await;
|
||||
}
|
||||
return Err(internal_error(err.to_string()));
|
||||
}
|
||||
};
|
||||
if let Err(error) = self.notifications.response(request_id, result).await {
|
||||
if let Some(pending_stream) = pending_stream.take() {
|
||||
self.release_http_body_stream(&pending_stream.request_id)
|
||||
.await;
|
||||
}
|
||||
return Err(error);
|
||||
}
|
||||
if let Some(pending_stream) = pending_stream {
|
||||
self.start_http_body_stream(pending_stream).await;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) async fn fs_read_file(
|
||||
&self,
|
||||
params: FsReadFileParams,
|
||||
@@ -242,6 +303,44 @@ impl ExecServerHandler {
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner)
|
||||
.clone()
|
||||
}
|
||||
|
||||
async fn start_http_body_stream(
|
||||
self: &Arc<Self>,
|
||||
pending_stream: ExecutorPendingHttpBodyStream,
|
||||
) {
|
||||
let request_id = pending_stream.request_id.clone();
|
||||
if self.background_task_shutdown.is_cancelled() {
|
||||
self.release_http_body_stream(&request_id).await;
|
||||
return;
|
||||
}
|
||||
let finished_request_id = request_id.clone();
|
||||
let handler = Arc::clone(self);
|
||||
let notifications = self.notifications.clone();
|
||||
let shutdown = self.background_task_shutdown.clone();
|
||||
self.background_tasks.spawn(async move {
|
||||
tokio::select! {
|
||||
_ = shutdown.cancelled() => {}
|
||||
_ = ExecutorHttpRequestRunner::stream_body(pending_stream, notifications) => {}
|
||||
}
|
||||
handler.release_http_body_stream(&finished_request_id).await;
|
||||
});
|
||||
}
|
||||
|
||||
async fn release_http_body_stream(&self, request_id: &str) {
|
||||
let mut active_body_stream_ids = self.active_body_stream_ids.lock().await;
|
||||
active_body_stream_ids.remove(request_id);
|
||||
}
|
||||
|
||||
async fn reserve_http_body_stream(&self, request_id: &str) -> Result<(), JSONRPCErrorError> {
|
||||
let mut active_body_stream_ids = self.active_body_stream_ids.lock().await;
|
||||
if active_body_stream_ids.contains(request_id) {
|
||||
return Err(invalid_params(format!(
|
||||
"http/request streamResponse requestId `{request_id}` is already active"
|
||||
)));
|
||||
}
|
||||
active_body_stream_ids.insert(request_id.to_string());
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
Reference in New Issue
Block a user