mirror of
https://github.com/openai/codex.git
synced 2026-11-11 06:31:37 +00:00
Support remote code-mode hosts in app-server (#35098)
## What changed - Add `--code-mode-host ws://...` and `wss://...` support to `codex app-server`, gated by the `code_mode_host` feature. When omitted, app-server continues to start a local host. - Share one remote WebSocket connection across the process's threads, using the configured HTTP client's proxy and TLS policy and preserving the existing framed host protocol. - Reject invalid host URLs, bound WebSocket frame sizes, close connections cleanly, and return an error when a connection exceeds 1,024 pending delegate calls without disconnecting it. ## Testing - Cover CLI validation, WebSocket protocol execution and shutdown, connection sharing across app-server threads, and delegate-call capacity recovery. GitOrigin-RevId: 715e82d4d9db1e7e2f91b754a777dcab504e2ae4
This commit is contained in:
1 parent
0dfa778dae
commit
f61b51ddd9
28 files changed
+977
-65
No files matched your search
@@ -1,15 +1,37 @@
|
||||
use std::io;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
use codex_code_mode_protocol::CodeModeSessionProvider;
|
||||
use codex_code_mode_protocol::ExecuteRequest;
|
||||
use codex_code_mode_protocol::FunctionCallOutputContentItem;
|
||||
use codex_code_mode_protocol::RuntimeResponse;
|
||||
use codex_code_mode_protocol::host::CapabilitySet;
|
||||
use codex_code_mode_protocol::host::ClientToHost;
|
||||
use codex_code_mode_protocol::host::EncodedFrame;
|
||||
use codex_code_mode_protocol::host::HostHello;
|
||||
use codex_code_mode_protocol::host::HostRequest;
|
||||
use codex_code_mode_protocol::host::HostResponse;
|
||||
use codex_code_mode_protocol::host::HostToClient;
|
||||
use codex_code_mode_protocol::host::ProtocolVersion;
|
||||
use codex_code_mode_protocol::host::WireCellId;
|
||||
use codex_code_mode_protocol::host::WireContentItem;
|
||||
use codex_code_mode_protocol::host::WireResult;
|
||||
use codex_code_mode_protocol::host::WireRuntimeResponse;
|
||||
use codex_http_client::HttpClientFactory;
|
||||
use codex_http_client::OutboundProxyPolicy;
|
||||
use futures::SinkExt;
|
||||
use futures::StreamExt;
|
||||
use pretty_assertions::assert_eq;
|
||||
use tokio::net::TcpListener;
|
||||
use tokio::time::timeout;
|
||||
use tokio_tungstenite::accept_async;
|
||||
use tokio_tungstenite::tungstenite::Message;
|
||||
|
||||
use super::ProcessOwnedCodeModeSession;
|
||||
use super::ProcessOwnedCodeModeSessionProvider;
|
||||
use super::WebSocketCodeModeSessionProvider;
|
||||
use super::resolve_host_program;
|
||||
use crate::NoopCodeModeSessionDelegate;
|
||||
|
||||
@@ -107,6 +129,150 @@ async fn provider_falls_back_to_in_process_session_when_host_is_missing() {
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn websocket_provider_executes_over_shared_connector() {
|
||||
let listener = TcpListener::bind("127.0.0.1:0")
|
||||
.await
|
||||
.expect("websocket test listener should bind");
|
||||
let websocket_url = format!(
|
||||
"ws://{}",
|
||||
listener
|
||||
.local_addr()
|
||||
.expect("websocket test listener should have an address")
|
||||
);
|
||||
let server = tokio::spawn(async move {
|
||||
let (stream, _) = listener
|
||||
.accept()
|
||||
.await
|
||||
.expect("websocket test host should accept a connection");
|
||||
let mut websocket = accept_async(stream)
|
||||
.await
|
||||
.expect("websocket test host should complete the HTTP handshake");
|
||||
|
||||
while let Some(message) = websocket.next().await {
|
||||
let message = message.expect("websocket test host should receive a valid message");
|
||||
let frame = match message {
|
||||
Message::Binary(frame) => frame,
|
||||
Message::Ping(_) | Message::Pong(_) => continue,
|
||||
Message::Close(_) => break,
|
||||
Message::Text(_) | Message::Frame(_) => {
|
||||
panic!("websocket test host received an unexpected message: {message:?}");
|
||||
}
|
||||
};
|
||||
let request = EncodedFrame::decode_framed::<ClientToHost>(&frame)
|
||||
.expect("websocket test host should decode a framed protocol message");
|
||||
let responses = match request {
|
||||
ClientToHost::ClientHello(_) => vec![HostToClient::HostHello(HostHello::new(
|
||||
ProtocolVersion::V1,
|
||||
CapabilitySet::empty(),
|
||||
))],
|
||||
ClientToHost::Request {
|
||||
id,
|
||||
request: HostRequest::OpenSession { session_id },
|
||||
} => vec![HostToClient::Response {
|
||||
id,
|
||||
result: WireResult::Ok {
|
||||
value: HostResponse::SessionReady { session_id },
|
||||
},
|
||||
}],
|
||||
ClientToHost::Request {
|
||||
id,
|
||||
request: HostRequest::Execute { request, .. },
|
||||
} => {
|
||||
assert_eq!(request.source, "text('shared connector')");
|
||||
let cell_id = WireCellId::new("1");
|
||||
vec![
|
||||
HostToClient::Response {
|
||||
id,
|
||||
result: WireResult::Ok {
|
||||
value: HostResponse::ExecutionStarted {
|
||||
cell_id: cell_id.clone(),
|
||||
},
|
||||
},
|
||||
},
|
||||
HostToClient::InitialResponse {
|
||||
id,
|
||||
result: WireResult::Ok {
|
||||
value: WireRuntimeResponse::Result {
|
||||
cell_id,
|
||||
content_items: vec![WireContentItem::InputText {
|
||||
text: "shared connector".to_string(),
|
||||
}],
|
||||
error_text: None,
|
||||
},
|
||||
},
|
||||
},
|
||||
]
|
||||
}
|
||||
ClientToHost::Request {
|
||||
id,
|
||||
request: HostRequest::ShutdownSession { session_id },
|
||||
} => vec![HostToClient::Response {
|
||||
id,
|
||||
result: WireResult::Ok {
|
||||
value: HostResponse::SessionClosed { session_id },
|
||||
},
|
||||
}],
|
||||
request => {
|
||||
panic!("websocket test host received an unexpected request: {request:?}")
|
||||
}
|
||||
};
|
||||
|
||||
for response in responses {
|
||||
let frame = EncodedFrame::encode(&response)
|
||||
.expect("websocket test host should encode a framed response");
|
||||
websocket
|
||||
.send(Message::Binary(frame.into_framed_bytes().into()))
|
||||
.await
|
||||
.expect("websocket test host should send its response");
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let provider = WebSocketCodeModeSessionProvider::with_http_client_factory(
|
||||
websocket_url,
|
||||
HttpClientFactory::new(OutboundProxyPolicy::ReqwestDefault),
|
||||
);
|
||||
let session = provider
|
||||
.create_session(Arc::new(NoopCodeModeSessionDelegate))
|
||||
.await
|
||||
.expect("shared websocket connector should open a code-mode session");
|
||||
let response = session
|
||||
.execute(ExecuteRequest {
|
||||
tool_call_id: "shared-websocket".to_string(),
|
||||
enabled_tools: Vec::new(),
|
||||
source: "text('shared connector')".to_string(),
|
||||
yield_time_ms: None,
|
||||
max_output_tokens: None,
|
||||
})
|
||||
.await
|
||||
.expect("shared websocket connector should start a cell")
|
||||
.initial_response()
|
||||
.await
|
||||
.expect("shared websocket connector should return a cell result");
|
||||
|
||||
assert_eq!(
|
||||
response,
|
||||
RuntimeResponse::Result {
|
||||
cell_id: codex_code_mode_protocol::CellId::new("1".to_string()),
|
||||
content_items: vec![FunctionCallOutputContentItem::InputText {
|
||||
text: "shared connector".to_string(),
|
||||
}],
|
||||
error_text: None,
|
||||
}
|
||||
);
|
||||
session
|
||||
.shutdown()
|
||||
.await
|
||||
.expect("shared websocket connector should shut down its session");
|
||||
drop(session);
|
||||
drop(provider);
|
||||
timeout(Duration::from_secs(5), server)
|
||||
.await
|
||||
.expect("websocket test host should disconnect promptly")
|
||||
.expect("websocket test host task should succeed");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn shutdown_before_open_does_not_spawn_the_host() {
|
||||
let session = ProcessOwnedCodeModeSession::new();
|
||||
|
||||
Reference in new issue
Block a user