[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:
Ahmed Ibrahim
2026-04-22 20:36:34 +00:00
committed by GitHub
co-authored by Codex
parent 18a26d7bbc
commit 9360f267f3
10 changed files with 1169 additions and 105 deletions
+55 -5
View File
@@ -286,15 +286,19 @@ pub struct HttpRequestParams {
/// Optional request body bytes.
#[serde(default, rename = "bodyBase64")]
pub body: Option<ByteChunk>,
/// Optional request timeout in milliseconds.
#[serde(default)]
/// Request timeout in milliseconds.
///
/// Omitted or `null` disables the timeout. A number applies that exact
/// millisecond deadline.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub timeout_ms: Option<u64>,
/// Caller-chosen stream id for `http/request/bodyDelta` notifications.
///
/// The id must remain unique on a connection until the terminal body delta
/// arrives, even if the caller stops reading the stream earlier.
#[serde(default)]
pub request_id: Option<String>,
/// arrives, even if the caller stops reading the stream earlier. Buffered
/// requests still send an id so callers can keep one consistent request
/// envelope shape.
pub request_id: String,
/// Return after response headers and stream the response body as deltas.
#[serde(default)]
pub stream_response: bool,
@@ -391,3 +395,49 @@ mod base64_bytes {
.map_err(serde::de::Error::custom)
}
}
#[cfg(test)]
mod tests {
use super::HttpRequestParams;
use pretty_assertions::assert_eq;
#[test]
fn http_request_timeout_treats_omitted_and_null_as_no_timeout() {
let omitted: HttpRequestParams = serde_json::from_value(serde_json::json!({
"method": "GET",
"url": "https://example.test",
"requestId": "req-omitted-timeout",
}))
.expect("omitted timeout should deserialize");
let null_timeout: HttpRequestParams = serde_json::from_value(serde_json::json!({
"method": "GET",
"url": "https://example.test",
"requestId": "req-null-timeout",
"timeoutMs": null,
}))
.expect("null timeout should deserialize");
let explicit_timeout: HttpRequestParams = serde_json::from_value(serde_json::json!({
"method": "GET",
"url": "https://example.test",
"requestId": "req-explicit-timeout",
"timeoutMs": 1234,
}))
.expect("numeric timeout should deserialize");
assert_eq!(
(omitted.request_id.as_str(), omitted.timeout_ms),
("req-omitted-timeout", None)
);
assert_eq!(
(null_timeout.request_id.as_str(), null_timeout.timeout_ms),
("req-null-timeout", None)
);
assert_eq!(
(
explicit_timeout.request_id.as_str(),
explicit_timeout.timeout_ms
),
("req-explicit-timeout", Some(1234))
);
}
}